api-context-bridge-spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. import { contextBridge, BrowserWindow, ipcMain } from 'electron'
  2. import { expect } from 'chai'
  3. import * as fs from 'fs-extra'
  4. import * as http from 'http'
  5. import { AddressInfo } from 'net'
  6. import * as os from 'os'
  7. import * as path from 'path'
  8. import { closeWindow } from './window-helpers'
  9. import { emittedOnce } from './events-helpers'
  10. const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge')
  11. describe('contextBridge', () => {
  12. let w: BrowserWindow
  13. let dir: string
  14. let server: http.Server
  15. before(async () => {
  16. server = http.createServer((req, res) => {
  17. res.setHeader('Content-Type', 'text/html')
  18. res.end('')
  19. })
  20. await new Promise(resolve => server.listen(0, resolve))
  21. })
  22. after(async () => {
  23. if (server) await new Promise(resolve => server.close(resolve))
  24. server = null as any
  25. })
  26. afterEach(async () => {
  27. await closeWindow(w)
  28. if (dir) await fs.remove(dir)
  29. })
  30. it('should not be accessible when contextIsolation is disabled', async () => {
  31. w = new BrowserWindow({
  32. show: false,
  33. webPreferences: {
  34. contextIsolation: false,
  35. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  36. }
  37. })
  38. const [,bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')))
  39. expect(bound).to.equal(false)
  40. })
  41. it('should be accessible when contextIsolation is enabled', async () => {
  42. w = new BrowserWindow({
  43. show: false,
  44. webPreferences: {
  45. contextIsolation: true,
  46. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  47. }
  48. })
  49. const [,bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')))
  50. expect(bound).to.equal(true)
  51. })
  52. const generateTests = (useSandbox: boolean) => {
  53. describe(`with sandbox=${useSandbox}`, () => {
  54. const makeBindingWindow = async (bindingCreator: Function) => {
  55. const preloadContent = `const electron_1 = require('electron');
  56. ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
  57. const gc=require('vm').runInNewContext('gc');
  58. electron_1.contextBridge.exposeInMainWorld('GCRunner', {
  59. run: () => gc()
  60. });`}
  61. (${bindingCreator.toString()})();`
  62. const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'))
  63. dir = tmpDir
  64. await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent)
  65. w = new BrowserWindow({
  66. show: false,
  67. webPreferences: {
  68. contextIsolation: true,
  69. nodeIntegration: true,
  70. sandbox: useSandbox,
  71. preload: path.resolve(tmpDir, 'preload.js'),
  72. additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
  73. }
  74. })
  75. await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)
  76. }
  77. const callWithBindings = async (fn: Function) => {
  78. return await w.webContents.executeJavaScript(`(${fn.toString()})(window)`)
  79. }
  80. const getGCInfo = async (): Promise<{
  81. trackedValues: number;
  82. }> => {
  83. const [, info] = await emittedOnce(ipcMain, 'gc-info', () => w.webContents.send('get-gc-info'));
  84. return info;
  85. };
  86. const forceGCOnWindow = async () => {
  87. w.webContents.debugger.attach();
  88. await w.webContents.debugger.sendCommand('HeapProfiler.enable');
  89. await w.webContents.debugger.sendCommand('HeapProfiler.collectGarbage');
  90. await w.webContents.debugger.sendCommand('HeapProfiler.disable');
  91. w.webContents.debugger.detach();
  92. };
  93. it('should proxy numbers', async () => {
  94. await makeBindingWindow(() => {
  95. contextBridge.exposeInMainWorld('example', {
  96. myNumber: 123,
  97. })
  98. })
  99. const result = await callWithBindings((root: any) => {
  100. return root.example.myNumber
  101. })
  102. expect(result).to.equal(123)
  103. })
  104. it('should make properties unwriteable', async () => {
  105. await makeBindingWindow(() => {
  106. contextBridge.exposeInMainWorld('example', {
  107. myNumber: 123,
  108. })
  109. })
  110. const result = await callWithBindings((root: any) => {
  111. root.example.myNumber = 456
  112. return root.example.myNumber
  113. })
  114. expect(result).to.equal(123)
  115. })
  116. it('should proxy strings', async () => {
  117. await makeBindingWindow(() => {
  118. contextBridge.exposeInMainWorld('example', {
  119. myString: 'my-words',
  120. })
  121. })
  122. const result = await callWithBindings((root: any) => {
  123. return root.example.myString
  124. })
  125. expect(result).to.equal('my-words')
  126. })
  127. it('should proxy arrays', async () => {
  128. await makeBindingWindow(() => {
  129. contextBridge.exposeInMainWorld('example', {
  130. myArr: [123, 'my-words'],
  131. })
  132. })
  133. const result = await callWithBindings((root: any) => {
  134. return root.example.myArr
  135. })
  136. expect(result).to.deep.equal([123, 'my-words'])
  137. })
  138. it('should make arrays immutable', async () => {
  139. await makeBindingWindow(() => {
  140. contextBridge.exposeInMainWorld('example', {
  141. myArr: [123, 'my-words'],
  142. })
  143. })
  144. const immutable = await callWithBindings((root: any) => {
  145. try {
  146. root.example.myArr.push(456)
  147. return false
  148. } catch {
  149. return true
  150. }
  151. })
  152. expect(immutable).to.equal(true)
  153. })
  154. it('should proxy booleans', async () => {
  155. await makeBindingWindow(() => {
  156. contextBridge.exposeInMainWorld('example', {
  157. myBool: true,
  158. })
  159. })
  160. const result = await callWithBindings((root: any) => {
  161. return root.example.myBool
  162. })
  163. expect(result).to.equal(true)
  164. })
  165. it('should proxy promises and resolve with the correct value', async () => {
  166. await makeBindingWindow(() => {
  167. contextBridge.exposeInMainWorld('example', {
  168. myPromise: Promise.resolve('i-resolved'),
  169. })
  170. })
  171. const result = await callWithBindings(async (root: any) => {
  172. return await root.example.myPromise
  173. })
  174. expect(result).to.equal('i-resolved')
  175. })
  176. it('should proxy promises and reject with the correct value', async () => {
  177. await makeBindingWindow(() => {
  178. contextBridge.exposeInMainWorld('example', {
  179. myPromise: Promise.reject('i-rejected'),
  180. })
  181. })
  182. const result = await callWithBindings(async (root: any) => {
  183. try {
  184. await root.example.myPromise
  185. return null
  186. } catch (err) {
  187. return err
  188. }
  189. })
  190. expect(result).to.equal('i-rejected')
  191. })
  192. it('should proxy promises and resolve with the correct value if it resolves later', async () => {
  193. await makeBindingWindow(() => {
  194. contextBridge.exposeInMainWorld('example', {
  195. myPromise: () => new Promise(r => setTimeout(() => r('delayed'), 20)),
  196. })
  197. })
  198. const result = await callWithBindings(async (root: any) => {
  199. return await root.example.myPromise()
  200. })
  201. expect(result).to.equal('delayed')
  202. })
  203. it('should proxy nested promises correctly', async () => {
  204. await makeBindingWindow(() => {
  205. contextBridge.exposeInMainWorld('example', {
  206. myPromise: () => new Promise(r => setTimeout(() => r(Promise.resolve(123)), 20)),
  207. })
  208. })
  209. const result = await callWithBindings(async (root: any) => {
  210. return await root.example.myPromise()
  211. })
  212. expect(result).to.equal(123)
  213. })
  214. it('should proxy methods', async () => {
  215. await makeBindingWindow(() => {
  216. contextBridge.exposeInMainWorld('example', {
  217. getNumber: () => 123,
  218. getString: () => 'help',
  219. getBoolean: () => false,
  220. getPromise: async () => 'promise'
  221. })
  222. })
  223. const result = await callWithBindings(async (root: any) => {
  224. return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()]
  225. })
  226. expect(result).to.deep.equal([123, 'help', false, 'promise'])
  227. })
  228. it('should proxy methods that are callable multiple times', async () => {
  229. await makeBindingWindow(() => {
  230. contextBridge.exposeInMainWorld('example', {
  231. doThing: () => 123
  232. })
  233. })
  234. const result = await callWithBindings(async (root: any) => {
  235. return [root.example.doThing(), root.example.doThing(), root.example.doThing()]
  236. })
  237. expect(result).to.deep.equal([123, 123, 123])
  238. })
  239. it('should proxy methods in the reverse direction', async () => {
  240. await makeBindingWindow(() => {
  241. contextBridge.exposeInMainWorld('example', {
  242. callWithNumber: (fn: any) => fn(123),
  243. })
  244. })
  245. const result = await callWithBindings(async (root: any) => {
  246. return root.example.callWithNumber((n: number) => n + 1)
  247. })
  248. expect(result).to.equal(124)
  249. })
  250. it('should proxy promises in the reverse direction', async () => {
  251. await makeBindingWindow(() => {
  252. contextBridge.exposeInMainWorld('example', {
  253. getPromiseValue: async (p: Promise<any>) => await p,
  254. })
  255. })
  256. const result = await callWithBindings(async (root: any) => {
  257. return await root.example.getPromiseValue(Promise.resolve('my-proxied-value'))
  258. })
  259. expect(result).to.equal('my-proxied-value')
  260. })
  261. it('should proxy objects with number keys', async () => {
  262. await makeBindingWindow(() => {
  263. contextBridge.exposeInMainWorld('example', {
  264. [1]: 123,
  265. [2]: 456,
  266. '3': 789
  267. })
  268. })
  269. const result = await callWithBindings(async (root: any) => {
  270. return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)]
  271. })
  272. expect(result).to.deep.equal([123, 456, 789, false])
  273. })
  274. it('it should proxy null and undefined correctly', async () => {
  275. await makeBindingWindow(() => {
  276. contextBridge.exposeInMainWorld('example', {
  277. values: [null, undefined]
  278. })
  279. })
  280. const result = await callWithBindings((root: any) => {
  281. // Convert to strings as although the context bridge keeps the right value
  282. // IPC does not
  283. return root.example.values.map((val: any) => `${val}`)
  284. })
  285. expect(result).to.deep.equal(['null', 'undefined'])
  286. })
  287. it('should proxy typed arrays and regexps through the serializer', async () => {
  288. await makeBindingWindow(() => {
  289. contextBridge.exposeInMainWorld('example', {
  290. arr: new Uint8Array(100),
  291. regexp: /a/g
  292. })
  293. })
  294. const result = await callWithBindings((root: any) => {
  295. return [root.example.arr.__proto__ === Uint8Array.prototype, root.example.regexp.__proto__ === RegExp.prototype]
  296. })
  297. expect(result).to.deep.equal([true, true])
  298. })
  299. it('it should handle recursive objects', async () => {
  300. await makeBindingWindow(() => {
  301. const o: any = { value: 135 }
  302. o.o = o
  303. contextBridge.exposeInMainWorld('example', {
  304. o,
  305. })
  306. })
  307. const result = await callWithBindings((root: any) => {
  308. return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value]
  309. })
  310. expect(result).to.deep.equal([135, 135, 135])
  311. })
  312. // Can only run tests which use the GCRunner in non-sandboxed environments
  313. if (!useSandbox) {
  314. it('should release the global hold on methods sent across contexts', async () => {
  315. await makeBindingWindow(() => {
  316. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: process.electronBinding('v8_util').getWeaklyTrackedValues().length }));
  317. const { weaklyTrackValue } = process.electronBinding('v8_util');
  318. contextBridge.exposeInMainWorld('example', {
  319. getFunction: () => () => 123,
  320. track: weaklyTrackValue
  321. });
  322. });
  323. await callWithBindings(async (root: any) => {
  324. root.GCRunner.run();
  325. });
  326. expect((await getGCInfo()).trackedValues).to.equal(0);
  327. await callWithBindings(async (root: any) => {
  328. const fn = root.example.getFunction();
  329. root.example.track(fn);
  330. root.x = [fn];
  331. });
  332. expect((await getGCInfo()).trackedValues).to.equal(1);
  333. await callWithBindings(async (root: any) => {
  334. root.x = [];
  335. root.GCRunner.run();
  336. });
  337. expect((await getGCInfo()).trackedValues).to.equal(0);
  338. });
  339. }
  340. if (useSandbox) {
  341. it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => {
  342. await makeBindingWindow(() => {
  343. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: process.electronBinding('v8_util').getWeaklyTrackedValues().length }));
  344. const { weaklyTrackValue } = process.electronBinding('v8_util');
  345. contextBridge.exposeInMainWorld('example', {
  346. getFunction: () => () => 123,
  347. track: weaklyTrackValue
  348. });
  349. require('electron').ipcRenderer.send('window-ready-for-tasking');
  350. });
  351. const loadPromise = emittedOnce(ipcMain, 'window-ready-for-tasking');
  352. expect((await getGCInfo()).trackedValues).to.equal(0);
  353. await callWithBindings((root: any) => {
  354. root.example.track(root.example.getFunction());
  355. });
  356. expect((await getGCInfo()).trackedValues).to.equal(1);
  357. await callWithBindings((root: any) => {
  358. root.location.reload();
  359. });
  360. await loadPromise;
  361. await forceGCOnWindow();
  362. // If this is ever "2" it means we leaked the exposed function and
  363. // therefore the entire context after a reload
  364. expect((await getGCInfo()).trackedValues).to.equal(0);
  365. });
  366. }
  367. it('it should not let you overwrite existing exposed things', async () => {
  368. await makeBindingWindow(() => {
  369. let threw = false
  370. contextBridge.exposeInMainWorld('example', {
  371. attempt: 1,
  372. getThrew: () => threw
  373. })
  374. try {
  375. contextBridge.exposeInMainWorld('example', {
  376. attempt: 2,
  377. getThrew: () => threw
  378. })
  379. } catch {
  380. threw = true
  381. }
  382. })
  383. const result = await callWithBindings((root: any) => {
  384. return [root.example.attempt, root.example.getThrew()]
  385. })
  386. expect(result).to.deep.equal([1, true])
  387. })
  388. it('should work with complex nested methods and promises', async () => {
  389. await makeBindingWindow(() => {
  390. contextBridge.exposeInMainWorld('example', {
  391. first: (second: Function) => second(async (fourth: Function) => {
  392. return await fourth()
  393. })
  394. })
  395. })
  396. const result = await callWithBindings((root: any) => {
  397. return root.example.first((third: Function) => {
  398. return third(() => Promise.resolve('final value'))
  399. })
  400. })
  401. expect(result).to.equal('final value')
  402. })
  403. it('should throw an error when recursion depth is exceeded', async () => {
  404. await makeBindingWindow(() => {
  405. contextBridge.exposeInMainWorld('example', {
  406. doThing: (a: any) => console.log(a)
  407. })
  408. })
  409. let threw = await callWithBindings((root: any) => {
  410. try {
  411. let a: any = []
  412. for (let i = 0; i < 999; i++) {
  413. a = [ a ]
  414. }
  415. root.example.doThing(a)
  416. return false
  417. } catch {
  418. return true
  419. }
  420. })
  421. expect(threw).to.equal(false)
  422. threw = await callWithBindings((root: any) => {
  423. try {
  424. let a: any = []
  425. for (let i = 0; i < 1000; i++) {
  426. a = [ a ]
  427. }
  428. root.example.doThing(a)
  429. return false
  430. } catch {
  431. return true
  432. }
  433. })
  434. expect(threw).to.equal(true)
  435. })
  436. it('should not leak prototypes', async () => {
  437. await makeBindingWindow(() => {
  438. contextBridge.exposeInMainWorld('example', {
  439. number: 123,
  440. string: 'string',
  441. boolean: true,
  442. arr: [123, 'string', true, ['foo']],
  443. getObject: () => ({ thing: 123 }),
  444. getNumber: () => 123,
  445. getString: () => 'string',
  446. getBoolean: () => true,
  447. getArr: () => [123, 'string', true, ['foo']],
  448. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']]}),
  449. getFunctionFromFunction: async () => () => null,
  450. object: {
  451. number: 123,
  452. string: 'string',
  453. boolean: true,
  454. arr: [123, 'string', true, ['foo']],
  455. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']]}),
  456. },
  457. receiveArguments: (fn: any) => fn({ key: 'value' })
  458. })
  459. })
  460. const result = await callWithBindings(async (root: any) => {
  461. const { example } = root
  462. let arg: any
  463. example.receiveArguments((o: any) => { arg = o })
  464. const protoChecks = [
  465. [example, Object],
  466. [example.number, Number],
  467. [example.string, String],
  468. [example.boolean, Boolean],
  469. [example.arr, Array],
  470. [example.arr[0], Number],
  471. [example.arr[1], String],
  472. [example.arr[2], Boolean],
  473. [example.arr[3], Array],
  474. [example.arr[3][0], String],
  475. [example.getNumber, Function],
  476. [example.getNumber(), Number],
  477. [example.getObject(), Object],
  478. [example.getString(), String],
  479. [example.getBoolean(), Boolean],
  480. [example.getArr(), Array],
  481. [example.getArr()[0], Number],
  482. [example.getArr()[1], String],
  483. [example.getArr()[2], Boolean],
  484. [example.getArr()[3], Array],
  485. [example.getArr()[3][0], String],
  486. [example.getFunctionFromFunction, Function],
  487. [example.getFunctionFromFunction(), Promise],
  488. [await example.getFunctionFromFunction(), Function],
  489. [example.getPromise(), Promise],
  490. [await example.getPromise(), Object],
  491. [(await example.getPromise()).number, Number],
  492. [(await example.getPromise()).string, String],
  493. [(await example.getPromise()).boolean, Boolean],
  494. [(await example.getPromise()).fn, Function],
  495. [(await example.getPromise()).fn(), String],
  496. [(await example.getPromise()).arr, Array],
  497. [(await example.getPromise()).arr[0], Number],
  498. [(await example.getPromise()).arr[1], String],
  499. [(await example.getPromise()).arr[2], Boolean],
  500. [(await example.getPromise()).arr[3], Array],
  501. [(await example.getPromise()).arr[3][0], String],
  502. [example.object, Object],
  503. [example.object.number, Number],
  504. [example.object.string, String],
  505. [example.object.boolean, Boolean],
  506. [example.object.arr, Array],
  507. [example.object.arr[0], Number],
  508. [example.object.arr[1], String],
  509. [example.object.arr[2], Boolean],
  510. [example.object.arr[3], Array],
  511. [example.object.arr[3][0], String],
  512. [await example.object.getPromise(), Object],
  513. [(await example.object.getPromise()).number, Number],
  514. [(await example.object.getPromise()).string, String],
  515. [(await example.object.getPromise()).boolean, Boolean],
  516. [(await example.object.getPromise()).fn, Function],
  517. [(await example.object.getPromise()).fn(), String],
  518. [(await example.object.getPromise()).arr, Array],
  519. [(await example.object.getPromise()).arr[0], Number],
  520. [(await example.object.getPromise()).arr[1], String],
  521. [(await example.object.getPromise()).arr[2], Boolean],
  522. [(await example.object.getPromise()).arr[3], Array],
  523. [(await example.object.getPromise()).arr[3][0], String],
  524. [arg, Object],
  525. [arg.key, String]
  526. ]
  527. return {
  528. protoMatches: protoChecks.map(([a, Constructor]) => a.__proto__ === Constructor.prototype)
  529. }
  530. })
  531. // Every protomatch should be true
  532. expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true))
  533. })
  534. })
  535. }
  536. generateTests(true)
  537. generateTests(false)
  538. })