guest-window-manager.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * Create and minimally track guest windows at the direction of the renderer
  3. * (via window.open). Here, "guest" roughly means "child" — it's not necessarily
  4. * emblematic of its process status; both in-process (same-origin) and
  5. * out-of-process (cross-origin) are created here. "Embedder" roughly means
  6. * "parent."
  7. */
  8. import { BrowserWindow } from 'electron/main';
  9. import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';
  10. import { parseFeatures } from '@electron/internal/browser/parse-features-string';
  11. type PostData = LoadURLOptions['postData']
  12. export type WindowOpenArgs = {
  13. url: string,
  14. frameName: string,
  15. features: string,
  16. }
  17. const frameNamesToWindow = new Map<string, WebContents>();
  18. const registerFrameNameToGuestWindow = (name: string, webContents: WebContents) => frameNamesToWindow.set(name, webContents);
  19. const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
  20. const getGuestWebContentsByFrameName = (name: string) => frameNamesToWindow.get(name);
  21. /**
  22. * `openGuestWindow` is called to create and setup event handling for the new
  23. * window.
  24. */
  25. export function openGuestWindow ({ embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener, createWindow }: {
  26. embedder: WebContents,
  27. guest?: WebContents,
  28. referrer: Referrer,
  29. disposition: string,
  30. postData?: PostData,
  31. overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
  32. windowOpenArgs: WindowOpenArgs,
  33. outlivesOpener: boolean,
  34. createWindow?: Electron.CreateWindowFunction
  35. }): void {
  36. const { url, frameName, features } = windowOpenArgs;
  37. const { options: parsedOptions } = parseFeatures(features);
  38. const browserWindowOptions = {
  39. show: true,
  40. width: 800,
  41. height: 600,
  42. ...parsedOptions,
  43. ...overrideBrowserWindowOptions
  44. };
  45. // To spec, subsequent window.open calls with the same frame name (`target` in
  46. // spec parlance) will reuse the previous window.
  47. // https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
  48. const existingWebContents = getGuestWebContentsByFrameName(frameName);
  49. if (existingWebContents) {
  50. if (existingWebContents.isDestroyed()) {
  51. // FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
  52. unregisterFrameName(frameName);
  53. } else {
  54. existingWebContents.loadURL(url);
  55. return;
  56. }
  57. }
  58. if (createWindow) {
  59. const webContents = createWindow({
  60. webContents: guest,
  61. ...browserWindowOptions
  62. });
  63. if (guest != null) {
  64. if (webContents !== guest) {
  65. throw new Error('Invalid webContents. Created window should be connected to webContents passed with options object.');
  66. }
  67. webContents.loadURL(url, {
  68. httpReferrer: referrer,
  69. ...(postData && {
  70. postData,
  71. extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
  72. })
  73. });
  74. handleWindowLifecycleEvents({ embedder, frameName, guest, outlivesOpener });
  75. }
  76. return;
  77. }
  78. const window = new BrowserWindow({
  79. webContents: guest,
  80. ...browserWindowOptions
  81. });
  82. if (!guest) {
  83. // When we open a new window from a link (via OpenURLFromTab),
  84. // the browser process is responsible for initiating navigation
  85. // in the new window.
  86. window.loadURL(url, {
  87. httpReferrer: referrer,
  88. ...(postData && {
  89. postData,
  90. extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
  91. })
  92. });
  93. }
  94. handleWindowLifecycleEvents({ embedder, frameName, guest: window.webContents, outlivesOpener });
  95. embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
  96. }
  97. /**
  98. * Manage the relationship between embedder window and guest window. When the
  99. * guest is destroyed, notify the embedder. When the embedder is destroyed, so
  100. * too is the guest destroyed; this is Electron convention and isn't based in
  101. * browser behavior.
  102. */
  103. const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
  104. embedder: WebContents,
  105. guest: WebContents,
  106. frameName: string,
  107. outlivesOpener: boolean
  108. }) {
  109. const closedByEmbedder = function () {
  110. guest.removeListener('destroyed', closedByUser);
  111. guest.destroy();
  112. };
  113. const closedByUser = function () {
  114. // Embedder might have been closed
  115. if (!embedder.isDestroyed() && !outlivesOpener) {
  116. embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
  117. }
  118. };
  119. if (!outlivesOpener) {
  120. embedder.once('current-render-view-deleted' as any, closedByEmbedder);
  121. }
  122. guest.once('destroyed', closedByUser);
  123. if (frameName) {
  124. registerFrameNameToGuestWindow(frameName, guest);
  125. guest.once('destroyed', function () {
  126. unregisterFrameName(frameName);
  127. });
  128. }
  129. };
  130. // Security options that child windows will always inherit from parent windows
  131. const securityWebPreferences: { [key: string]: boolean } = {
  132. contextIsolation: true,
  133. javascript: false,
  134. nodeIntegration: false,
  135. sandbox: true,
  136. webviewTag: false,
  137. nodeIntegrationInSubFrames: false,
  138. enableWebSQL: false
  139. };
  140. export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {} }: {
  141. embedder: WebContents,
  142. insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'],
  143. // Note that override preferences are considered elevated, and should only be
  144. // sourced from the main process, as they override security defaults. If you
  145. // have unvetted prefs, use parsedWebPreferences.
  146. secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'],
  147. }) {
  148. const parentWebPreferences = embedder.getLastWebPreferences()!;
  149. const securityWebPreferencesFromParent = (Object.keys(securityWebPreferences).reduce((map, key) => {
  150. if (securityWebPreferences[key] === parentWebPreferences[key as keyof Electron.WebPreferences]) {
  151. (map as any)[key] = parentWebPreferences[key as keyof Electron.WebPreferences];
  152. }
  153. return map;
  154. }, {} as Electron.WebPreferences));
  155. return {
  156. ...parsedWebPreferences,
  157. // Note that order is key here, we want to disallow the renderer's
  158. // ability to change important security options but allow main (via
  159. // setWindowOpenHandler) to change them.
  160. ...securityWebPreferencesFromParent,
  161. ...secureOverrideWebPreferences
  162. };
  163. }
  164. function formatPostDataHeaders (postData: PostData) {
  165. if (!postData) return;
  166. const { contentType, boundary } = parseContentTypeFormat(postData);
  167. if (boundary != null) { return `content-type: ${contentType}; boundary=${boundary}`; }
  168. return `content-type: ${contentType}`;
  169. }
  170. const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
  171. const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';
  172. // Figure out appropriate headers for post data.
  173. export const parseContentTypeFormat = function (postData: Exclude<PostData, undefined>) {
  174. if (postData.length) {
  175. if (postData[0].type === 'rawData') {
  176. // For multipart forms, the first element will start with the boundary
  177. // notice, which looks something like `------WebKitFormBoundary12345678`
  178. // Note, this regex would fail when submitting a urlencoded form with an
  179. // input attribute of name="--theKey", but, uhh, don't do that?
  180. const postDataFront = postData[0].bytes.toString();
  181. const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
  182. if (boundary) {
  183. return {
  184. boundary: boundary[0].substr(2),
  185. contentType: MULTIPART_CONTENT_TYPE
  186. };
  187. }
  188. }
  189. }
  190. // Either the form submission didn't contain any inputs (the postData array
  191. // was empty), or we couldn't find the boundary and thus we can assume this is
  192. // a key=value style form.
  193. return {
  194. contentType: URL_ENCODED_CONTENT_TYPE
  195. };
  196. };