Compare commits

..

32 Commits

Author SHA1 Message Date
Sudowoodo Release Bot
c3e6746fa9 Bump v15.0.0 2021-09-21 11:50:28 -07:00
Samuel Attard
436c3ba644 Revert "Bump v15.0.0"
This reverts commit dc6f951f43.
2021-09-21 11:48:38 -07:00
Samuel Attard
599babae00 Revert "Bump v15.0.1"
This reverts commit 5f7da14447.
2021-09-21 11:48:31 -07:00
Sudowoodo Release Bot
5f7da14447 Bump v15.0.1 2021-09-21 11:43:53 -07:00
Sudowoodo Release Bot
dc6f951f43 Bump v15.0.0 2021-09-21 11:40:26 -07:00
trop[bot]
f4fa6c0cf4 fix: propagate window.open settings to child window (#31049)
Co-authored-by: VerteDinde <khammond@slack-corp.com>
2021-09-21 10:34:32 -07:00
trop[bot]
98de50451a fix: suppress insecure resource warning for more local hostnames (#31036) 2021-09-21 10:48:26 +02:00
Sudowoodo Release Bot
c78ab7a1f2 Revert "Bump v15.0.0"
This reverts commit 0928f322fa.
2021-09-20 17:16:32 -07:00
Sudowoodo Release Bot
0928f322fa Bump v15.0.0 2021-09-20 11:31:33 -07:00
trop[bot]
c6c59bf0b3 build: fix release CI jobs start script (#31026)
* build: fix release CI jobs start script

This broke in #30492, we weren't handled 20X status codes and weren't authing to appveyor correctly.

* build: do not pass undefined to Auth header in CI scripts

(cherry picked from commit a48968c1ce)

* build: use basic auth to trigger CI if either a username OR password is provided

(cherry picked from commit d1bd9afbbf)

* build: manually pull 64bit dugite for 32bit tests (#30531)

(cherry picked from commit 8b9d0092cb)

Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>
Co-authored-by: Samuel Attard <sam@electronjs.org>
2021-09-20 11:02:51 -07:00
Keeley Hammond
9d3bcfc559 fix: ensure web_contents() is alive before grabbing view (#31027) 2021-09-20 10:14:12 -07:00
Sudowoodo Release Bot
fb2473bb5e Revert "Bump v15.0.0-beta.8"
This reverts commit 18c5401d08.
2021-09-20 07:43:58 -07:00
electron-roller[bot]
4664b5da57 chore: bump chromium to 94.0.4606.51 (15-x-y) (#30895)
* chore: bump chromium in DEPS to 94.0.4606.41

* chore: bump chromium in DEPS to 94.0.4606.50

* chore: bump chromium in DEPS to 94.0.4606.51

* chore: update patches

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: VerteDinde <keeleymhammond@gmail.com>
2021-09-20 10:42:16 -04:00
Sudowoodo Release Bot
18c5401d08 Bump v15.0.0-beta.8 2021-09-20 06:31:53 -07:00
trop[bot]
3f81697e48 fix: disabling and enabling resizability on macOS (#31014)
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2021-09-20 09:34:55 +09:00
trop[bot]
a00a43ba25 chore: update links of documentation of chromes (#31008)
chrome now use developer.chrome.com/docs/extensions/* instead of developer.chrome.com/extensions/*

Co-authored-by: 祈緒ちゃん - Kiochan <sunxingchen@live.com>
2021-09-17 22:10:48 +09:00
trop[bot]
ce1435be30 fix: links to images (#31005)
Images that used the inline link format do not show up on Docusaurus or
the old website infrastructure. There are only 2 guides using it so it
is faster to change the format rather than figuring out why the parsin
logic does not work.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Ref: https://github.com/electron/electronjs.org-new/issues/84

Co-authored-by: Antón Molleda <amolleda@gmail.com>
2021-09-17 16:49:03 +09:00
trop[bot]
c625b77bde fix: add casing for WCO edge (#30995)
Co-authored-by: mlaurencin <mlaurencin@electronjs.org>
2021-09-16 18:49:03 -04:00
Samuel Attard
4969c4ab19 build: ensure we await correctly in npm publish script 2021-09-16 13:48:53 -07:00
Sudowoodo Release Bot
9e1736f5bc Bump v15.0.0-beta.7 2021-09-16 07:36:07 -07:00
Sudowoodo Release Bot
200e26c602 Revert "Bump v15.0.0-beta.7"
This reverts commit bbc4545742.
2021-09-16 07:32:57 -07:00
Sudowoodo Release Bot
bbc4545742 Bump v15.0.0-beta.7 2021-09-16 06:33:26 -07:00
trop[bot]
9874e940ab build: embed binary checksums in the npm package (#30647)
* build: embed binary checksums in the npm package

* Update docs/tutorial/installation.md

Co-authored-by: Jeremy Rose <jeremya@chromium.org>

* refactor: replace reduce with loop

* refactor: remove all usages of the legacy request module (#30492)

* Replaces request with got
* Replaces nugget with got streams
* Replaces request in docs with got
* Upgrades dugite to drop requests dependency

* build: do not excessively log response bodies

* build: fix publish-to-npm script post requests migration

* chore: revert accidental package bumps

Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>
Co-authored-by: Samuel Attard <sam@electronjs.org>
Co-authored-by: Jeremy Rose <jeremya@chromium.org>
2021-09-16 09:36:54 +09:00
trop[bot]
304453fa5c fix: always include pepper flash font file (#30970)
Co-authored-by: Micha Hanselmann <micha.hanselmann@gmail.com>
2021-09-16 09:29:34 +09:00
Keeley Hammond
16a32a30d9 chore: update E15 node module version (#30773)
Ref: https://github.com/nodejs/node/pull/39950/files
2021-09-15 16:33:08 -07:00
trop[bot]
a227474809 chore: correct hierarchy of BrowserWindow headings (#30974)
* chore: correct hierarchy of BrowserWindow headings

* Update docs/api/browser-window.md

Co-authored-by: Mark Lee <malept@users.noreply.github.com>

* Update docs/api/browser-window.md

Co-authored-by: Mark Lee <malept@users.noreply.github.com>

* Update docs/api/browser-window.md

Co-authored-by: Mark Lee <malept@users.noreply.github.com>

Co-authored-by: Erick Zhao <erick@hotmail.ca>
Co-authored-by: Mark Lee <malept@users.noreply.github.com>
2021-09-15 15:41:29 +09:00
trop[bot]
3a2055ebba docs: update context isolation doc (#30977)
* docs: update context isolation doc

* Apply suggestions from code review

Co-authored-by: Mark Lee <malept@users.noreply.github.com>

Co-authored-by: Erick Zhao <erick@hotmail.ca>
Co-authored-by: Cheng Zhao <github@zcbenz.com>
Co-authored-by: Mark Lee <malept@users.noreply.github.com>
2021-09-15 15:40:35 +09:00
Sudowoodo Release Bot
87042e1493 Bump v15.0.0-beta.6 2021-09-14 06:03:11 -07:00
Sudowoodo Release Bot
86f3aa0376 Revert "Bump v15.0.0-beta.6"
This reverts commit 33eb6263c0.
2021-09-13 20:35:36 -07:00
Sudowoodo Release Bot
33eb6263c0 Bump v15.0.0-beta.6 2021-09-13 15:31:50 -07:00
trop[bot]
05d5ee4486 fix: remove conflicting RunFileChooserEnd for Mac (#30936)
Co-authored-by: VerteDinde <keeleymhammond@gmail.com>
2021-09-13 15:26:00 -07:00
trop[bot]
98845392d8 feat: add support for validating asar archives on macOS (#30900)
* feat: add support for validating asar archives on macOS

* chore: fix lint

* chore: update as per feedback

* feat: switch implementation to asar integrity hash checks

* feat: make ranged requests work with the asar file validator DataSourceFilter

* chore: fix lint

* chore: fix missing log include on non-darwin

* fix: do not pull block size out of missing optional

* fix: match ValidateOrDie symbol on non-darwin

* chore: fix up asar specs by repacking archives

* fix: maintain integrity chain, do not load file integrity if header integrity was not loaded

* debug test

* Update node-spec.ts

* fix: initialize header_validated_

* chore: update PR per feedback

* chore: update per feedback

* build: use final asar module

* Update fuses.json5

* chore: fix compile errors

Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>
Co-authored-by: Samuel Attard <sam@electronjs.org>
Co-authored-by: Samuel Attard <sattard@slack-corp.com>
2021-09-13 09:58:59 -07:00
75 changed files with 1172 additions and 855 deletions

View File

@@ -1209,6 +1209,9 @@ steps-tests: &steps-tests
(cd electron && node script/yarn test --runners=main --trace-uncaught --enable-logging)
(cd electron && node script/yarn test --runners=remote --trace-uncaught --enable-logging)
else
if [ "$TARGET_ARCH" == "ia32" ]; then
npm_config_arch=x64 node electron/node_modules/dugite/script/download-git.js
fi
(cd electron && node script/yarn test --runners=main --trace-uncaught --enable-logging --files $(circleci tests glob spec-main/*-spec.ts | circleci tests split --split-by=timings))
(cd electron && node script/yarn test --runners=remote --trace-uncaught --enable-logging --files $(circleci tests glob spec/*-spec.js | circleci tests split --split-by=timings))
fi

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ compile_commands.json
# npm package
/npm/dist
/npm/path.txt
/npm/checksums.json
.npmrc

View File

@@ -1006,6 +1006,12 @@ if (is_mac) {
outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ]
}
asar_hashed_info_plist("electron_app_plist") {
keys = [ "DEFAULT_APP_ASAR_HEADER_SHA" ]
hash_targets = [ ":default_app_asar_header_hash" ]
plist_file = "shell/browser/resources/mac/Info.plist"
}
mac_app_bundle("electron_app") {
output_name = electron_product_name
sources = filenames.app_sources
@@ -1013,6 +1019,7 @@ if (is_mac) {
include_dirs = [ "." ]
deps = [
":electron_app_framework_bundle_data",
":electron_app_plist",
":electron_app_resources",
":electron_fuses",
"//base",
@@ -1021,7 +1028,7 @@ if (is_mac) {
if (is_mas_build) {
deps += [ ":electron_login_helper_app" ]
}
info_plist = "shell/browser/resources/mac/Info.plist"
info_plist_target = ":electron_app_plist"
extra_substitutions = [
"ELECTRON_BUNDLE_ID=$electron_mac_bundle_id",
"ELECTRON_VERSION=$electron_version",

2
DEPS
View File

@@ -15,7 +15,7 @@ gclient_gn_args = [
vars = {
'chromium_version':
'94.0.4606.31',
'94.0.4606.51',
'node_version':
'v16.5.0',
'nan_version':

View File

@@ -1 +1 @@
15.0.0-beta.5
15.0.0

View File

@@ -2,7 +2,7 @@ is_electron_build = true
root_extra_deps = [ "//electron" ]
# Registry of NMVs --> https://github.com/nodejs/node/blob/master/doc/abi_version_registry.json
node_module_version = 89
node_module_version = 98
v8_promise_internal_field_count = 1
v8_typed_array_max_size_in_heap = 0

View File

@@ -57,4 +57,42 @@ template("asar") {
rebase_path(outputs[0]),
]
}
node_action(target_name + "_header_hash") {
invoker_out = invoker.outputs
deps = [ ":" + invoker.target_name ]
sources = invoker.outputs
script = "//electron/script/gn-asar-hash.js"
outputs = [ "$target_gen_dir/asar_hashes/$target_name.hash" ]
args = [
rebase_path(invoker_out[0]),
rebase_path(outputs[0]),
]
}
}
template("asar_hashed_info_plist") {
node_action(target_name) {
assert(defined(invoker.plist_file),
"Need plist_file to add hashed assets to")
assert(defined(invoker.keys), "Need keys to replace with asset hash")
assert(defined(invoker.hash_targets), "Need hash_targets to read hash from")
deps = invoker.hash_targets
script = "//electron/script/gn-plist-but-with-hashes.js"
inputs = [ invoker.plist_file ]
outputs = [ "$target_gen_dir/hashed_plists/$target_name.plist" ]
hash_files = []
foreach(hash_target, invoker.hash_targets) {
hash_files += get_target_outputs(hash_target)
}
args = [
rebase_path(invoker.plist_file),
rebase_path(outputs[0]),
] + invoker.keys + rebase_path(hash_files)
}
}

View File

@@ -5,5 +5,7 @@
"run_as_node": "1",
"cookie_encryption": "0",
"node_options": "1",
"node_cli_inspect": "1"
"node_cli_inspect": "1",
"embedded_asar_integrity_validation": "0",
"only_load_app_from_asar": "0"
}

View File

@@ -314,17 +314,13 @@ source_set("plugins") {
sources += [
"//chrome/renderer/pepper/chrome_renderer_pepper_host_factory.cc",
"//chrome/renderer/pepper/chrome_renderer_pepper_host_factory.h",
"//chrome/renderer/pepper/pepper_flash_font_file_host.cc",
"//chrome/renderer/pepper/pepper_flash_font_file_host.h",
"//chrome/renderer/pepper/pepper_shared_memory_message_filter.cc",
"//chrome/renderer/pepper/pepper_shared_memory_message_filter.h",
]
if (enable_pdf_viewer) {
sources += [
"//chrome/renderer/pepper/pepper_flash_font_file_host.cc",
"//chrome/renderer/pepper/pepper_flash_font_file_host.h",
]
if (enable_pdf_viewer) {
deps += [ "//components/pdf/renderer" ]
}
deps += [ "//components/pdf/renderer" ]
}
deps += [
"//components/strings",

View File

@@ -22,12 +22,13 @@ win.loadFile('index.html')
To create a window without chrome, or a transparent window in arbitrary shape,
you can use the [Frameless Window](frameless-window.md) API.
## Showing window gracefully
## Showing the window gracefully
When loading a page in the window directly, users may see the page load incrementally, which is not a good experience for a native app. To make the window display
without visual flash, there are two solutions for different situations.
When loading a page in the window directly, users may see the page load incrementally,
which is not a good experience for a native app. To make the window display
without a visual flash, there are two solutions for different situations.
## Using `ready-to-show` event
### Using the `ready-to-show` event
While loading the page, the `ready-to-show` event will be emitted when the renderer
process has rendered the page for the first time if the window has not been shown yet. Showing
@@ -48,7 +49,7 @@ event.
Please note that using this event implies that the renderer will be considered "visible" and
paint even though `show` is false. This event will never fire if you use `paintWhenInitiallyHidden: false`
## Setting `backgroundColor`
### Setting the `backgroundColor` property
For a complex app, the `ready-to-show` event could be emitted too late, making
the app feel slow. In this case, it is recommended to show the window

View File

@@ -86,8 +86,8 @@ available from next tick of the process.
const { session } = require('electron')
session.defaultSession.on('will-download', (event, item, webContents) => {
event.preventDefault()
require('request')(item.getURL(), (data) => {
require('fs').writeFileSync('/somewhere', data)
require('got')(item.getURL()).then((response) => {
require('fs').writeFileSync('/somewhere', response.body)
})
})
```

View File

@@ -4,39 +4,38 @@
Context Isolation is a feature that ensures that both your `preload` scripts and Electron's internal logic run in a separate context to the website you load in a [`webContents`](../api/web-contents.md). This is important for security purposes as it helps prevent the website from accessing Electron internals or the powerful APIs your preload script has access to.
This means that the `window` object that your preload script has access to is actually a **different** object than the website would have access to. For example, if you set `window.hello = 'wave'` in your preload script and context isolation is enabled `window.hello` will be undefined if the website tries to access it.
This means that the `window` object that your preload script has access to is actually a **different** object than the website would have access to. For example, if you set `window.hello = 'wave'` in your preload script and context isolation is enabled, `window.hello` will be undefined if the website tries to access it.
Every single application should have context isolation enabled and from Electron 12 it will be enabled by default.
## How do I enable it?
From Electron 12, it will be enabled by default. For lower versions it is an option in the `webPreferences` option when constructing `new BrowserWindow`'s.
```javascript
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true
}
})
```
Context isolation has been enabled by default since Electron 12, and it is a recommended security setting for _all applications_.
## Migration
> I used to provide APIs from my preload script using `window.X = apiObject` now what?
> Without context isolation, I used to provide APIs from my preload script using `window.X = apiObject`. Now what?
Exposing APIs from your preload script to the loaded website is a common usecase and there is a dedicated module in Electron to help you do this in a painless way.
### Before: context isolation disabled
**Before: With context isolation disabled**
Exposing APIs from your preload script to a loaded website in the renderer process is a common use-case. With context isolation disabled, your preload script would share a common global `window` object with the renderer. You could then attach arbitrary properties to a preload script:
```javascript
```javascript title='preload.js'
// preload with contextIsolation disabled
window.myAPI = {
doAThing: () => {}
}
```
**After: With context isolation enabled**
The `doAThing()` function could then be used directly in the renderer process:
```javascript
```javascript title='renderer.js'
// use the exposed API in the renderer
window.myAPI.doAThing()
```
### After: context isolation enabled
There is a dedicated module in Electron to help you do this in a painless way. The [`contextBridge`](../api/context-bridge.md) module can be used to **safely** expose APIs from your preload script's isolated context to the context the website is running in. The API will also be accessible from the website on `window.myAPI` just like it was before.
```javascript title='preload.js'
// preload with contextIsolation enabled
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
@@ -44,26 +43,63 @@ contextBridge.exposeInMainWorld('myAPI', {
})
```
The [`contextBridge`](../api/context-bridge.md) module can be used to **safely** expose APIs from the isolated context your preload script runs in to the context the website is running in. The API will also be accessible from the website on `window.myAPI` just like it was before.
```javascript title='renderer.js'
// use the exposed API in the renderer
window.myAPI.doAThing()
```
You should read the `contextBridge` documentation linked above to fully understand its limitations. For instance you can't send custom prototypes or symbols over the bridge.
Please read the `contextBridge` documentation linked above to fully understand its limitations. For instance, you can't send custom prototypes or symbols over the bridge.
## Security Considerations
## Security considerations
Just enabling `contextIsolation` and using `contextBridge` does not automatically mean that everything you do is safe. For instance this code is **unsafe**.
Just enabling `contextIsolation` and using `contextBridge` does not automatically mean that everything you do is safe. For instance, this code is **unsafe**.
```javascript
```javascript title='preload.js'
// ❌ Bad code
contextBridge.exposeInMainWorld('myAPI', {
send: ipcRenderer.send
})
```
It directly exposes a powerful API without any kind of argument filtering. This would allow any website to send arbitrary IPC messages which you do not want to be possible. The correct way to expose IPC-based APIs would instead be to provide one method per IPC message.
It directly exposes a powerful API without any kind of argument filtering. This would allow any website to send arbitrary IPC messages, which you do not want to be possible. The correct way to expose IPC-based APIs would instead be to provide one method per IPC message.
```javascript
```javascript title='preload.js'
// ✅ Good code
contextBridge.exposeInMainWorld('myAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
```
## Usage with TypeScript
If you're building your Electron app with TypeScript, you'll want to add types to your APIs exposed over the context bridge. The renderer's `window` object won't have the correct typings unless you extend the types with a [declaration file].
For example, given this `preload.ts` script:
```typescript title='preload.ts'
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
```
You can create a `renderer.d.ts` declaration file and globally augment the `Window` interface:
```typescript title='renderer.d.ts'
export interface IElectronAPI {
loadPreferences: () => Promise<void>,
}
declare global {
interface Window {
electronAPI: IElectronAPI
}
}
```
Doing so will ensure that the TypeScript compiler will know about the `electronAPI` property on your global `window` object when writing scripts in your renderer process:
```typescript title='renderer.ts'
window.electronAPI.loadPreferences()
```
[declaration file]: https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html

View File

@@ -90,6 +90,11 @@ ELECTRON_CUSTOM_DIR="{{ version }}"
The above configuration will download from URLs such as
`https://npm.taobao.org/mirrors/electron/8.0.0/electron-v8.0.0-linux-x64.zip`.
If your mirror serves artifacts with different checksums to the official
Electron release you may have to set `ELECTRON_USE_REMOTE_CHECKSUMS=1` to
force Electron to use the remote `SHASUMS256.txt` file to verify the checksum
instead of the embedded checksums.
#### Cache
Alternatively, you can override the local cache. `@electron/get` will cache

View File

@@ -120,9 +120,9 @@ file in the directory you executed it in. Both files can be analyzed using
the Chrome Developer Tools, using the `Performance` and `Memory` tabs
respectively.
![Performance CPU Profile][performance-cpu-prof]
![Performance CPU Profile](../images/performance-cpu-prof.png)
![Performance Heap Memory Profile][performance-heap-prof]
![Performance Heap Memory Profile](../images/performance-heap-prof.png)
In this example, on the author's machine, we saw that loading `request` took
almost half a second, whereas `node-fetch` took dramatically less memory
@@ -412,8 +412,6 @@ As of writing this article, the popular choices include [Webpack][webpack],
[Parcel][parcel], and [rollup.js][rollup].
[security]: ./security.md
[performance-cpu-prof]: ../images/performance-cpu-prof.png
[performance-heap-prof]: ../images/performance-heap-prof.png
[chrome-devtools-tutorial]: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/
[worker-threads]: https://nodejs.org/api/worker_threads.html
[web-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

View File

@@ -7,16 +7,16 @@ without the need of switching to the window itself.
On Windows, you can use a taskbar button to display a progress bar.
![Windows Progress Bar][windows-progress-bar]
![Windows Progress Bar][https://cloud.githubusercontent.com/assets/639601/5081682/16691fda-6f0e-11e4-9676-49b6418f1264.png]
On macOS, the progress bar will be displayed as a part of the dock icon.
![macOS Progress Bar][macos-progress-bar]
![macOS Progress Bar](../images/macos-progress-bar.png)
On Linux, the Unity graphical interface also has a similar feature that allows
you to specify the progress bar in the launcher.
![Linux Progress Bar][linux-progress-bar]
![Linux Progress Bar](../images/linux-progress-bar.png)
> NOTE: on Windows, each window can have its own progress bar, whereas on macOS
and Linux (Unity) there can be only one progress bar for the application.
@@ -102,8 +102,4 @@ For macOS, the progress bar will also be indicated for your application
when using [Mission Control](https://support.apple.com/en-us/HT204100):
![Mission Control Progress Bar](../images/mission-control-progress-bar.png)
[windows-progress-bar]: https://cloud.githubusercontent.com/assets/639601/5081682/16691fda-6f0e-11e4-9676-49b6418f1264.png
[macos-progress-bar]: ../images/macos-progress-bar.png
[linux-progress-bar]: ../images/linux-progress-bar.png
[setprogressbar]: ../api/browser-window.md#winsetprogressbarprogress-options

View File

@@ -70,10 +70,10 @@ until the maintainers feel the maintenance burden is too high to continue doing
### Currently supported versions
* 15.x.y
* 14.x.y
* 13.x.y
* 12.x.y
* 11.x.y
* 12
### End-of-life

View File

@@ -195,6 +195,7 @@ filenames = {
"shell/browser/ui/tray_icon_cocoa.mm",
"shell/common/api/electron_api_clipboard_mac.mm",
"shell/common/api/electron_api_native_image_mac.mm",
"shell/common/asar/archive_mac.mm",
"shell/common/application_info_mac.mm",
"shell/common/language_util_mac.mm",
"shell/common/mac/main_application_bundle.h",
@@ -408,6 +409,8 @@ filenames = {
"shell/browser/native_window.cc",
"shell/browser/native_window.h",
"shell/browser/native_window_observer.h",
"shell/browser/net/asar/asar_file_validator.cc",
"shell/browser/net/asar/asar_file_validator.h",
"shell/browser/net/asar/asar_url_loader.cc",
"shell/browser/net/asar/asar_url_loader.h",
"shell/browser/net/asar/asar_url_loader_factory.cc",

View File

@@ -1,6 +1,7 @@
import { Buffer } from 'buffer';
import * as path from 'path';
import * as util from 'util';
import type * as Crypto from 'crypto';
const asar = process._linkedBinding('electron_common_asar');
@@ -194,6 +195,20 @@ const overrideAPI = function (module: Record<string, any>, name: string, pathArg
}
};
let crypto: typeof Crypto;
function validateBufferIntegrity (buffer: Buffer, integrity: NodeJS.AsarFileInfo['integrity']) {
if (!integrity) return;
// Delay load crypto to improve app boot performance
// when integrity protection is not enabled
crypto = crypto || require('crypto');
const actual = crypto.createHash(integrity.algorithm).update(buffer).digest('hex');
if (actual !== integrity.hash) {
console.error(`ASAR Integrity Violation: got a hash mismatch (${actual} vs ${integrity.hash})`);
process.exit(1);
}
}
const makePromiseFunction = function (orig: Function, pathArgumentIndex: number) {
return function (this: any, ...args: any[]) {
const pathArgument = args[pathArgumentIndex];
@@ -531,7 +546,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
}
const buffer = Buffer.alloc(info.size);
const fd = archive.getFd();
const fd = archive.getFdAndValidateIntegrityLater();
if (!(fd >= 0)) {
const error = createError(AsarError.NOT_FOUND, { asarPath, filePath });
nextTick(callback, [error]);
@@ -540,6 +555,7 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
logASARAccess(asarPath, filePath, info.offset);
fs.read(fd, buffer, 0, info.size, info.offset, (error: Error) => {
validateBufferIntegrity(buffer, info.integrity);
callback(error, encoding ? buffer.toString(encoding) : buffer);
});
}
@@ -595,11 +611,12 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
const { encoding } = options;
const buffer = Buffer.alloc(info.size);
const fd = archive.getFd();
const fd = archive.getFdAndValidateIntegrityLater();
if (!(fd >= 0)) throw createError(AsarError.NOT_FOUND, { asarPath, filePath });
logASARAccess(asarPath, filePath, info.offset);
fs.readSync(fd, buffer, 0, info.size, info.offset);
validateBufferIntegrity(buffer, info.integrity);
return (encoding) ? buffer.toString(encoding) : buffer;
};
@@ -713,11 +730,12 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
}
const buffer = Buffer.alloc(info.size);
const fd = archive.getFd();
const fd = archive.getFdAndValidateIntegrityLater();
if (!(fd >= 0)) return [];
logASARAccess(asarPath, filePath, info.offset);
fs.readSync(fd, buffer, 0, info.size, info.offset);
validateBufferIntegrity(buffer, info.integrity);
const str = buffer.toString('utf8');
return [str, str.length > 0];
};

View File

@@ -4,6 +4,7 @@ import type { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron/m
import * as url from 'url';
import * as path from 'path';
import { openGuestWindow, makeWebPreferences, parseContentTypeFormat } from '@electron/internal/browser/guest-window-manager';
import { parseFeatures } from '@electron/internal/common/parse-features-string';
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
@@ -665,6 +666,16 @@ WebContents.prototype._init = function () {
postBody
};
windowOpenOverriddenOptions = this._callWindowOpenHandler(event, details);
// if attempting to use this API with the deprecated window.open event,
// windowOpenOverriddenOptions will always return null. This ensures
// short-term backwards compatibility until window.open is removed.
const parsedFeatures = parseFeatures(rawFeatures);
const overriddenFeatures: BrowserWindowConstructorOptions = {
...parsedFeatures.options,
webPreferences: parsedFeatures.webPreferences
};
windowOpenOverriddenOptions = windowOpenOverriddenOptions || overriddenFeatures;
if (!event.defaultPrevented) {
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
// Allow setting of backgroundColor as a webPreference even though

View File

@@ -81,9 +81,10 @@ require('@electron/internal/browser/guest-view-manager');
require('@electron/internal/browser/guest-window-proxy');
// Now we try to load app's package.json.
const v8Util = process._linkedBinding('electron_common_v8_util');
let packagePath = null;
let packageJson = null;
const searchPaths = ['app', 'app.asar', 'default_app.asar'];
const searchPaths: string[] = v8Util.getHiddenValue(global, 'appSearchPaths');
if (process.resourcesPath) {
for (packagePath of searchPaths) {

View File

@@ -103,10 +103,14 @@ const warnAboutInsecureResources = function () {
return;
}
const isLocal = (url: URL): boolean =>
['localhost', '127.0.0.1', '[::1]', ''].includes(url.hostname);
const isInsecure = (url: URL): boolean =>
['http:', 'ftp:'].includes(url.protocol) && !isLocal(url);
const resources = window.performance
.getEntriesByType('resource')
.filter(({ name }) => /^(http|ftp):/gi.test(name || ''))
.filter(({ name }) => new URL(name).hostname !== 'localhost')
.filter(({ name }) => isInsecure(new URL(name)))
.map(({ name }) => `- ${name}`)
.join('\n');

View File

@@ -41,6 +41,7 @@ downloadArtifact({
artifactName: 'electron',
force: process.env.force_no_cache === 'true',
cacheRoot: process.env.electron_config_cache,
checksums: process.env.electron_use_remote_checksums ? undefined : require('./checksums.json'),
platform,
arch
}).then(extractFile).catch(err => {

View File

@@ -8,7 +8,7 @@
"postinstall": "node install.js"
},
"dependencies": {
"@electron/get": "^1.0.1",
"@electron/get": "^1.13.0",
"@types/node": "^14.6.2",
"extract-zip": "^1.0.3"
},

View File

@@ -1,6 +1,6 @@
{
"name": "electron",
"version": "15.0.0-beta.5",
"version": "15.0.0",
"repository": "https://github.com/electron/electron",
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
"devDependencies": {
@@ -30,12 +30,12 @@
"@types/webpack-env": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
"asar": "^3.0.3",
"asar": "^3.1.0",
"aws-sdk": "^2.727.1",
"check-for-leaks": "^1.2.1",
"colors": "^1.4.0",
"dotenv-safe": "^4.0.4",
"dugite": "^1.45.0",
"dugite": "^1.103.0",
"eslint": "^7.4.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
@@ -54,12 +54,10 @@
"markdownlint": "^0.21.1",
"markdownlint-cli": "^0.25.0",
"minimist": "^1.2.5",
"nugget": "^2.0.1",
"null-loader": "^4.0.0",
"pre-flight": "^1.1.0",
"remark-cli": "^4.0.0",
"remark-preset-lint-markdown-style-guide": "^2.1.1",
"request": "^2.88.2",
"semver": "^5.6.0",
"shx": "^0.3.2",
"standard-markdown": "^6.0.0",

View File

@@ -46,10 +46,10 @@ index 135ad9b24745f6d016806f3c95993efe65b370f3..c5e694cd56b9db940e8564c82e2773aa
}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 3ddf1368f8ac78f32f523b2abde0addb79dcd9df..d878826b9782e65f92bf9316400f59cb55d2a4df 100644
index 0fcf061bf93b4b11603ef944cf8dd4d456cf5f45..c2a035289bcd7424f1a2645da86133de5288ebaa 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -5324,7 +5324,6 @@ test("unit_tests") {
@@ -5325,7 +5325,6 @@ test("unit_tests") {
assert(toolkit_views)
sources += [ "../browser/ui/startup/credential_provider_signin_info_fetcher_win_unittest.cc" ]
deps += [
@@ -57,7 +57,7 @@ index 3ddf1368f8ac78f32f523b2abde0addb79dcd9df..d878826b9782e65f92bf9316400f59cb
"//chrome/browser:chrome_process_finder",
"//chrome/browser/safe_browsing/chrome_cleaner",
"//chrome/browser/safe_browsing/chrome_cleaner:public",
@@ -5337,6 +5336,12 @@ test("unit_tests") {
@@ -5338,6 +5337,12 @@ test("unit_tests") {
"//components/chrome_cleaner/public/proto",
"//ui/events/devices:test_support",
]
@@ -70,7 +70,7 @@ index 3ddf1368f8ac78f32f523b2abde0addb79dcd9df..d878826b9782e65f92bf9316400f59cb
}
# TODO(crbug.com/931218): Ninja cannot handle certain characters appearing
@@ -5929,7 +5934,6 @@ test("unit_tests") {
@@ -5930,7 +5935,6 @@ test("unit_tests") {
}
deps += [
@@ -78,7 +78,7 @@ index 3ddf1368f8ac78f32f523b2abde0addb79dcd9df..d878826b9782e65f92bf9316400f59cb
"//chrome/browser:cart_db_content_proto",
"//chrome/browser/media/router:test_support",
"//chrome/browser/promo_browser_command:mojo_bindings",
@@ -5965,6 +5969,9 @@ test("unit_tests") {
@@ -5966,6 +5970,9 @@ test("unit_tests") {
"//ui/color:test_support",
"//ui/native_theme:test_support",
]

View File

@@ -9,10 +9,10 @@ potentially prevent a window from being created.
TODO(loc): this patch is currently broken.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 9bf31d2cb714f5def7db8ef4966d2ebff6223b92..621e5109e75db6e39ad488df0924b7bb98cc2aed 100644
index a0cfdfbe055153861d35e5598c0ecea1aec3a0d1..2cba72ac8fc5b204e4c1e3a1d61fada89adced2a 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -6367,6 +6367,7 @@ void RenderFrameHostImpl::CreateNewWindow(
@@ -6413,6 +6413,7 @@ void RenderFrameHostImpl::CreateNewWindow(
last_committed_origin_, params->window_container_type,
params->target_url, params->referrer.To<Referrer>(),
params->frame_name, params->disposition, *params->features,

View File

@@ -6,10 +6,10 @@ Subject: frame_host_manager.patch
Allows embedder to intercept site instances created by chromium.
diff --git a/content/browser/renderer_host/render_frame_host_manager.cc b/content/browser/renderer_host/render_frame_host_manager.cc
index fd21a0ae3c653e262dd049674a4f943feb23ddf6..f5857235687e0b923e9d73e17c19c36495d10680 100644
index d44fe1f7dbef9d5bc1ffbc508fad5d5642e850e7..f6dfd1d7cd9b4f9b8f59613ee2960dc4cc27e1a3 100644
--- a/content/browser/renderer_host/render_frame_host_manager.cc
+++ b/content/browser/renderer_host/render_frame_host_manager.cc
@@ -3077,6 +3077,9 @@ RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
@@ -3095,6 +3095,9 @@ RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
request->ResetStateForSiteInstanceChange();
}

View File

@@ -14,10 +14,10 @@ Note that we also need to manually update embedder's
`api::WebContents::IsFullscreenForTabOrPending` value.
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 621e5109e75db6e39ad488df0924b7bb98cc2aed..7c7c1ad7d31a547e13411548468f777e76fff0fd 100644
index 2cba72ac8fc5b204e4c1e3a1d61fada89adced2a..1cbf9e4d9919321895d8823f539f5f917a31e8a1 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -5790,6 +5790,15 @@ void RenderFrameHostImpl::EnterFullscreen(
@@ -5836,6 +5836,15 @@ void RenderFrameHostImpl::EnterFullscreen(
notified_instances.insert(parent_site_instance);
}

View File

@@ -6,5 +6,4 @@ workaround_an_undefined_symbol_error.patch
do_not_export_private_v8_symbols_on_windows.patch
fix_build_deprecated_attirbute_for_older_msvc_versions.patch
fix_disable_implies_dcheck_for_node_stream_array_buffers.patch
cppgc-js_fix_snapshot_node_merging.patch
cppgc-js_support_eager_traced_value_in_ephemeron_pairs.patch

View File

@@ -1,50 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Michael Lippautz <mlippautz@chromium.org>
Date: Tue, 24 Aug 2021 12:34:24 +0200
Subject: cppgc-js: Fix snapshot node merging
In Blink, WindowProxy may be referred from two diffrent JS wrapper
objects during page refresh (same site navigation reusing parts of the
DOM). In this intermediate state, the old frame state is not yet
reclaimed while the new state is already being added.
We would like to only merge nodes when there's a 1:1 relation between
C++ and JS objects. Unfortunately, WindowProxy breaks that assumption
in that the C++ object doesn't directly point to the wrapper. In
addition, merging this case is important as otherwise detachedness
would not be propagated to the Window object (JS wrapper) which is the
main user of detachedness.
The CL allows overriding merged nodes, picking a random merged state
during pageload while still resulting in the regular snapshot behavior
outside of reloading the same page.
The proper fix is addressing chromium:1218404 and only create merged
nodes when the back reference points to the same object.
Bug: chromium:1241610
Change-Id: Ie77b51a56ce90ef377124304bb025342a724c600
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3114139
Reviewed-by: Anton Bikineev <bikineev@chromium.org>
Commit-Queue: Michael Lippautz <mlippautz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#76453}
diff --git a/src/heap/cppgc-js/cpp-snapshot.cc b/src/heap/cppgc-js/cpp-snapshot.cc
index 28859d84f66aafa6b90325086d2289f2ef43a704..e72c6ad46998bb40ba91d37a740dc0ecfee2838b 100644
--- a/src/heap/cppgc-js/cpp-snapshot.cc
+++ b/src/heap/cppgc-js/cpp-snapshot.cc
@@ -47,7 +47,13 @@ class EmbedderNode : public v8::EmbedderGraph::Node {
void SetWrapperNode(v8::EmbedderGraph::Node* wrapper_node) {
// An embedder node may only be merged with a single wrapper node, as
// consumers of the graph may merge a node and its wrapper node.
- DCHECK_NULL(wrapper_node_);
+ //
+ // TODO(chromium:1218404): Add a DCHECK() to avoid overriding an already
+ // set `wrapper_node_`. This can currently happen with global proxies that
+ // are rewired (and still kept alive) after reloading a page, see
+ // `CreateMergedNode`. We accept overriding the wrapper node in such cases,
+ // leading to a random merged node and separated nodes for all other
+ // proxies.
wrapper_node_ = wrapper_node;
}
Node* WrapperNode() final { return wrapper_node_; }

View File

@@ -28,7 +28,7 @@ Commit-Queue: Michael Lippautz <mlippautz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#76658}
diff --git a/src/heap/cppgc-js/cpp-snapshot.cc b/src/heap/cppgc-js/cpp-snapshot.cc
index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f0b7c60e5 100644
index dc55753ff625a135b6e494344ee49105eb59121e..9b20b5c0a7831ea026819f90ea80c10eb2324282 100644
--- a/src/heap/cppgc-js/cpp-snapshot.cc
+++ b/src/heap/cppgc-js/cpp-snapshot.cc
@@ -264,6 +264,10 @@ class State final : public StateBase {
@@ -82,7 +82,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
DCHECK(parent.IsVisibleNotDependent());
auto& current = states_.GetExistingState(header);
if (!current.IsVisibleNotDependent()) return;
@@ -449,7 +466,8 @@ class CppGraphBuilderImpl final {
@@ -443,7 +460,8 @@ class CppGraphBuilderImpl final {
}
}
@@ -92,7 +92,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
DCHECK(parent.IsVisibleNotDependent());
v8::Local<v8::Value> v8_value = ref.Get(cpp_heap_.isolate());
if (!v8_value.IsEmpty()) {
@@ -457,12 +475,19 @@ class CppGraphBuilderImpl final {
@@ -451,12 +469,19 @@ class CppGraphBuilderImpl final {
parent.set_node(AddNode(*parent.header()));
}
auto* v8_node = graph_.V8Node(v8_value);
@@ -114,7 +114,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
void* back_reference_object = ExtractEmbedderDataBackref(
reinterpret_cast<v8::internal::Isolate*>(cpp_heap_.isolate()),
@@ -612,8 +637,18 @@ class WeakVisitor : public JSVisitor {
@@ -598,8 +623,18 @@ class WeakVisitor : public JSVisitor {
void VisitEphemeron(const void* key, const void* value,
cppgc::TraceDescriptor value_desc) final {
// For ephemerons, the key retains the value.
@@ -134,7 +134,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
}
protected:
@@ -659,7 +694,7 @@ class GraphBuildingVisitor final : public JSVisitor {
@@ -645,7 +680,7 @@ class GraphBuildingVisitor final : public JSVisitor {
void Visit(const void*, cppgc::TraceDescriptor desc) final {
graph_builder_.AddEdge(
parent_scope_.ParentAsRegularState(),
@@ -143,7 +143,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
}
void VisitWeakContainer(const void* object,
cppgc::TraceDescriptor strong_desc,
@@ -669,7 +704,8 @@ class GraphBuildingVisitor final : public JSVisitor {
@@ -655,7 +690,8 @@ class GraphBuildingVisitor final : public JSVisitor {
// container itself.
graph_builder_.AddEdge(
parent_scope_.ParentAsRegularState(),
@@ -153,7 +153,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
}
void VisitRoot(const void*, cppgc::TraceDescriptor desc,
const cppgc::SourceLocation& loc) final {
@@ -681,12 +717,18 @@ class GraphBuildingVisitor final : public JSVisitor {
@@ -667,12 +703,18 @@ class GraphBuildingVisitor final : public JSVisitor {
const void*, const cppgc::SourceLocation&) final {}
// JS handling.
void Visit(const TracedReferenceBase& ref) final {
@@ -173,7 +173,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
};
// Base class for transforming recursion into iteration. Items are processed
@@ -779,6 +821,19 @@ void CppGraphBuilderImpl::VisitForVisibility(State* parent,
@@ -765,6 +807,19 @@ void CppGraphBuilderImpl::VisitForVisibility(State* parent,
}
}
@@ -193,7 +193,7 @@ index e72c6ad46998bb40ba91d37a740dc0ecfee2838b..f64a7de116ac69221e3610370c7e2a5f
void CppGraphBuilderImpl::VisitEphemeronForVisibility(
const HeapObjectHeader& key, const HeapObjectHeader& value) {
auto& key_state = states_.GetOrCreateState(key);
@@ -834,6 +889,12 @@ void CppGraphBuilderImpl::Run() {
@@ -820,6 +875,12 @@ void CppGraphBuilderImpl::Run() {
state.ForAllEphemeronEdges([this, &state](const HeapObjectHeader& value) {
AddEdge(state, value, "part of key -> value pair in ephemeron table");
});

View File

@@ -1,52 +1,23 @@
const args = require('minimist')(process.argv.slice(2));
const nugget = require('nugget');
const request = require('request');
const fs = require('fs');
const got = require('got');
const stream = require('stream');
const { promisify } = require('util');
async function makeRequest (requestOptions, parseResponse) {
return new Promise((resolve, reject) => {
request(requestOptions, (err, res, body) => {
if (!err && res.statusCode >= 200 && res.statusCode < 300) {
if (parseResponse) {
const build = JSON.parse(body);
resolve(build);
} else {
resolve(body);
}
} else {
if (args.verbose) {
console.error('Error occurred while requesting:', requestOptions.url);
if (parseResponse) {
try {
console.log('Error: ', `(status ${res.statusCode})`, err || JSON.parse(res.body), requestOptions);
} catch (err) {
console.log('Error: ', `(status ${res.statusCode})`, err || res.body, requestOptions);
}
} else {
console.log('Error: ', `(status ${res.statusCode})`, err || res.body, requestOptions);
}
}
reject(err);
}
});
});
}
const pipeline = promisify(stream.pipeline);
async function downloadArtifact (name, buildNum, dest) {
const circleArtifactUrl = `https://circleci.com/api/v1.1/project/github/electron/electron/${args.buildNum}/artifacts?circle-token=${process.env.CIRCLE_TOKEN}`;
const artifacts = await makeRequest({
method: 'GET',
url: circleArtifactUrl,
const responsePromise = got(circleArtifactUrl, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
}, true).catch(err => {
if (args.verbose) {
console.log('Error calling CircleCI:', err);
} else {
console.error('Error calling CircleCI to get artifact details');
}
});
const [response, artifacts] = await Promise.all([responsePromise, responsePromise.json()]);
if (response.statusCode !== 200) {
console.error('Could not fetch circleci artifact list, got status code:', response.statusCode);
}
const artifactToDownload = artifacts.find(artifact => {
return (artifact.path === name);
});
@@ -86,19 +57,10 @@ async function downloadWithRetry (url, directory) {
}
function downloadFile (url, directory) {
return new Promise((resolve, reject) => {
const nuggetOpts = {
dir: directory,
quiet: args.verbose
};
nugget(url, nuggetOpts, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
return pipeline(
got.stream(url),
fs.createWriteStream(directory)
);
}
if (!args.name || !args.buildNum || !args.dest) {

9
script/gn-asar-hash.js Normal file
View File

@@ -0,0 +1,9 @@
const asar = require('asar');
const crypto = require('crypto');
const fs = require('fs');
const archive = process.argv[2];
const hashFile = process.argv[3];
const { headerString } = asar.getRawHeader(archive);
fs.writeFileSync(hashFile, crypto.createHash('SHA256').update(headerString).digest('hex'));

View File

@@ -0,0 +1,16 @@
const fs = require('fs');
const [,, plistPath, outputPath, ...keySet] = process.argv;
const keyPairs = {};
for (let i = 0; i * 2 < keySet.length; i++) {
keyPairs[keySet[i]] = fs.readFileSync(keySet[(keySet.length / 2) + i], 'utf8');
}
let plistContents = fs.readFileSync(plistPath, 'utf8');
for (const key of Object.keys(keyPairs)) {
plistContents = plistContents.replace(`$\{${key}}`, keyPairs[key]);
}
fs.writeFileSync(outputPath, plistContents);

View File

@@ -1,7 +1,7 @@
if (!process.env.CI) require('dotenv-safe').load();
const assert = require('assert');
const request = require('request');
const got = require('got');
const BUILD_APPVEYOR_URL = 'https://ci.appveyor.com/api/builds';
const CIRCLECI_PIPELINE_URL = 'https://circleci.com/api/v2/project/gh/electron/electron/pipeline';
@@ -35,31 +35,24 @@ const vstsArmJobs = [
let jobRequestedCount = 0;
async function makeRequest (requestOptions, parseResponse) {
return new Promise((resolve, reject) => {
request(requestOptions, (err, res, body) => {
if (!err && res.statusCode >= 200 && res.statusCode < 300) {
if (parseResponse) {
const build = JSON.parse(body);
resolve(build);
} else {
resolve(body);
}
} else {
console.error('Error occurred while requesting:', requestOptions.url);
if (parseResponse) {
try {
console.log('Error: ', `(status ${res.statusCode})`, err || JSON.parse(res.body));
} catch (err) {
console.log('Error: ', `(status ${res.statusCode})`, res.body);
}
} else {
console.log('Error: ', `(status ${res.statusCode})`, err || res.body);
}
reject(err);
}
});
async function makeRequest ({ auth, url, headers, body, method }) {
const clonedHeaders = {
...(headers || {})
};
if (auth && auth.bearer) {
clonedHeaders.Authorization = `Bearer ${auth.bearer}`;
}
const response = await got(url, {
headers: clonedHeaders,
body,
method,
auth: auth && (auth.username || auth.password) ? `${auth.username}:${auth.password}` : undefined
});
if (response.statusCode < 200 || response.statusCode >= 300) {
console.error('Error: ', `(status ${response.statusCode})`, response.body);
throw new Error(`Unexpected status code ${response.statusCode} from ${url}`);
}
return JSON.parse(response.body);
}
async function circleCIcall (targetBranch, workflowName, options) {

View File

@@ -0,0 +1,43 @@
const { Octokit } = require('@octokit/rest');
const got = require('got');
const octokit = new Octokit({
userAgent: 'electron-asset-fetcher',
auth: process.env.ELECTRON_GITHUB_TOKEN
});
async function getAssetContents (repo, assetId) {
const requestOptions = octokit.repos.getReleaseAsset.endpoint({
owner: 'electron',
repo,
asset_id: assetId,
headers: {
Accept: 'application/octet-stream'
}
});
const { url, headers } = requestOptions;
headers.authorization = `token ${process.env.ELECTRON_GITHUB_TOKEN}`;
const response = await got(url, {
followRedirect: false,
method: 'HEAD',
headers
});
if (!response.headers.location) {
console.error(response.headers, `${response.body}`.slice(0, 300));
throw new Error(`cannot find asset[${assetId}], asset download did not redirect`);
}
const fileResponse = await got(response.headers.location);
if (fileResponse.statusCode !== 200) {
console.error(fileResponse.headers, `${fileResponse.body}`.slice(0, 300));
throw new Error(`cannot download asset[${assetId}] from ${response.headers.location}, got status: ${fileResponse.status}`);
}
return fileResponse.body;
}
module.exports = {
getAssetContents
};

View File

@@ -2,12 +2,14 @@ const temp = require('temp');
const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');
const { getCurrentBranch, ELECTRON_DIR } = require('../lib/utils');
const request = require('request');
const got = require('got');
const semver = require('semver');
const { getCurrentBranch, ELECTRON_DIR } = require('../lib/utils');
const rootPackageJson = require('../../package.json');
const { Octokit } = require('@octokit/rest');
const { getAssetContents } = require('./get-asset');
const octokit = new Octokit({
userAgent: 'electron-npm-publisher',
auth: process.env.ELECTRON_GITHUB_TOKEN
@@ -86,27 +88,41 @@ new Promise((resolve, reject) => {
}
return release;
})
.then((release) => {
.then(async (release) => {
const tsdAsset = release.assets.find((asset) => asset.name === 'electron.d.ts');
if (!tsdAsset) {
throw new Error(`cannot find electron.d.ts from v${rootPackageJson.version} release assets`);
}
return new Promise((resolve, reject) => {
request.get({
url: tsdAsset.url,
headers: {
accept: 'application/octet-stream',
'user-agent': 'electron-npm-publisher'
}
}, (err, response, body) => {
if (err || response.statusCode !== 200) {
reject(err || new Error('Cannot download electron.d.ts'));
} else {
fs.writeFileSync(path.join(tempDir, 'electron.d.ts'), body);
resolve(release);
}
});
});
const typingsContent = await getAssetContents(
rootPackageJson.version.indexOf('nightly') > 0 ? 'nightlies' : 'electron',
tsdAsset.id
);
fs.writeFileSync(path.join(tempDir, 'electron.d.ts'), typingsContent);
return release;
})
.then(async (release) => {
const checksumsAsset = release.assets.find((asset) => asset.name === 'SHASUMS256.txt');
if (!checksumsAsset) {
throw new Error(`cannot find SHASUMS256.txt from v${rootPackageJson.version} release assets`);
}
const checksumsContent = await getAssetContents(
rootPackageJson.version.indexOf('nightly') > 0 ? 'nightlies' : 'electron',
checksumsAsset.id
);
const checksumsObject = {};
for (const line of checksumsContent.trim().split('\n')) {
const [checksum, file] = line.split(' *');
checksumsObject[file] = checksum;
}
fs.writeFileSync(path.join(tempDir, 'checksums.json'), JSON.stringify(checksumsObject, null, 2));
return release;
})
.then(async (release) => {
const currentBranch = await getCurrentBranch();
@@ -150,10 +166,26 @@ new Promise((resolve, reject) => {
// test that the package can install electron prebuilt from github release
const tarballPath = path.join(tempDir, `${rootPackageJson.name}-${rootPackageJson.version}.tgz`);
return new Promise((resolve, reject) => {
childProcess.execSync(`npm install ${tarballPath} --force --silent`, {
const result = childProcess.spawnSync('npm', ['install', tarballPath, '--force', '--silent'], {
env: Object.assign({}, process.env, { electron_config_cache: tempDir }),
cwd: tempDir
cwd: tempDir,
stdio: 'inherit'
});
if (result.status !== 0) {
return reject(new Error(`npm install failed with status ${result.status}`));
}
try {
const electronPath = require(path.resolve(tempDir, 'node_modules', rootPackageJson.name));
if (typeof electronPath !== 'string') {
return reject(new Error(`path to electron binary (${electronPath}) returned by the ${rootPackageJson.name} module is not a string`));
}
if (!fs.existsSync(electronPath)) {
return reject(new Error(`path to electron binary (${electronPath}) returned by the ${rootPackageJson.name} module does not exist on disk`));
}
} catch (e) {
console.error(e);
return reject(new Error(`loading the generated ${rootPackageJson.name} module failed with an error`));
}
resolve(tarballPath);
});
})
@@ -186,6 +218,6 @@ new Promise((resolve, reject) => {
}
})
.catch((err) => {
console.error(`Error: ${err}`);
console.error('Error:', err);
process.exit(1);
});

View File

@@ -384,7 +384,7 @@ async function verifyDraftGitHubReleaseAssets (release) {
console.log('Fetching authenticated GitHub artifact URLs to verify shasums');
const remoteFilesToHash = await Promise.all(release.assets.map(async asset => {
const requestOptions = await octokit.repos.getReleaseAsset.endpoint({
const requestOptions = octokit.repos.getReleaseAsset.endpoint({
owner: 'electron',
repo: targetRepo,
asset_id: asset.id,

View File

@@ -17,7 +17,7 @@ void BrowserWindow::UpdateDraggableRegions(
if (window_->has_frame())
return;
if (&draggable_regions_ != &regions) {
if (&draggable_regions_ != &regions && web_contents()) {
auto* view =
static_cast<content::WebContentsImpl*>(web_contents())->GetView();
if (view) {

View File

@@ -183,7 +183,9 @@ void FileSelectHelper::OnOpenDialogDone(gin_helper::Dictionary result) {
browser_context->prefs()->SetFilePath(prefs::kSelectFileLastDirectory,
paths[0].DirName());
}
#if !defined(OS_MAC)
RunFileChooserEnd();
#endif
}
}
}

View File

@@ -191,6 +191,7 @@ class NativeWindowMac : public NativeWindow,
protected:
// views::WidgetDelegate:
views::View* GetContentsView() override;
bool CanMaximize() const override;
// ui::NativeThemeObserver:
void OnNativeThemeUpdated(ui::NativeTheme* observed_theme) override;

View File

@@ -1715,31 +1715,35 @@ void NativeWindowMac::SetStyleMask(bool on, NSUInteger flag) {
// we explicitly disable resizing while setting it.
ScopedDisableResize disable_resize;
bool was_maximizable = IsMaximizable();
if (on)
[window_ setStyleMask:[window_ styleMask] | flag];
else
[window_ setStyleMask:[window_ styleMask] & (~flag)];
// Change style mask will make the zoom button revert to default, probably
// a bug of Cocoa or macOS.
SetMaximizable(was_maximizable);
SetMaximizable(maximizable_);
}
void NativeWindowMac::SetCollectionBehavior(bool on, NSUInteger flag) {
bool was_maximizable = IsMaximizable();
if (on)
[window_ setCollectionBehavior:[window_ collectionBehavior] | flag];
else
[window_ setCollectionBehavior:[window_ collectionBehavior] & (~flag)];
// Change collectionBehavior will make the zoom button revert to default,
// probably a bug of Cocoa or macOS.
SetMaximizable(was_maximizable);
SetMaximizable(maximizable_);
}
views::View* NativeWindowMac::GetContentsView() {
return root_view_.get();
}
bool NativeWindowMac::CanMaximize() const {
return maximizable_;
}
void NativeWindowMac::OnNativeThemeUpdated(ui::NativeTheme* observed_theme) {
base::PostTask(
FROM_HERE, {content::BrowserThread::UI},

View File

@@ -0,0 +1,152 @@
// Copyright (c) 2021 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/net/asar/asar_file_validator.h"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "crypto/sha2.h"
namespace asar {
AsarFileValidator::AsarFileValidator(IntegrityPayload integrity,
base::File file)
: file_(std::move(file)), integrity_(std::move(integrity)) {
current_block_ = 0;
max_block_ = integrity_.blocks.size() - 1;
}
AsarFileValidator::~AsarFileValidator() = default;
void AsarFileValidator::OnRead(base::span<char> buffer,
mojo::FileDataSource::ReadResult* result) {
DCHECK(!done_reading_);
uint64_t buffer_size = result->bytes_read;
// Compute how many bytes we should hash, and add them to the current hash.
uint32_t block_size = integrity_.block_size;
uint64_t bytes_added = 0;
while (bytes_added < buffer_size) {
if (current_block_ > max_block_) {
LOG(FATAL)
<< "Unexpected number of blocks while validating ASAR file stream";
return;
}
// Create a hash if we don't have one yet
if (!current_hash_) {
current_hash_byte_count_ = 0;
switch (integrity_.algorithm) {
case HashAlgorithm::SHA256:
current_hash_ =
crypto::SecureHash::Create(crypto::SecureHash::SHA256);
break;
case HashAlgorithm::NONE:
CHECK(false);
break;
}
}
// Compute how many bytes we should hash, and add them to the current hash.
// We need to either add just enough bytes to fill up a block (block_size -
// current_bytes) or use every remaining byte (buffer_size - bytes_added)
int bytes_to_hash = std::min(block_size - current_hash_byte_count_,
buffer_size - bytes_added);
DCHECK_GT(bytes_to_hash, 0);
current_hash_->Update(buffer.data() + bytes_added, bytes_to_hash);
bytes_added += bytes_to_hash;
current_hash_byte_count_ += bytes_to_hash;
total_hash_byte_count_ += bytes_to_hash;
if (current_hash_byte_count_ == block_size && !FinishBlock()) {
LOG(FATAL) << "Failed to validate block while streaming ASAR file: "
<< current_block_;
return;
}
}
}
bool AsarFileValidator::FinishBlock() {
if (current_hash_byte_count_ == 0) {
if (!done_reading_ || current_block_ > max_block_) {
return true;
}
}
if (!current_hash_) {
// This happens when we fail to read the resource. Compute empty content's
// hash in this case.
current_hash_ = crypto::SecureHash::Create(crypto::SecureHash::SHA256);
}
uint8_t actual[crypto::kSHA256Length];
// If the file reader is done we need to make sure we've either read up to the
// end of the file (the check below) or up to the end of a block_size byte
// boundary. If the below check fails we compute the next block boundary, how
// many bytes are needed to get there and then we manually read those bytes
// from our own file handle ensuring the data producer is unaware but we can
// validate the hash still.
if (done_reading_ &&
total_hash_byte_count_ - extra_read_ != read_max_ - read_start_) {
uint64_t bytes_needed = std::min(
integrity_.block_size - current_hash_byte_count_,
read_max_ - read_start_ - total_hash_byte_count_ + extra_read_);
uint64_t offset = read_start_ + total_hash_byte_count_ - extra_read_;
std::vector<uint8_t> abandoned_buffer(bytes_needed);
if (!file_.ReadAndCheck(offset, abandoned_buffer)) {
LOG(FATAL) << "Failed to read required portion of streamed ASAR archive";
return false;
}
current_hash_->Update(&abandoned_buffer.front(), bytes_needed);
}
current_hash_->Finish(actual, sizeof(actual));
current_hash_.reset();
current_hash_byte_count_ = 0;
const std::string expected_hash = integrity_.blocks[current_block_];
const std::string actual_hex_hash =
base::ToLowerASCII(base::HexEncode(actual, sizeof(actual)));
if (expected_hash != actual_hex_hash) {
return false;
}
current_block_++;
return true;
}
void AsarFileValidator::OnDone() {
DCHECK(!done_reading_);
done_reading_ = true;
if (!FinishBlock()) {
LOG(FATAL) << "Failed to validate block while ending ASAR file stream: "
<< current_block_;
}
}
void AsarFileValidator::SetRange(uint64_t read_start,
uint64_t extra_read,
uint64_t read_max) {
read_start_ = read_start;
extra_read_ = extra_read;
read_max_ = read_max;
}
void AsarFileValidator::SetCurrentBlock(int current_block) {
current_block_ = current_block;
}
} // namespace asar

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2021 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_NET_ASAR_ASAR_FILE_VALIDATOR_H_
#define SHELL_BROWSER_NET_ASAR_ASAR_FILE_VALIDATOR_H_
#include <algorithm>
#include <memory>
#include "crypto/secure_hash.h"
#include "mojo/public/cpp/system/file_data_source.h"
#include "mojo/public/cpp/system/filtered_data_source.h"
#include "shell/common/asar/archive.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace asar {
class AsarFileValidator : public mojo::FilteredDataSource::Filter {
public:
AsarFileValidator(IntegrityPayload integrity, base::File file);
~AsarFileValidator() override;
void OnRead(base::span<char> buffer,
mojo::FileDataSource::ReadResult* result) override;
void OnDone() override;
void SetRange(uint64_t read_start, uint64_t extra_read, uint64_t read_max);
void SetCurrentBlock(int current_block);
protected:
bool FinishBlock();
private:
base::File file_;
IntegrityPayload integrity_;
// The offset in the file_ that the underlying file reader is starting at
uint64_t read_start_ = 0;
// The number of bytes this DataSourceFilter will have seen that aren't used
// by the DataProducer. These extra bytes are exclusively for hash validation
// but we need to know how many we've used so we know when we're done.
uint64_t extra_read_ = 0;
// The maximum offset in the file_ that we should read to, used to determine
// which bytes we're missing or if we need to read up to a block boundary in
// OnDone
uint64_t read_max_ = 0;
bool done_reading_ = false;
int current_block_;
int max_block_;
uint64_t current_hash_byte_count_ = 0;
uint64_t total_hash_byte_count_ = 0;
std::unique_ptr<crypto::SecureHash> current_hash_;
DISALLOW_COPY_AND_ASSIGN(AsarFileValidator);
};
} // namespace asar
#endif // SHELL_BROWSER_NET_ASAR_ASAR_FILE_VALIDATOR_H_

View File

@@ -4,6 +4,7 @@
#include "shell/browser/net/asar/asar_url_loader.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
@@ -13,6 +14,7 @@
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "content/public/browser/file_url_loader.h"
#include "electron/fuses.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
@@ -23,6 +25,7 @@
#include "net/http/http_byte_range.h"
#include "net/http/http_util.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "shell/browser/net/asar/asar_file_validator.h"
#include "shell/common/asar/archive.h"
#include "shell/common/asar/asar_util.h"
@@ -127,6 +130,7 @@ class AsarURLLoader : public network::mojom::URLLoader {
OnClientComplete(net::ERR_FILE_NOT_FOUND);
return;
}
bool is_verifying_file = info.integrity.has_value();
// For unpacked path, read like normal file.
base::FilePath real_path;
@@ -149,12 +153,26 @@ class AsarURLLoader : public network::mojom::URLLoader {
base::File file(info.unpacked ? real_path : archive->path(),
base::File::FLAG_OPEN | base::File::FLAG_READ);
auto file_data_source =
std::make_unique<mojo::FileDataSource>(std::move(file));
mojo::DataPipeProducer::DataSource* data_source = file_data_source.get();
std::make_unique<mojo::FileDataSource>(file.Duplicate());
std::unique_ptr<mojo::DataPipeProducer::DataSource> readable_data_source;
mojo::FileDataSource* file_data_source_raw = file_data_source.get();
AsarFileValidator* file_validator_raw = nullptr;
uint32_t block_size = 0;
if (info.integrity.has_value()) {
block_size = info.integrity.value().block_size;
auto asar_validator = std::make_unique<AsarFileValidator>(
std::move(info.integrity.value()), std::move(file));
file_validator_raw = asar_validator.get();
readable_data_source.reset(new mojo::FilteredDataSource(
std::move(file_data_source), std::move(asar_validator)));
} else {
readable_data_source = std::move(file_data_source);
}
std::vector<char> initial_read_buffer(net::kMaxBytesToSniff);
auto read_result =
data_source->Read(info.offset, base::span<char>(initial_read_buffer));
std::vector<char> initial_read_buffer(
std::min(static_cast<uint32_t>(net::kMaxBytesToSniff), info.size));
auto read_result = readable_data_source.get()->Read(
info.offset, base::span<char>(initial_read_buffer));
if (read_result.result != MOJO_RESULT_OK) {
OnClientComplete(ConvertMojoResultToNetError(read_result.result));
return;
@@ -183,6 +201,7 @@ class AsarURLLoader : public network::mojom::URLLoader {
}
uint64_t first_byte_to_send = 0;
uint64_t total_bytes_dropped_from_head = initial_read_buffer.size();
uint64_t total_bytes_to_send = info.size;
if (byte_range.IsValid()) {
@@ -214,6 +233,24 @@ class AsarURLLoader : public network::mojom::URLLoader {
// Discount the bytes we just sent from the total range.
first_byte_to_send = read_result.bytes_read;
total_bytes_to_send -= write_size;
} else if (is_verifying_file &&
first_byte_to_send >= static_cast<uint64_t>(block_size)) {
// If validation is active and the range of bytes the request wants starts
// beyond the first block we need to read the next 4MB-1KB to validate
// that block. Then we can skip ahead to the target block in the SetRange
// call below If we hit this case it is assumed that none of the data read
// will be needed by the producer
uint64_t bytes_to_drop = block_size - net::kMaxBytesToSniff;
total_bytes_dropped_from_head += bytes_to_drop;
std::vector<char> abandoned_buffer(bytes_to_drop);
auto abandon_read_result =
readable_data_source.get()->Read(info.offset + net::kMaxBytesToSniff,
base::span<char>(abandoned_buffer));
if (abandon_read_result.result != MOJO_RESULT_OK) {
OnClientComplete(
ConvertMojoResultToNetError(abandon_read_result.result));
return;
}
}
if (!net::GetMimeTypeFromFile(path, &head->mime_type)) {
@@ -234,23 +271,67 @@ class AsarURLLoader : public network::mojom::URLLoader {
if (total_bytes_to_send == 0) {
// There's definitely no more data, so we're already done.
// We provide the range data to the file validator so that
// it can validate the tiny amount of data we did send
if (file_validator_raw)
file_validator_raw->SetRange(info.offset + first_byte_to_send,
total_bytes_dropped_from_head,
info.offset + info.size);
OnFileWritten(MOJO_RESULT_OK);
return;
}
if (is_verifying_file) {
int start_block = first_byte_to_send / block_size;
// If we're starting from the first block, we might not be starting from
// where we sniffed. We might be a few KB into a file so we need to read
// the data in the middle so it gets hashed.
//
// If we're starting from a later block we might be starting half-way
// through the block regardless of what was sniffed. We need to read the
// data from the start of our initial block up to the start of our actual
// read point so it gets hashed.
uint64_t bytes_to_drop =
start_block == 0 ? first_byte_to_send - net::kMaxBytesToSniff
: first_byte_to_send - (start_block * block_size);
if (file_validator_raw)
file_validator_raw->SetCurrentBlock(start_block);
if (bytes_to_drop > 0) {
uint64_t dropped_bytes_offset =
info.offset + (start_block * block_size);
if (start_block == 0)
dropped_bytes_offset += net::kMaxBytesToSniff;
total_bytes_dropped_from_head += bytes_to_drop;
std::vector<char> abandoned_buffer(bytes_to_drop);
auto abandon_read_result = readable_data_source.get()->Read(
dropped_bytes_offset, base::span<char>(abandoned_buffer));
if (abandon_read_result.result != MOJO_RESULT_OK) {
OnClientComplete(
ConvertMojoResultToNetError(abandon_read_result.result));
return;
}
}
}
// In case of a range request, seek to the appropriate position before
// sending the remaining bytes asynchronously. Under normal conditions
// (i.e., no range request) this Seek is effectively a no-op.
//
// Note that in Electron we also need to add file offset.
file_data_source->SetRange(
file_data_source_raw->SetRange(
first_byte_to_send + info.offset,
first_byte_to_send + info.offset + total_bytes_to_send);
if (file_validator_raw)
file_validator_raw->SetRange(info.offset + first_byte_to_send,
total_bytes_dropped_from_head,
info.offset + info.size);
data_producer_ =
std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle));
data_producer_->Write(
std::move(file_data_source),
std::move(readable_data_source),
base::BindOnce(&AsarURLLoader::OnFileWritten, base::Unretained(this)));
}

View File

@@ -49,5 +49,15 @@
<string>This app needs access to Bluetooth</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app needs access to Bluetooth</string>
<key>ElectronAsarIntegrity</key>
<dict>
<key>Resources/default_app.asar</key>
<dict>
<key>algorithm</key>
<string>SHA256</string>
<key>hash</key>
<string>${DEFAULT_APP_ASAR_HEADER_SHA}</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -50,8 +50,8 @@ END
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 15,0,0,5
PRODUCTVERSION 15,0,0,5
FILEVERSION 15,0,0,0
PRODUCTVERSION 15,0,0,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L

View File

@@ -235,9 +235,10 @@ void WinFrameView::LayoutCaptionButtons() {
// portion to return the correct hit test and be manually resized properly.
// Alternatives can be explored, but the differences in view structures
// between Electron and Chromium may result in this as the best option.
int variable_width =
IsMaximized() ? preferred_size.width() : preferred_size.width() - 1;
caption_button_container_->SetBounds(width() - preferred_size.width(),
WindowTopY(), preferred_size.width() - 1,
height);
WindowTopY(), variable_width, height);
}
void WinFrameView::LayoutWindowControlsOverlay() {

View File

@@ -32,13 +32,12 @@ class Archive : public gin::Wrappable<Archive> {
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override {
return gin::ObjectTemplateBuilder(isolate)
.SetProperty("path", &Archive::GetPath)
.SetMethod("getFileInfo", &Archive::GetFileInfo)
.SetMethod("stat", &Archive::Stat)
.SetMethod("readdir", &Archive::Readdir)
.SetMethod("realpath", &Archive::Realpath)
.SetMethod("copyFileOut", &Archive::CopyFileOut)
.SetMethod("getFd", &Archive::GetFD);
.SetMethod("getFdAndValidateIntegrityLater", &Archive::GetFD);
}
const char* GetTypeName() override { return "Archive"; }
@@ -47,9 +46,6 @@ class Archive : public gin::Wrappable<Archive> {
Archive(v8::Isolate* isolate, std::unique_ptr<asar::Archive> archive)
: archive_(std::move(archive)) {}
// Returns the path of the file.
base::FilePath GetPath() { return archive_->path(); }
// Reads the offset and size of file.
v8::Local<v8::Value> GetFileInfo(v8::Isolate* isolate,
const base::FilePath& path) {
@@ -60,6 +56,20 @@ class Archive : public gin::Wrappable<Archive> {
dict.Set("size", info.size);
dict.Set("unpacked", info.unpacked);
dict.Set("offset", info.offset);
if (info.integrity.has_value()) {
gin_helper::Dictionary integrity(isolate, v8::Object::New(isolate));
asar::HashAlgorithm algorithm = info.integrity.value().algorithm;
switch (algorithm) {
case asar::HashAlgorithm::SHA256:
integrity.Set("algorithm", "SHA256");
break;
case asar::HashAlgorithm::NONE:
CHECK(false);
break;
}
integrity.Set("hash", info.integrity.value().hash);
dict.Set("integrity", integrity);
}
return dict.GetHandle();
}
@@ -108,7 +118,7 @@ class Archive : public gin::Wrappable<Archive> {
int GetFD() const {
if (!archive_)
return -1;
return archive_->GetFD();
return archive_->GetUnsafeFD();
}
private:

View File

@@ -18,6 +18,8 @@
#include "base/task/post_task.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "electron/fuses.h"
#include "shell/common/asar/asar_util.h"
#include "shell/common/asar/scoped_temporary_file.h"
#if defined(OS_WIN)
@@ -95,6 +97,7 @@ bool GetNodeFromPath(std::string path,
bool FillFileInfoWithNode(Archive::FileInfo* info,
uint32_t header_size,
bool load_integrity,
const base::DictionaryValue* node) {
int size;
if (!node->GetInteger("size", &size))
@@ -113,11 +116,56 @@ bool FillFileInfoWithNode(Archive::FileInfo* info,
node->GetBoolean("executable", &info->executable);
#if defined(OS_MAC)
if (load_integrity &&
electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled()) {
const base::DictionaryValue* integrity;
if (node->GetDictionary("integrity", &integrity)) {
IntegrityPayload integrity_payload;
std::string algorithm;
const base::ListValue* blocks;
int block_size;
if (integrity->GetString("algorithm", &algorithm) &&
integrity->GetString("hash", &integrity_payload.hash) &&
integrity->GetInteger("blockSize", &block_size) &&
integrity->GetList("blocks", &blocks) && block_size > 0) {
integrity_payload.block_size = static_cast<uint32_t>(block_size);
for (size_t i = 0; i < blocks->GetSize(); i++) {
std::string block;
if (!blocks->GetString(i, &block)) {
LOG(FATAL)
<< "Invalid block integrity value for file in ASAR archive";
}
integrity_payload.blocks.push_back(block);
}
if (algorithm == "SHA256") {
integrity_payload.algorithm = HashAlgorithm::SHA256;
info->integrity = std::move(integrity_payload);
}
}
}
if (!info->integrity.has_value()) {
LOG(FATAL) << "Failed to read integrity for file in ASAR archive";
return false;
}
}
#endif
return true;
}
} // namespace
IntegrityPayload::IntegrityPayload()
: algorithm(HashAlgorithm::NONE), block_size(0) {}
IntegrityPayload::~IntegrityPayload() = default;
IntegrityPayload::IntegrityPayload(const IntegrityPayload& other) = default;
Archive::FileInfo::FileInfo()
: unpacked(false), executable(false), size(0), offset(0) {}
Archive::FileInfo::~FileInfo() = default;
Archive::Archive(const base::FilePath& path)
: initialized_(false), path_(path), file_(base::File::FILE_OK) {
base::ThreadRestrictions::ScopedAllowIO allow_io;
@@ -191,6 +239,32 @@ bool Archive::Init() {
return false;
}
#if defined(OS_MAC)
// Validate header signature if required and possible
if (electron::fuses::IsEmbeddedAsarIntegrityValidationEnabled() &&
RelativePath().has_value()) {
absl::optional<IntegrityPayload> integrity = HeaderIntegrity();
if (!integrity.has_value()) {
LOG(FATAL) << "Failed to get integrity for validatable asar archive: "
<< RelativePath().value();
return false;
}
// Currently we only support the sha256 algorithm, we can add support for
// more below ensure we read them in preference order from most secure to
// least
if (integrity.value().algorithm != HashAlgorithm::NONE) {
ValidateIntegrityOrDie(header.c_str(), header.length(),
integrity.value());
} else {
LOG(FATAL) << "No eligible hash for validatable asar archive: "
<< RelativePath().value();
}
header_validated_ = true;
}
#endif
absl::optional<base::Value> value = base::JSONReader::Read(header);
if (!value || !value->is_dict()) {
LOG(ERROR) << "Failed to parse header";
@@ -203,6 +277,16 @@ bool Archive::Init() {
return true;
}
#if !defined(OS_MAC)
absl::optional<IntegrityPayload> Archive::HeaderIntegrity() const {
return absl::nullopt;
}
absl::optional<base::FilePath> Archive::RelativePath() const {
return absl::nullopt;
}
#endif
bool Archive::GetFileInfo(const base::FilePath& path, FileInfo* info) const {
if (!header_)
return false;
@@ -215,7 +299,7 @@ bool Archive::GetFileInfo(const base::FilePath& path, FileInfo* info) const {
if (node->GetString("link", &link))
return GetFileInfo(base::FilePath::FromUTF8Unsafe(link), info);
return FillFileInfoWithNode(info, header_size_, node);
return FillFileInfoWithNode(info, header_size_, header_validated_, node);
}
bool Archive::Stat(const base::FilePath& path, Stats* stats) const {
@@ -238,7 +322,7 @@ bool Archive::Stat(const base::FilePath& path, Stats* stats) const {
return true;
}
return FillFileInfoWithNode(stats, header_size_, node);
return FillFileInfoWithNode(stats, header_size_, header_validated_, node);
}
bool Archive::Readdir(const base::FilePath& path,
@@ -304,7 +388,8 @@ bool Archive::CopyFileOut(const base::FilePath& path, base::FilePath* out) {
auto temp_file = std::make_unique<ScopedTemporaryFile>();
base::FilePath::StringType ext = path.Extension();
if (!temp_file->InitFromFile(&file_, ext, info.offset, info.size))
if (!temp_file->InitFromFile(&file_, ext, info.offset, info.size,
info.integrity))
return false;
#if defined(OS_POSIX)
@@ -319,7 +404,7 @@ bool Archive::CopyFileOut(const base::FilePath& path, base::FilePath* out) {
return true;
}
int Archive::GetFD() const {
int Archive::GetUnsafeFD() const {
return fd_;
}

View File

@@ -6,12 +6,14 @@
#define SHELL_COMMON_ASAR_ARCHIVE_H_
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/synchronization/lock.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace base {
class DictionaryValue;
@@ -21,16 +23,33 @@ namespace asar {
class ScopedTemporaryFile;
enum HashAlgorithm {
SHA256,
NONE,
};
struct IntegrityPayload {
IntegrityPayload();
~IntegrityPayload();
IntegrityPayload(const IntegrityPayload& other);
HashAlgorithm algorithm;
std::string hash;
uint32_t block_size;
std::vector<std::string> blocks;
};
// This class represents an asar package, and provides methods to read
// information from it. It is thread-safe after |Init| has been called.
class Archive {
public:
struct FileInfo {
FileInfo() : unpacked(false), executable(false), size(0), offset(0) {}
FileInfo();
~FileInfo();
bool unpacked;
bool executable;
uint32_t size;
uint64_t offset;
absl::optional<IntegrityPayload> integrity;
};
struct Stats : public FileInfo {
@@ -46,6 +65,9 @@ class Archive {
// Read and parse the header.
bool Init();
absl::optional<IntegrityPayload> HeaderIntegrity() const;
absl::optional<base::FilePath> RelativePath() const;
// Get the info of a file.
bool GetFileInfo(const base::FilePath& path, FileInfo* info) const;
@@ -64,12 +86,16 @@ class Archive {
bool CopyFileOut(const base::FilePath& path, base::FilePath* out);
// Returns the file's fd.
int GetFD() const;
// Using this fd will not validate the integrity of any files
// you read out of the ASAR manually. Callers are responsible
// for integrity validation after this fd is handed over.
int GetUnsafeFD() const;
base::FilePath path() const { return path_; }
private:
bool initialized_;
bool header_validated_ = false;
const base::FilePath path_;
base::File file_;
int fd_ = -1;

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2021 Slack Technologies, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/asar/archive.h"
#include <CommonCrypto/CommonDigest.h>
#include <CoreFoundation/CoreFoundation.h>
#include <Foundation/Foundation.h>
#include <iomanip>
#include <string>
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/strings/sys_string_conversions.h"
#include "shell/common/asar/asar_util.h"
namespace asar {
absl::optional<base::FilePath> Archive::RelativePath() const {
base::FilePath bundle_path = base::mac::MainBundlePath().Append("Contents");
base::FilePath relative_path;
if (!bundle_path.AppendRelativePath(path_, &relative_path))
return absl::nullopt;
return relative_path;
}
absl::optional<IntegrityPayload> Archive::HeaderIntegrity() const {
absl::optional<base::FilePath> relative_path = RelativePath();
// Callers should have already asserted this
CHECK(relative_path.has_value());
NSDictionary* integrity = [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"ElectronAsarIntegrity"];
// Integrity not provided
if (!integrity)
return absl::nullopt;
NSString* ns_relative_path =
base::mac::FilePathToNSString(relative_path.value());
NSDictionary* integrity_payload = [integrity objectForKey:ns_relative_path];
if (!integrity_payload)
return absl::nullopt;
NSString* algorithm = [integrity_payload objectForKey:@"algorithm"];
NSString* hash = [integrity_payload objectForKey:@"hash"];
if (algorithm && hash && [algorithm isEqualToString:@"SHA256"]) {
IntegrityPayload header_integrity;
header_integrity.algorithm = HashAlgorithm::SHA256;
header_integrity.hash = base::SysNSStringToUTF8(hash);
return header_integrity;
}
return absl::nullopt;
}
} // namespace asar

View File

@@ -11,11 +11,16 @@
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread_local.h"
#include "base/threading/thread_restrictions.h"
#include "crypto/secure_hash.h"
#include "crypto/sha2.h"
#include "shell/common/asar/archive.h"
namespace asar {
@@ -130,9 +135,38 @@ bool ReadFileToString(const base::FilePath& path, std::string* contents) {
return false;
contents->resize(info.size);
return static_cast<int>(info.size) ==
src.Read(info.offset, const_cast<char*>(contents->data()),
contents->size());
if (static_cast<int>(info.size) !=
src.Read(info.offset, const_cast<char*>(contents->data()),
contents->size())) {
return false;
}
if (info.integrity.has_value()) {
ValidateIntegrityOrDie(contents->data(), contents->size(),
info.integrity.value());
}
return true;
}
void ValidateIntegrityOrDie(const char* data,
size_t size,
const IntegrityPayload& integrity) {
if (integrity.algorithm == HashAlgorithm::SHA256) {
uint8_t hash[crypto::kSHA256Length];
auto hasher = crypto::SecureHash::Create(crypto::SecureHash::SHA256);
hasher->Update(data, size);
hasher->Finish(hash, sizeof(hash));
const std::string hex_hash =
base::ToLowerASCII(base::HexEncode(hash, sizeof(hash)));
if (integrity.hash != hex_hash) {
LOG(FATAL) << "Integrity check failed for asar archive ("
<< integrity.hash << " vs " << hex_hash << ")";
}
} else {
LOG(FATAL) << "Unsupported hashing algorithm in ValidateIntegrityOrDie";
}
}
} // namespace asar

View File

@@ -15,6 +15,7 @@ class FilePath;
namespace asar {
class Archive;
struct IntegrityPayload;
// Gets or creates and caches a new Archive from the path.
std::shared_ptr<Archive> GetOrCreateAsarArchive(const base::FilePath& path);
@@ -31,6 +32,10 @@ bool GetAsarArchivePath(const base::FilePath& full_path,
// Same with base::ReadFileToString but supports asar Archive.
bool ReadFileToString(const base::FilePath& path, std::string* contents);
void ValidateIntegrityOrDie(const char* data,
size_t size,
const IntegrityPayload& integrity);
} // namespace asar
#endif // SHELL_COMMON_ASAR_ASAR_UTIL_H_

View File

@@ -7,7 +7,9 @@
#include <vector>
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/threading/thread_restrictions.h"
#include "shell/common/asar/asar_util.h"
namespace asar {
@@ -48,21 +50,28 @@ bool ScopedTemporaryFile::Init(const base::FilePath::StringType& ext) {
return true;
}
bool ScopedTemporaryFile::InitFromFile(base::File* src,
const base::FilePath::StringType& ext,
uint64_t offset,
uint64_t size) {
bool ScopedTemporaryFile::InitFromFile(
base::File* src,
const base::FilePath::StringType& ext,
uint64_t offset,
uint64_t size,
const absl::optional<IntegrityPayload>& integrity) {
if (!src->IsValid())
return false;
if (!Init(ext))
return false;
base::ThreadRestrictions::ScopedAllowIO allow_io;
std::vector<char> buf(size);
int len = src->Read(offset, buf.data(), buf.size());
if (len != static_cast<int>(size))
return false;
if (integrity.has_value()) {
ValidateIntegrityOrDie(buf.data(), buf.size(), integrity.value());
}
base::File dest(path_, base::File::FLAG_OPEN | base::File::FLAG_WRITE);
if (!dest.IsValid())
return false;

View File

@@ -6,6 +6,8 @@
#define SHELL_COMMON_ASAR_SCOPED_TEMPORARY_FILE_H_
#include "base/files/file_path.h"
#include "shell/common/asar/archive.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace base {
class File;
@@ -31,7 +33,8 @@ class ScopedTemporaryFile {
bool InitFromFile(base::File* src,
const base::FilePath::StringType& ext,
uint64_t offset,
uint64_t size);
uint64_t size,
const absl::optional<IntegrityPayload>& integrity);
base::FilePath path() const { return path_; }

View File

@@ -433,13 +433,29 @@ node::Environment* NodeBindings::CreateEnvironment(
break;
}
gin_helper::Dictionary global(context->GetIsolate(), context->Global());
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary global(isolate, context->Global());
// Do not set DOM globals for renderer process.
// We must set this before the node bootstrapper which is run inside
// CreateEnvironment
if (browser_env_ != BrowserEnvironment::kBrowser)
global.Set("_noBrowserGlobals", true);
if (browser_env_ == BrowserEnvironment::kBrowser) {
const std::vector<std::string> search_paths = {"app.asar", "app",
"default_app.asar"};
const std::vector<std::string> app_asar_search_paths = {"app.asar"};
context->Global()->SetPrivate(
context,
v8::Private::ForApi(
isolate,
gin::ConvertToV8(isolate, "appSearchPaths").As<v8::String>()),
gin::ConvertToV8(isolate,
electron::fuses::IsOnlyLoadAppFromAsarEnabled()
? app_asar_search_paths
: search_paths));
}
std::vector<std::string> exec_args;
base::FilePath resources_path = GetResourcesPath();
std::string init_script = "electron/js2c/" + process_type + "_init";

View File

@@ -3669,6 +3669,18 @@ describe('BrowserWindow module', () => {
}
});
// On Linux there is no "resizable" property of a window.
ifit(process.platform !== 'linux')('does affect maximizability when disabled and enabled', () => {
const w = new BrowserWindow({ show: false });
expect(w.resizable).to.be.true('resizable');
expect(w.maximizable).to.be.true('maximizable');
w.resizable = false;
expect(w.maximizable).to.be.false('not maximizable');
w.resizable = true;
expect(w.maximizable).to.be.true('maximizable');
});
ifit(process.platform === 'win32')('works for a window smaller than 64x64', () => {
const w = new BrowserWindow({
show: false,

View File

@@ -30,6 +30,7 @@ app.on('window-all-closed', () => null);
// Use fake device for Media Stream to replace actual camera and microphone.
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
app.commandLine.appendSwitch('host-rules', 'MAP localhost2 127.0.0.1');
global.standardScheme = 'app';
global.zoomScheme = 'zoom';

View File

@@ -350,7 +350,7 @@ describe('node feature', () => {
});
it('Can find a module using a package.json main field', () => {
const result = childProcess.spawnSync(process.execPath, [path.resolve(fixtures, 'api', 'electron-main-module', 'app.asar')]);
const result = childProcess.spawnSync(process.execPath, [path.resolve(fixtures, 'api', 'electron-main-module', 'app.asar')], { stdio: 'inherit' });
expect(result.status).to.equal(0);
});

View File

@@ -59,7 +59,7 @@ describe('security warnings', () => {
});
});
}).listen(0, '127.0.0.1', () => {
serverUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
serverUrl = `http://localhost2:${(server.address() as AddressInfo).port}`;
done();
});
});

View File

@@ -1564,7 +1564,7 @@ describe('asar package', function () {
forked.on('message', function (stats) {
try {
expect(stats.isFile).to.be.true();
expect(stats.size).to.equal(778);
expect(stats.size).to.equal(3458);
done();
} catch (e) {
done(e);
@@ -1588,7 +1588,7 @@ describe('asar package', function () {
try {
const stats = JSON.parse(output);
expect(stats.isFile).to.be.true();
expect(stats.size).to.equal(778);
expect(stats.size).to.equal(3458);
done();
} catch (e) {
done(e);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

22
spec/fixtures/test.asar/repack.js vendored Normal file
View File

@@ -0,0 +1,22 @@
// Use this script to regenerate these fixture files
// using a new version of the asar package
const asar = require('asar');
const fs = require('fs');
const os = require('os');
const path = require('path');
const archives = [];
for (const child of fs.readdirSync(__dirname)) {
if (child.endsWith('.asar')) {
archives.push(path.resolve(__dirname, child));
}
}
for (const archive of archives) {
const tmp = fs.mkdtempSync(path.resolve(os.tmpdir(), 'asar-spec-'));
asar.extractAll(archive, tmp);
asar.createPackageWithOptions(tmp, archive, {
unpack: fs.existsSync(archive + '.unpacked') ? '*' : undefined
});
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1103,7 +1103,7 @@ shell.writeShortcutLink('/home/user/Desktop/shortcut.lnk', 'update', shell.readS
session.defaultSession.on('will-download', (event, item, webContents) => {
event.preventDefault()
require('request')(item.getURL(), (data: any) => {
require('got')(item.getURL()).then((data: any) => {
require('fs').writeFileSync('/somewhere', data)
})
})

View File

@@ -64,6 +64,10 @@ declare namespace NodeJS {
size: number;
unpacked: boolean;
offset: number;
integrity?: {
algorithm: 'SHA256';
hash: string;
}
};
type AsarFileStat = {
@@ -75,13 +79,12 @@ declare namespace NodeJS {
}
interface AsarArchive {
readonly path: string;
getFileInfo(path: string): AsarFileInfo | false;
stat(path: string): AsarFileStat | false;
readdir(path: string): string[] | false;
realpath(path: string): string | false;
copyFileOut(path: string): string | false;
getFd(): number | -1;
getFdAndValidateIntegrityLater(): number | -1;
}
interface AsarBinding {

View File

@@ -290,7 +290,7 @@ declare namespace ElectronInternal {
declare namespace Chrome {
namespace Tabs {
// https://developer.chrome.com/extensions/tabs#method-executeScript
// https://developer.chrome.com/docs/extensions/tabs#method-executeScript
interface ExecuteScriptDetails {
code?: string;
file?: string;
@@ -303,7 +303,7 @@ declare namespace Chrome {
type ExecuteScriptCallback = (result: Array<any>) => void;
// https://developer.chrome.com/extensions/tabs#method-sendMessage
// https://developer.chrome.com/docs/extensions/tabs#method-sendMessage
interface SendMessageDetails {
frameId?: number;
}

725
yarn.lock

File diff suppressed because it is too large Load Diff