feat: preloads and nodeIntegration in iframes (#16425)

* feat: add support for node / preloads in subframes

This feature has delibrately been built / implemented in such a way
that it has minimum impact on existing apps / code-paths.
Without enabling the new "nodeSupportInSubFrames" option basically none of this
new code will be hit.

The things that I believe need extra scrutiny are:

* Introduction of `event.reply` for IPC events and usage of `event.reply` instead of `event.sender.send()`
* Usage of `node::FreeEnvironment(env)` when the new option is enabled in order to avoid memory leaks.  I have tested this quite a bit and haven't managed to cause a crash but it is still feature flagged behind the "nodeSupportInSubFrames" flag to avoid potential impact.

Closes #10569
Closes #10401
Closes #11868
Closes #12505
Closes #14035

* feat: add support preloads in subframes for sandboxed renderers

* spec: add tests for new nodeSupportInSubFrames option

* spec: fix specs for .reply and ._replyInternal for internal messages

* chore: revert change to use flag instead of environment set size

* chore: clean up subframe impl

* chore: apply suggestions from code review

Co-Authored-By: MarshallOfSound <samuel.r.attard@gmail.com>

* chore: clean up reply usage

* chore: fix TS docs generation

* chore: cleanup after rebase

* chore: rename wrap to add in event fns
This commit is contained in:
Samuel Attard 2019-01-22 11:24:46 -08:00 committed by GitHub
parent 92b9525cfd
commit 58a6fe13d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 332 additions and 49 deletions

View file

@ -143,6 +143,18 @@ WebContents.prototype._sendInternalToAll = function (channel, ...args) {
return this._send(internal, sendToAll, channel, args)
}
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
} else if (typeof frameId !== 'number') {
throw new Error('Missing required frameId argument')
}
const internal = false
const sendToAll = false
return this._sendToFrame(internal, sendToAll, frameId, channel, args)
}
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
@ -330,6 +342,22 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
}))
}
const addReplyToEvent = (event) => {
event.reply = (...args) => {
event.sender.sendToFrame(event.frameId, ...args)
}
}
const addReplyInternalToEvent = (event) => {
Object.defineProperty(event, '_replyInternal', {
configurable: false,
enumerable: false,
value: (...args) => {
event.sender._sendToFrameInternal(event.frameId, ...args)
}
})
}
// Add JavaScript wrappers for WebContents class.
WebContents.prototype._init = function () {
// The navigation controller.
@ -343,6 +371,7 @@ WebContents.prototype._init = function () {
// Dispatch IPC messages to the ipc module.
this.on('-ipc-message', function (event, [channel, ...args]) {
addReplyToEvent(event)
this.emit('ipc-message', event, channel, ...args)
ipcMain.emit(channel, event, ...args)
})
@ -354,11 +383,13 @@ WebContents.prototype._init = function () {
},
get: function () {}
})
addReplyToEvent(event)
this.emit('ipc-message-sync', event, channel, ...args)
ipcMain.emit(channel, event, ...args)
})
this.on('ipc-internal-message', function (event, [channel, ...args]) {
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args)
})
@ -369,6 +400,7 @@ WebContents.prototype._init = function () {
},
get: function () {}
})
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args)
})

View file

@ -180,7 +180,7 @@ ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message,
page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID)
ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
event.sender._sendInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result)
event._replyInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result)
})
resultID++
})
@ -196,7 +196,7 @@ ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBa
contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID)
ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
event.sender._sendInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
event._replyInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
})
resultID++
})

View file

@ -18,7 +18,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
event.sender.emit('desktop-capturer-get-sources', customEvent)
if (customEvent.defaultPrevented) {
event.sender._sendInternal(capturerResult(id), [])
event._replyInternal(capturerResult(id), [])
return
}
@ -30,7 +30,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
thumbnailSize,
fetchWindowIcons
},
webContents: event.sender
event
}
requestsQueue.push(request)
if (requestsQueue.length === 1) {
@ -40,14 +40,13 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
// If the WebContents is destroyed before receiving result, just remove the
// reference from requestsQueue to make the module not send the result to it.
event.sender.once('destroyed', () => {
request.webContents = null
request.event = null
})
})
desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
// Receiving sources result from main process, now send them back to renderer.
const handledRequest = requestsQueue.shift()
const handledWebContents = handledRequest.webContents
const unhandledRequestsQueue = []
const result = sources.map(source => {
@ -60,16 +59,16 @@ desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
}
})
if (handledWebContents) {
handledWebContents._sendInternal(capturerResult(handledRequest.id), result)
if (handledRequest.event) {
handledRequest.event._replyInternal(capturerResult(handledRequest.id), result)
}
// Check the queue to see whether there is another identical request & handle
requestsQueue.forEach(request => {
const webContents = request.webContents
const event = request.event
if (deepEqual(handledRequest.options, request.options)) {
if (webContents) {
webContents._sendInternal(capturerResult(request.id), result)
if (event) {
event._replyInternal(capturerResult(request.id), result)
}
} else {
unhandledRequestsQueue.push(request)

View file

@ -246,7 +246,8 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn
['nativeWindowOpen', true],
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true]
['sandbox', true],
['nodeIntegrationInSubFrames', false]
])
// Inherit certain option values from embedder
@ -350,7 +351,7 @@ const handleMessage = function (channel, handler) {
}
handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', function (event, params, requestId) {
event.sender._sendInternal(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params))
event._replyInternal(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params))
})
handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST_SYNC', function (event, params) {
@ -400,7 +401,7 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', function (event, request
}, error => {
return [errorUtils.serialize(error)]
}).then(responseArgs => {
event.sender._sendInternal(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, ...responseArgs)
event._replyInternal(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, ...responseArgs)
})
})

View file

@ -16,7 +16,8 @@ const inheritedWebPreferences = new Map([
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true],
['webviewTag', false]
['webviewTag', false],
['nodeIntegrationInSubFrames', false]
])
// Copy attribute of |parent| to |child| if it is not defined in |child|.