api-context-bridge-spec.ts 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  1. import { BrowserWindow, ipcMain } from 'electron/main';
  2. import { contextBridge } from 'electron/renderer';
  3. import { expect } from 'chai';
  4. import * as fs from 'fs-extra';
  5. import * as http from 'http';
  6. import * as os from 'os';
  7. import * as path from 'path';
  8. import * as cp from 'child_process';
  9. import { closeWindow } from './window-helpers';
  10. import { emittedOnce } from './events-helpers';
  11. import { AddressInfo } from 'net';
  12. const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge');
  13. describe('contextBridge', () => {
  14. let w: BrowserWindow;
  15. let dir: string;
  16. let server: http.Server;
  17. before(async () => {
  18. server = http.createServer((req, res) => {
  19. res.setHeader('Content-Type', 'text/html');
  20. res.end('');
  21. });
  22. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  23. });
  24. after(async () => {
  25. if (server) await new Promise(resolve => server.close(resolve));
  26. server = null as any;
  27. });
  28. afterEach(async () => {
  29. await closeWindow(w);
  30. if (dir) await fs.remove(dir);
  31. });
  32. it('should not be accessible when contextIsolation is disabled', async () => {
  33. w = new BrowserWindow({
  34. show: false,
  35. webPreferences: {
  36. contextIsolation: false,
  37. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  38. }
  39. });
  40. const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')));
  41. expect(bound).to.equal(false);
  42. });
  43. it('should be accessible when contextIsolation is enabled', async () => {
  44. w = new BrowserWindow({
  45. show: false,
  46. webPreferences: {
  47. contextIsolation: true,
  48. preload: path.resolve(fixturesPath, 'can-bind-preload.js')
  49. }
  50. });
  51. const [, bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html')));
  52. expect(bound).to.equal(true);
  53. });
  54. const generateTests = (useSandbox: boolean) => {
  55. describe(`with sandbox=${useSandbox}`, () => {
  56. const makeBindingWindow = async (bindingCreator: Function) => {
  57. const preloadContent = `const renderer_1 = require('electron');
  58. ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
  59. const gc=require('vm').runInNewContext('gc');
  60. renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
  61. run: () => gc()
  62. });`}
  63. (${bindingCreator.toString()})();`;
  64. const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
  65. dir = tmpDir;
  66. await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent);
  67. w = new BrowserWindow({
  68. show: false,
  69. webPreferences: {
  70. contextIsolation: true,
  71. nodeIntegration: true,
  72. sandbox: useSandbox,
  73. preload: path.resolve(tmpDir, 'preload.js'),
  74. additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
  75. }
  76. });
  77. await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`);
  78. };
  79. const callWithBindings = (fn: Function) =>
  80. w.webContents.executeJavaScript(`(${fn.toString()})(window)`);
  81. const getGCInfo = async (): Promise<{
  82. trackedValues: number;
  83. }> => {
  84. const [, info] = await emittedOnce(ipcMain, 'gc-info', () => w.webContents.send('get-gc-info'));
  85. return info;
  86. };
  87. const forceGCOnWindow = async () => {
  88. w.webContents.debugger.attach();
  89. await w.webContents.debugger.sendCommand('HeapProfiler.enable');
  90. await w.webContents.debugger.sendCommand('HeapProfiler.collectGarbage');
  91. await w.webContents.debugger.sendCommand('HeapProfiler.disable');
  92. w.webContents.debugger.detach();
  93. };
  94. it('should proxy numbers', async () => {
  95. await makeBindingWindow(() => {
  96. contextBridge.exposeInMainWorld('example', 123);
  97. });
  98. const result = await callWithBindings((root: any) => {
  99. return root.example;
  100. });
  101. expect(result).to.equal(123);
  102. });
  103. it('should make global properties read-only', async () => {
  104. await makeBindingWindow(() => {
  105. contextBridge.exposeInMainWorld('example', 123);
  106. });
  107. const result = await callWithBindings((root: any) => {
  108. root.example = 456;
  109. return root.example;
  110. });
  111. expect(result).to.equal(123);
  112. });
  113. it('should proxy nested numbers', async () => {
  114. await makeBindingWindow(() => {
  115. contextBridge.exposeInMainWorld('example', {
  116. myNumber: 123
  117. });
  118. });
  119. const result = await callWithBindings((root: any) => {
  120. return root.example.myNumber;
  121. });
  122. expect(result).to.equal(123);
  123. });
  124. it('should make properties unwriteable', async () => {
  125. await makeBindingWindow(() => {
  126. contextBridge.exposeInMainWorld('example', {
  127. myNumber: 123
  128. });
  129. });
  130. const result = await callWithBindings((root: any) => {
  131. root.example.myNumber = 456;
  132. return root.example.myNumber;
  133. });
  134. expect(result).to.equal(123);
  135. });
  136. it('should proxy strings', async () => {
  137. await makeBindingWindow(() => {
  138. contextBridge.exposeInMainWorld('example', 'my-words');
  139. });
  140. const result = await callWithBindings((root: any) => {
  141. return root.example;
  142. });
  143. expect(result).to.equal('my-words');
  144. });
  145. it('should proxy nested strings', async () => {
  146. await makeBindingWindow(() => {
  147. contextBridge.exposeInMainWorld('example', {
  148. myString: 'my-words'
  149. });
  150. });
  151. const result = await callWithBindings((root: any) => {
  152. return root.example.myString;
  153. });
  154. expect(result).to.equal('my-words');
  155. });
  156. it('should proxy arrays', async () => {
  157. await makeBindingWindow(() => {
  158. contextBridge.exposeInMainWorld('example', [123, 'my-words']);
  159. });
  160. const result = await callWithBindings((root: any) => {
  161. return [root.example, Array.isArray(root.example)];
  162. });
  163. expect(result).to.deep.equal([[123, 'my-words'], true]);
  164. });
  165. it('should proxy nested arrays', async () => {
  166. await makeBindingWindow(() => {
  167. contextBridge.exposeInMainWorld('example', {
  168. myArr: [123, 'my-words']
  169. });
  170. });
  171. const result = await callWithBindings((root: any) => {
  172. return root.example.myArr;
  173. });
  174. expect(result).to.deep.equal([123, 'my-words']);
  175. });
  176. it('should make arrays immutable', async () => {
  177. await makeBindingWindow(() => {
  178. contextBridge.exposeInMainWorld('example', [123, 'my-words']);
  179. });
  180. const immutable = await callWithBindings((root: any) => {
  181. try {
  182. root.example.push(456);
  183. return false;
  184. } catch {
  185. return true;
  186. }
  187. });
  188. expect(immutable).to.equal(true);
  189. });
  190. it('should make nested arrays immutable', async () => {
  191. await makeBindingWindow(() => {
  192. contextBridge.exposeInMainWorld('example', {
  193. myArr: [123, 'my-words']
  194. });
  195. });
  196. const immutable = await callWithBindings((root: any) => {
  197. try {
  198. root.example.myArr.push(456);
  199. return false;
  200. } catch {
  201. return true;
  202. }
  203. });
  204. expect(immutable).to.equal(true);
  205. });
  206. it('should proxy booleans', async () => {
  207. await makeBindingWindow(() => {
  208. contextBridge.exposeInMainWorld('example', true);
  209. });
  210. const result = await callWithBindings((root: any) => {
  211. return root.example;
  212. });
  213. expect(result).to.equal(true);
  214. });
  215. it('should proxy nested booleans', async () => {
  216. await makeBindingWindow(() => {
  217. contextBridge.exposeInMainWorld('example', {
  218. myBool: true
  219. });
  220. });
  221. const result = await callWithBindings((root: any) => {
  222. return root.example.myBool;
  223. });
  224. expect(result).to.equal(true);
  225. });
  226. it('should proxy promises and resolve with the correct value', async () => {
  227. await makeBindingWindow(() => {
  228. contextBridge.exposeInMainWorld('example',
  229. Promise.resolve('i-resolved')
  230. );
  231. });
  232. const result = await callWithBindings((root: any) => {
  233. return root.example;
  234. });
  235. expect(result).to.equal('i-resolved');
  236. });
  237. it('should proxy nested promises and resolve with the correct value', async () => {
  238. await makeBindingWindow(() => {
  239. contextBridge.exposeInMainWorld('example', {
  240. myPromise: Promise.resolve('i-resolved')
  241. });
  242. });
  243. const result = await callWithBindings((root: any) => {
  244. return root.example.myPromise;
  245. });
  246. expect(result).to.equal('i-resolved');
  247. });
  248. it('should proxy promises and reject with the correct value', async () => {
  249. await makeBindingWindow(() => {
  250. contextBridge.exposeInMainWorld('example', Promise.reject(new Error('i-rejected')));
  251. });
  252. const result = await callWithBindings(async (root: any) => {
  253. try {
  254. await root.example;
  255. return null;
  256. } catch (err) {
  257. return err;
  258. }
  259. });
  260. expect(result).to.be.an.instanceOf(Error).with.property('message', 'i-rejected');
  261. });
  262. it('should proxy nested promises and reject with the correct value', async () => {
  263. await makeBindingWindow(() => {
  264. contextBridge.exposeInMainWorld('example', {
  265. myPromise: Promise.reject(new Error('i-rejected'))
  266. });
  267. });
  268. const result = await callWithBindings(async (root: any) => {
  269. try {
  270. await root.example.myPromise;
  271. return null;
  272. } catch (err) {
  273. return err;
  274. }
  275. });
  276. expect(result).to.be.an.instanceOf(Error).with.property('message', 'i-rejected');
  277. });
  278. it('should proxy promises and resolve with the correct value if it resolves later', async () => {
  279. await makeBindingWindow(() => {
  280. contextBridge.exposeInMainWorld('example', {
  281. myPromise: () => new Promise(resolve => setTimeout(() => resolve('delayed'), 20))
  282. });
  283. });
  284. const result = await callWithBindings((root: any) => {
  285. return root.example.myPromise();
  286. });
  287. expect(result).to.equal('delayed');
  288. });
  289. it('should proxy nested promises correctly', async () => {
  290. await makeBindingWindow(() => {
  291. contextBridge.exposeInMainWorld('example', {
  292. myPromise: () => new Promise(resolve => setTimeout(() => resolve(Promise.resolve(123)), 20))
  293. });
  294. });
  295. const result = await callWithBindings((root: any) => {
  296. return root.example.myPromise();
  297. });
  298. expect(result).to.equal(123);
  299. });
  300. it('should proxy methods', async () => {
  301. await makeBindingWindow(() => {
  302. contextBridge.exposeInMainWorld('example', {
  303. getNumber: () => 123,
  304. getString: () => 'help',
  305. getBoolean: () => false,
  306. getPromise: async () => 'promise'
  307. });
  308. });
  309. const result = await callWithBindings(async (root: any) => {
  310. return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()];
  311. });
  312. expect(result).to.deep.equal([123, 'help', false, 'promise']);
  313. });
  314. it('should proxy functions', async () => {
  315. await makeBindingWindow(() => {
  316. contextBridge.exposeInMainWorld('example', () => 'return-value');
  317. });
  318. const result = await callWithBindings(async (root: any) => {
  319. return root.example();
  320. });
  321. expect(result).equal('return-value');
  322. });
  323. it('should not double-proxy functions when they are returned to their origin side of the bridge', async () => {
  324. await makeBindingWindow(() => {
  325. contextBridge.exposeInMainWorld('example', (fn: any) => fn);
  326. });
  327. const result = await callWithBindings(async (root: any) => {
  328. const fn = () => null;
  329. return root.example(fn) === fn;
  330. });
  331. expect(result).equal(true);
  332. });
  333. it('should properly handle errors thrown in proxied functions', async () => {
  334. await makeBindingWindow(() => {
  335. contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); });
  336. });
  337. const result = await callWithBindings(async (root: any) => {
  338. try {
  339. root.example();
  340. } catch (e) {
  341. return e.message;
  342. }
  343. });
  344. expect(result).equal('oh no');
  345. });
  346. it('should proxy methods that are callable multiple times', async () => {
  347. await makeBindingWindow(() => {
  348. contextBridge.exposeInMainWorld('example', {
  349. doThing: () => 123
  350. });
  351. });
  352. const result = await callWithBindings(async (root: any) => {
  353. return [root.example.doThing(), root.example.doThing(), root.example.doThing()];
  354. });
  355. expect(result).to.deep.equal([123, 123, 123]);
  356. });
  357. it('should proxy methods in the reverse direction', async () => {
  358. await makeBindingWindow(() => {
  359. contextBridge.exposeInMainWorld('example', {
  360. callWithNumber: (fn: any) => fn(123)
  361. });
  362. });
  363. const result = await callWithBindings(async (root: any) => {
  364. return root.example.callWithNumber((n: number) => n + 1);
  365. });
  366. expect(result).to.equal(124);
  367. });
  368. it('should proxy promises in the reverse direction', async () => {
  369. await makeBindingWindow(() => {
  370. contextBridge.exposeInMainWorld('example', {
  371. getPromiseValue: (p: Promise<any>) => p
  372. });
  373. });
  374. const result = await callWithBindings((root: any) => {
  375. return root.example.getPromiseValue(Promise.resolve('my-proxied-value'));
  376. });
  377. expect(result).to.equal('my-proxied-value');
  378. });
  379. it('should proxy objects with number keys', async () => {
  380. await makeBindingWindow(() => {
  381. contextBridge.exposeInMainWorld('example', {
  382. 1: 123,
  383. 2: 456,
  384. 3: 789
  385. });
  386. });
  387. const result = await callWithBindings(async (root: any) => {
  388. return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)];
  389. });
  390. expect(result).to.deep.equal([123, 456, 789, false]);
  391. });
  392. it('it should proxy null', async () => {
  393. await makeBindingWindow(() => {
  394. contextBridge.exposeInMainWorld('example', null);
  395. });
  396. const result = await callWithBindings((root: any) => {
  397. // Convert to strings as although the context bridge keeps the right value
  398. // IPC does not
  399. return `${root.example}`;
  400. });
  401. expect(result).to.deep.equal('null');
  402. });
  403. it('it should proxy undefined', async () => {
  404. await makeBindingWindow(() => {
  405. contextBridge.exposeInMainWorld('example', undefined);
  406. });
  407. const result = await callWithBindings((root: any) => {
  408. // Convert to strings as although the context bridge keeps the right value
  409. // IPC does not
  410. return `${root.example}`;
  411. });
  412. expect(result).to.deep.equal('undefined');
  413. });
  414. it('it should proxy nested null and undefined correctly', async () => {
  415. await makeBindingWindow(() => {
  416. contextBridge.exposeInMainWorld('example', {
  417. values: [null, undefined]
  418. });
  419. });
  420. const result = await callWithBindings((root: any) => {
  421. // Convert to strings as although the context bridge keeps the right value
  422. // IPC does not
  423. return root.example.values.map((val: any) => `${val}`);
  424. });
  425. expect(result).to.deep.equal(['null', 'undefined']);
  426. });
  427. it('should proxy symbols', async () => {
  428. await makeBindingWindow(() => {
  429. const mySymbol = Symbol('unique');
  430. const isSymbol = (s: Symbol) => s === mySymbol;
  431. contextBridge.exposeInMainWorld('symbol', mySymbol);
  432. contextBridge.exposeInMainWorld('isSymbol', isSymbol);
  433. });
  434. const result = await callWithBindings((root: any) => {
  435. return root.isSymbol(root.symbol);
  436. });
  437. expect(result).to.equal(true, 'symbols should be equal across contexts');
  438. });
  439. it('should proxy symbols such that symbol equality works', async () => {
  440. await makeBindingWindow(() => {
  441. const mySymbol = Symbol('unique');
  442. contextBridge.exposeInMainWorld('example', {
  443. getSymbol: () => mySymbol,
  444. isSymbol: (s: Symbol) => s === mySymbol
  445. });
  446. });
  447. const result = await callWithBindings((root: any) => {
  448. return root.example.isSymbol(root.example.getSymbol());
  449. });
  450. expect(result).to.equal(true, 'symbols should be equal across contexts');
  451. });
  452. it('should proxy symbols such that symbol key lookup works', async () => {
  453. await makeBindingWindow(() => {
  454. const mySymbol = Symbol('unique');
  455. contextBridge.exposeInMainWorld('example', {
  456. getSymbol: () => mySymbol,
  457. getObject: () => ({ [mySymbol]: 123 })
  458. });
  459. });
  460. const result = await callWithBindings((root: any) => {
  461. return root.example.getObject()[root.example.getSymbol()];
  462. });
  463. expect(result).to.equal(123, 'symbols key lookup should work across contexts');
  464. });
  465. it('should proxy typed arrays', async () => {
  466. await makeBindingWindow(() => {
  467. contextBridge.exposeInMainWorld('example', new Uint8Array(100));
  468. });
  469. const result = await callWithBindings((root: any) => {
  470. return Object.getPrototypeOf(root.example) === Uint8Array.prototype;
  471. });
  472. expect(result).equal(true);
  473. });
  474. it('should proxy regexps', async () => {
  475. await makeBindingWindow(() => {
  476. contextBridge.exposeInMainWorld('example', /a/g);
  477. });
  478. const result = await callWithBindings((root: any) => {
  479. return Object.getPrototypeOf(root.example) === RegExp.prototype;
  480. });
  481. expect(result).equal(true);
  482. });
  483. it('should proxy typed arrays and regexps through the serializer', async () => {
  484. await makeBindingWindow(() => {
  485. contextBridge.exposeInMainWorld('example', {
  486. arr: new Uint8Array(100),
  487. regexp: /a/g
  488. });
  489. });
  490. const result = await callWithBindings((root: any) => {
  491. return [
  492. Object.getPrototypeOf(root.example.arr) === Uint8Array.prototype,
  493. Object.getPrototypeOf(root.example.regexp) === RegExp.prototype
  494. ];
  495. });
  496. expect(result).to.deep.equal([true, true]);
  497. });
  498. it('should handle recursive objects', async () => {
  499. await makeBindingWindow(() => {
  500. const o: any = { value: 135 };
  501. o.o = o;
  502. contextBridge.exposeInMainWorld('example', {
  503. o
  504. });
  505. });
  506. const result = await callWithBindings((root: any) => {
  507. return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value];
  508. });
  509. expect(result).to.deep.equal([135, 135, 135]);
  510. });
  511. it('should handle DOM elements', async () => {
  512. await makeBindingWindow(() => {
  513. contextBridge.exposeInMainWorld('example', {
  514. getElem: () => document.body
  515. });
  516. });
  517. const result = await callWithBindings((root: any) => {
  518. return [root.example.getElem().tagName, root.example.getElem().constructor.name, typeof root.example.getElem().querySelector];
  519. });
  520. expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']);
  521. });
  522. it('should handle DOM elements going backwards over the bridge', async () => {
  523. await makeBindingWindow(() => {
  524. contextBridge.exposeInMainWorld('example', {
  525. getElemInfo: (fn: Function) => {
  526. const elem = fn();
  527. return [elem.tagName, elem.constructor.name, typeof elem.querySelector];
  528. }
  529. });
  530. });
  531. const result = await callWithBindings((root: any) => {
  532. return root.example.getElemInfo(() => document.body);
  533. });
  534. expect(result).to.deep.equal(['BODY', 'HTMLBodyElement', 'function']);
  535. });
  536. // Can only run tests which use the GCRunner in non-sandboxed environments
  537. if (!useSandbox) {
  538. it('should release the global hold on methods sent across contexts', async () => {
  539. await makeBindingWindow(() => {
  540. const trackedValues: WeakRef<object>[] = [];
  541. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: trackedValues.filter(value => value.deref()).length }));
  542. contextBridge.exposeInMainWorld('example', {
  543. getFunction: () => () => 123,
  544. track: (value: object) => { trackedValues.push(new WeakRef(value)); }
  545. });
  546. });
  547. await callWithBindings(async (root: any) => {
  548. root.GCRunner.run();
  549. });
  550. expect((await getGCInfo()).trackedValues).to.equal(0);
  551. await callWithBindings(async (root: any) => {
  552. const fn = root.example.getFunction();
  553. root.example.track(fn);
  554. root.x = [fn];
  555. });
  556. expect((await getGCInfo()).trackedValues).to.equal(1);
  557. await callWithBindings(async (root: any) => {
  558. root.x = [];
  559. root.GCRunner.run();
  560. });
  561. expect((await getGCInfo()).trackedValues).to.equal(0);
  562. });
  563. }
  564. if (useSandbox) {
  565. it('should not leak the global hold on methods sent across contexts when reloading a sandboxed renderer', async () => {
  566. await makeBindingWindow(() => {
  567. const trackedValues: WeakRef<object>[] = [];
  568. require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', { trackedValues: trackedValues.filter(value => value.deref()).length }));
  569. contextBridge.exposeInMainWorld('example', {
  570. getFunction: () => () => 123,
  571. track: (value: object) => { trackedValues.push(new WeakRef(value)); }
  572. });
  573. require('electron').ipcRenderer.send('window-ready-for-tasking');
  574. });
  575. const loadPromise = emittedOnce(ipcMain, 'window-ready-for-tasking');
  576. expect((await getGCInfo()).trackedValues).to.equal(0);
  577. await callWithBindings((root: any) => {
  578. root.example.track(root.example.getFunction());
  579. });
  580. expect((await getGCInfo()).trackedValues).to.equal(1);
  581. await callWithBindings((root: any) => {
  582. root.location.reload();
  583. });
  584. await loadPromise;
  585. await forceGCOnWindow();
  586. // If this is ever "2" it means we leaked the exposed function and
  587. // therefore the entire context after a reload
  588. expect((await getGCInfo()).trackedValues).to.equal(0);
  589. });
  590. }
  591. it('it should not let you overwrite existing exposed things', async () => {
  592. await makeBindingWindow(() => {
  593. let threw = false;
  594. contextBridge.exposeInMainWorld('example', {
  595. attempt: 1,
  596. getThrew: () => threw
  597. });
  598. try {
  599. contextBridge.exposeInMainWorld('example', {
  600. attempt: 2,
  601. getThrew: () => threw
  602. });
  603. } catch {
  604. threw = true;
  605. }
  606. });
  607. const result = await callWithBindings((root: any) => {
  608. return [root.example.attempt, root.example.getThrew()];
  609. });
  610. expect(result).to.deep.equal([1, true]);
  611. });
  612. it('should work with complex nested methods and promises', async () => {
  613. await makeBindingWindow(() => {
  614. contextBridge.exposeInMainWorld('example', {
  615. first: (second: Function) => second((fourth: Function) => {
  616. return fourth();
  617. })
  618. });
  619. });
  620. const result = await callWithBindings((root: any) => {
  621. return root.example.first((third: Function) => {
  622. return third(() => Promise.resolve('final value'));
  623. });
  624. });
  625. expect(result).to.equal('final value');
  626. });
  627. it('should work with complex nested methods and promises attached directly to the global', async () => {
  628. await makeBindingWindow(() => {
  629. contextBridge.exposeInMainWorld('example',
  630. (second: Function) => second((fourth: Function) => {
  631. return fourth();
  632. })
  633. );
  634. });
  635. const result = await callWithBindings((root: any) => {
  636. return root.example((third: Function) => {
  637. return third(() => Promise.resolve('final value'));
  638. });
  639. });
  640. expect(result).to.equal('final value');
  641. });
  642. it('should throw an error when recursion depth is exceeded', async () => {
  643. await makeBindingWindow(() => {
  644. contextBridge.exposeInMainWorld('example', {
  645. doThing: (a: any) => console.log(a)
  646. });
  647. });
  648. let threw = await callWithBindings((root: any) => {
  649. try {
  650. let a: any = [];
  651. for (let i = 0; i < 999; i++) {
  652. a = [a];
  653. }
  654. root.example.doThing(a);
  655. return false;
  656. } catch {
  657. return true;
  658. }
  659. });
  660. expect(threw).to.equal(false);
  661. threw = await callWithBindings((root: any) => {
  662. try {
  663. let a: any = [];
  664. for (let i = 0; i < 1000; i++) {
  665. a = [a];
  666. }
  667. root.example.doThing(a);
  668. return false;
  669. } catch {
  670. return true;
  671. }
  672. });
  673. expect(threw).to.equal(true);
  674. });
  675. it('should copy thrown errors into the other context', async () => {
  676. await makeBindingWindow(() => {
  677. contextBridge.exposeInMainWorld('example', {
  678. throwNormal: () => {
  679. throw new Error('whoops');
  680. },
  681. throwWeird: () => {
  682. throw 'this is no error...'; // eslint-disable-line no-throw-literal
  683. },
  684. throwNotClonable: () => {
  685. return Object(Symbol('foo'));
  686. },
  687. argumentConvert: () => {}
  688. });
  689. });
  690. const result = await callWithBindings((root: any) => {
  691. const getError = (fn: Function) => {
  692. try {
  693. fn();
  694. } catch (e) {
  695. return e;
  696. }
  697. return null;
  698. };
  699. const normalIsError = Object.getPrototypeOf(getError(root.example.throwNormal)) === Error.prototype;
  700. const weirdIsError = Object.getPrototypeOf(getError(root.example.throwWeird)) === Error.prototype;
  701. const notClonableIsError = Object.getPrototypeOf(getError(root.example.throwNotClonable)) === Error.prototype;
  702. const argumentConvertIsError = Object.getPrototypeOf(getError(() => root.example.argumentConvert(Object(Symbol('test'))))) === Error.prototype;
  703. return [normalIsError, weirdIsError, notClonableIsError, argumentConvertIsError];
  704. });
  705. expect(result).to.deep.equal([true, true, true, true], 'should all be errors in the current context');
  706. });
  707. it('should not leak prototypes', async () => {
  708. await makeBindingWindow(() => {
  709. contextBridge.exposeInMainWorld('example', {
  710. number: 123,
  711. string: 'string',
  712. boolean: true,
  713. arr: [123, 'string', true, ['foo']],
  714. symbol: Symbol('foo'),
  715. bigInt: 10n,
  716. getObject: () => ({ thing: 123 }),
  717. getNumber: () => 123,
  718. getString: () => 'string',
  719. getBoolean: () => true,
  720. getArr: () => [123, 'string', true, ['foo']],
  721. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
  722. getFunctionFromFunction: async () => () => null,
  723. object: {
  724. number: 123,
  725. string: 'string',
  726. boolean: true,
  727. arr: [123, 'string', true, ['foo']],
  728. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
  729. },
  730. receiveArguments: (fn: any) => fn({ key: 'value' }),
  731. symbolKeyed: {
  732. [Symbol('foo')]: 123
  733. },
  734. getBody: () => document.body
  735. });
  736. });
  737. const result = await callWithBindings(async (root: any) => {
  738. const { example } = root;
  739. let arg: any;
  740. example.receiveArguments((o: any) => { arg = o; });
  741. const protoChecks = [
  742. ...Object.keys(example).map(key => [key, String]),
  743. ...Object.getOwnPropertySymbols(example.symbolKeyed).map(key => [key, Symbol]),
  744. [example, Object],
  745. [example.number, Number],
  746. [example.string, String],
  747. [example.boolean, Boolean],
  748. [example.arr, Array],
  749. [example.arr[0], Number],
  750. [example.arr[1], String],
  751. [example.arr[2], Boolean],
  752. [example.arr[3], Array],
  753. [example.arr[3][0], String],
  754. [example.symbol, Symbol],
  755. [example.bigInt, BigInt],
  756. [example.getNumber, Function],
  757. [example.getNumber(), Number],
  758. [example.getObject(), Object],
  759. [example.getString(), String],
  760. [example.getBoolean(), Boolean],
  761. [example.getArr(), Array],
  762. [example.getArr()[0], Number],
  763. [example.getArr()[1], String],
  764. [example.getArr()[2], Boolean],
  765. [example.getArr()[3], Array],
  766. [example.getArr()[3][0], String],
  767. [example.getFunctionFromFunction, Function],
  768. [example.getFunctionFromFunction(), Promise],
  769. [await example.getFunctionFromFunction(), Function],
  770. [example.getPromise(), Promise],
  771. [await example.getPromise(), Object],
  772. [(await example.getPromise()).number, Number],
  773. [(await example.getPromise()).string, String],
  774. [(await example.getPromise()).boolean, Boolean],
  775. [(await example.getPromise()).fn, Function],
  776. [(await example.getPromise()).fn(), String],
  777. [(await example.getPromise()).arr, Array],
  778. [(await example.getPromise()).arr[0], Number],
  779. [(await example.getPromise()).arr[1], String],
  780. [(await example.getPromise()).arr[2], Boolean],
  781. [(await example.getPromise()).arr[3], Array],
  782. [(await example.getPromise()).arr[3][0], String],
  783. [example.object, Object],
  784. [example.object.number, Number],
  785. [example.object.string, String],
  786. [example.object.boolean, Boolean],
  787. [example.object.arr, Array],
  788. [example.object.arr[0], Number],
  789. [example.object.arr[1], String],
  790. [example.object.arr[2], Boolean],
  791. [example.object.arr[3], Array],
  792. [example.object.arr[3][0], String],
  793. [await example.object.getPromise(), Object],
  794. [(await example.object.getPromise()).number, Number],
  795. [(await example.object.getPromise()).string, String],
  796. [(await example.object.getPromise()).boolean, Boolean],
  797. [(await example.object.getPromise()).fn, Function],
  798. [(await example.object.getPromise()).fn(), String],
  799. [(await example.object.getPromise()).arr, Array],
  800. [(await example.object.getPromise()).arr[0], Number],
  801. [(await example.object.getPromise()).arr[1], String],
  802. [(await example.object.getPromise()).arr[2], Boolean],
  803. [(await example.object.getPromise()).arr[3], Array],
  804. [(await example.object.getPromise()).arr[3][0], String],
  805. [arg, Object],
  806. [arg.key, String],
  807. [example.getBody(), HTMLBodyElement]
  808. ];
  809. return {
  810. protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
  811. };
  812. });
  813. // Every protomatch should be true
  814. expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
  815. });
  816. it('should not leak prototypes when attaching directly to the global', async () => {
  817. await makeBindingWindow(() => {
  818. const toExpose = {
  819. number: 123,
  820. string: 'string',
  821. boolean: true,
  822. arr: [123, 'string', true, ['foo']],
  823. symbol: Symbol('foo'),
  824. bigInt: 10n,
  825. getObject: () => ({ thing: 123 }),
  826. getNumber: () => 123,
  827. getString: () => 'string',
  828. getBoolean: () => true,
  829. getArr: () => [123, 'string', true, ['foo']],
  830. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
  831. getFunctionFromFunction: async () => () => null,
  832. getError: () => new Error('foo'),
  833. getWeirdError: () => {
  834. const e = new Error('foo');
  835. e.message = { garbage: true } as any;
  836. return e;
  837. },
  838. object: {
  839. number: 123,
  840. string: 'string',
  841. boolean: true,
  842. arr: [123, 'string', true, ['foo']],
  843. getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
  844. },
  845. receiveArguments: (fn: any) => fn({ key: 'value' }),
  846. symbolKeyed: {
  847. [Symbol('foo')]: 123
  848. }
  849. };
  850. const keys: string[] = [];
  851. Object.entries(toExpose).forEach(([key, value]) => {
  852. keys.push(key);
  853. contextBridge.exposeInMainWorld(key, value);
  854. });
  855. contextBridge.exposeInMainWorld('keys', keys);
  856. });
  857. const result = await callWithBindings(async (root: any) => {
  858. const { keys } = root;
  859. const cleanedRoot: any = {};
  860. for (const [key, value] of Object.entries(root)) {
  861. if (keys.includes(key)) {
  862. cleanedRoot[key] = value;
  863. }
  864. }
  865. let arg: any;
  866. cleanedRoot.receiveArguments((o: any) => { arg = o; });
  867. const protoChecks = [
  868. ...Object.keys(cleanedRoot).map(key => [key, String]),
  869. ...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map(key => [key, Symbol]),
  870. [cleanedRoot, Object],
  871. [cleanedRoot.number, Number],
  872. [cleanedRoot.string, String],
  873. [cleanedRoot.boolean, Boolean],
  874. [cleanedRoot.arr, Array],
  875. [cleanedRoot.arr[0], Number],
  876. [cleanedRoot.arr[1], String],
  877. [cleanedRoot.arr[2], Boolean],
  878. [cleanedRoot.arr[3], Array],
  879. [cleanedRoot.arr[3][0], String],
  880. [cleanedRoot.symbol, Symbol],
  881. [cleanedRoot.bigInt, BigInt],
  882. [cleanedRoot.getNumber, Function],
  883. [cleanedRoot.getNumber(), Number],
  884. [cleanedRoot.getObject(), Object],
  885. [cleanedRoot.getString(), String],
  886. [cleanedRoot.getBoolean(), Boolean],
  887. [cleanedRoot.getArr(), Array],
  888. [cleanedRoot.getArr()[0], Number],
  889. [cleanedRoot.getArr()[1], String],
  890. [cleanedRoot.getArr()[2], Boolean],
  891. [cleanedRoot.getArr()[3], Array],
  892. [cleanedRoot.getArr()[3][0], String],
  893. [cleanedRoot.getFunctionFromFunction, Function],
  894. [cleanedRoot.getFunctionFromFunction(), Promise],
  895. [await cleanedRoot.getFunctionFromFunction(), Function],
  896. [cleanedRoot.getError(), Error],
  897. [cleanedRoot.getError().message, String],
  898. [cleanedRoot.getWeirdError(), Error],
  899. [cleanedRoot.getWeirdError().message, String],
  900. [cleanedRoot.getPromise(), Promise],
  901. [await cleanedRoot.getPromise(), Object],
  902. [(await cleanedRoot.getPromise()).number, Number],
  903. [(await cleanedRoot.getPromise()).string, String],
  904. [(await cleanedRoot.getPromise()).boolean, Boolean],
  905. [(await cleanedRoot.getPromise()).fn, Function],
  906. [(await cleanedRoot.getPromise()).fn(), String],
  907. [(await cleanedRoot.getPromise()).arr, Array],
  908. [(await cleanedRoot.getPromise()).arr[0], Number],
  909. [(await cleanedRoot.getPromise()).arr[1], String],
  910. [(await cleanedRoot.getPromise()).arr[2], Boolean],
  911. [(await cleanedRoot.getPromise()).arr[3], Array],
  912. [(await cleanedRoot.getPromise()).arr[3][0], String],
  913. [cleanedRoot.object, Object],
  914. [cleanedRoot.object.number, Number],
  915. [cleanedRoot.object.string, String],
  916. [cleanedRoot.object.boolean, Boolean],
  917. [cleanedRoot.object.arr, Array],
  918. [cleanedRoot.object.arr[0], Number],
  919. [cleanedRoot.object.arr[1], String],
  920. [cleanedRoot.object.arr[2], Boolean],
  921. [cleanedRoot.object.arr[3], Array],
  922. [cleanedRoot.object.arr[3][0], String],
  923. [await cleanedRoot.object.getPromise(), Object],
  924. [(await cleanedRoot.object.getPromise()).number, Number],
  925. [(await cleanedRoot.object.getPromise()).string, String],
  926. [(await cleanedRoot.object.getPromise()).boolean, Boolean],
  927. [(await cleanedRoot.object.getPromise()).fn, Function],
  928. [(await cleanedRoot.object.getPromise()).fn(), String],
  929. [(await cleanedRoot.object.getPromise()).arr, Array],
  930. [(await cleanedRoot.object.getPromise()).arr[0], Number],
  931. [(await cleanedRoot.object.getPromise()).arr[1], String],
  932. [(await cleanedRoot.object.getPromise()).arr[2], Boolean],
  933. [(await cleanedRoot.object.getPromise()).arr[3], Array],
  934. [(await cleanedRoot.object.getPromise()).arr[3][0], String],
  935. [arg, Object],
  936. [arg.key, String]
  937. ];
  938. return {
  939. protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
  940. };
  941. });
  942. // Every protomatch should be true
  943. expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
  944. });
  945. describe('internalContextBridge', () => {
  946. describe('overrideGlobalValueFromIsolatedWorld', () => {
  947. it('should override top level properties', async () => {
  948. await makeBindingWindow(() => {
  949. contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld(['open'], () => ({ you: 'are a wizard' }));
  950. });
  951. const result = await callWithBindings(async (root: any) => {
  952. return root.open();
  953. });
  954. expect(result).to.deep.equal({ you: 'are a wizard' });
  955. });
  956. it('should override deep properties', async () => {
  957. await makeBindingWindow(() => {
  958. contextBridge.internalContextBridge!.overrideGlobalValueFromIsolatedWorld(['document', 'foo'], () => 'I am foo');
  959. });
  960. const result = await callWithBindings(async (root: any) => {
  961. return root.document.foo();
  962. });
  963. expect(result).to.equal('I am foo');
  964. });
  965. });
  966. describe('overrideGlobalPropertyFromIsolatedWorld', () => {
  967. it('should call the getter correctly', async () => {
  968. await makeBindingWindow(() => {
  969. let callCount = 0;
  970. const getter = () => {
  971. callCount++;
  972. return true;
  973. };
  974. contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], getter);
  975. contextBridge.exposeInMainWorld('foo', {
  976. callCount: () => callCount
  977. });
  978. });
  979. const result = await callWithBindings(async (root: any) => {
  980. return [root.isFun, root.foo.callCount()];
  981. });
  982. expect(result[0]).to.equal(true);
  983. expect(result[1]).to.equal(1);
  984. });
  985. it('should not make a setter if none is provided', async () => {
  986. await makeBindingWindow(() => {
  987. contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true);
  988. });
  989. const result = await callWithBindings(async (root: any) => {
  990. root.isFun = 123;
  991. return root.isFun;
  992. });
  993. expect(result).to.equal(true);
  994. });
  995. it('should call the setter correctly', async () => {
  996. await makeBindingWindow(() => {
  997. const callArgs: any[] = [];
  998. const setter = (...args: any[]) => {
  999. callArgs.push(args);
  1000. return true;
  1001. };
  1002. contextBridge.internalContextBridge!.overrideGlobalPropertyFromIsolatedWorld(['isFun'], () => true, setter);
  1003. contextBridge.exposeInMainWorld('foo', {
  1004. callArgs: () => callArgs
  1005. });
  1006. });
  1007. const result = await callWithBindings(async (root: any) => {
  1008. root.isFun = 123;
  1009. return root.foo.callArgs();
  1010. });
  1011. expect(result).to.have.lengthOf(1);
  1012. expect(result[0]).to.have.lengthOf(1);
  1013. expect(result[0][0]).to.equal(123);
  1014. });
  1015. });
  1016. describe('overrideGlobalValueWithDynamicPropsFromIsolatedWorld', () => {
  1017. it('should not affect normal values', async () => {
  1018. await makeBindingWindow(() => {
  1019. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1020. a: 123,
  1021. b: () => 2,
  1022. c: () => ({ d: 3 })
  1023. });
  1024. });
  1025. const result = await callWithBindings(async (root: any) => {
  1026. return [root.thing.a, root.thing.b(), root.thing.c()];
  1027. });
  1028. expect(result).to.deep.equal([123, 2, { d: 3 }]);
  1029. });
  1030. it('should work with getters', async () => {
  1031. await makeBindingWindow(() => {
  1032. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1033. get foo () {
  1034. return 'hi there';
  1035. }
  1036. });
  1037. });
  1038. const result = await callWithBindings(async (root: any) => {
  1039. return root.thing.foo;
  1040. });
  1041. expect(result).to.equal('hi there');
  1042. });
  1043. it('should work with nested getters', async () => {
  1044. await makeBindingWindow(() => {
  1045. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1046. get foo () {
  1047. return {
  1048. get bar () {
  1049. return 'hi there';
  1050. }
  1051. };
  1052. }
  1053. });
  1054. });
  1055. const result = await callWithBindings(async (root: any) => {
  1056. return root.thing.foo.bar;
  1057. });
  1058. expect(result).to.equal('hi there');
  1059. });
  1060. it('should work with setters', async () => {
  1061. await makeBindingWindow(() => {
  1062. let a: any = null;
  1063. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1064. get foo () {
  1065. return a;
  1066. },
  1067. set foo (arg: any) {
  1068. a = arg + 1;
  1069. }
  1070. });
  1071. });
  1072. const result = await callWithBindings(async (root: any) => {
  1073. root.thing.foo = 123;
  1074. return root.thing.foo;
  1075. });
  1076. expect(result).to.equal(124);
  1077. });
  1078. it('should work with nested getter / setter combos', async () => {
  1079. await makeBindingWindow(() => {
  1080. let a: any = null;
  1081. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1082. get thingy () {
  1083. return {
  1084. get foo () {
  1085. return a;
  1086. },
  1087. set foo (arg: any) {
  1088. a = arg + 1;
  1089. }
  1090. };
  1091. }
  1092. });
  1093. });
  1094. const result = await callWithBindings(async (root: any) => {
  1095. root.thing.thingy.foo = 123;
  1096. return root.thing.thingy.foo;
  1097. });
  1098. expect(result).to.equal(124);
  1099. });
  1100. it('should work with deep properties', async () => {
  1101. await makeBindingWindow(() => {
  1102. contextBridge.internalContextBridge!.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['thing'], {
  1103. a: () => ({
  1104. get foo () {
  1105. return 'still here';
  1106. }
  1107. })
  1108. });
  1109. });
  1110. const result = await callWithBindings(async (root: any) => {
  1111. return root.thing.a().foo;
  1112. });
  1113. expect(result).to.equal('still here');
  1114. });
  1115. });
  1116. });
  1117. });
  1118. };
  1119. generateTests(true);
  1120. generateTests(false);
  1121. });
  1122. describe('ContextBridgeMutability', () => {
  1123. it('should not make properties unwriteable and read-only if ContextBridgeMutability is on', async () => {
  1124. const appPath = path.join(fixturesPath, 'context-bridge-mutability');
  1125. const appProcess = cp.spawn(process.execPath, ['--enable-logging', '--enable-features=ContextBridgeMutability', appPath]);
  1126. let output = '';
  1127. appProcess.stdout.on('data', data => { output += data; });
  1128. await emittedOnce(appProcess, 'exit');
  1129. expect(output).to.include('some-modified-text');
  1130. expect(output).to.include('obj-modified-prop');
  1131. expect(output).to.include('1,2,5,3,4');
  1132. });
  1133. it('should make properties unwriteable and read-only if ContextBridgeMutability is off', async () => {
  1134. const appPath = path.join(fixturesPath, 'context-bridge-mutability');
  1135. const appProcess = cp.spawn(process.execPath, ['--enable-logging', appPath]);
  1136. let output = '';
  1137. appProcess.stdout.on('data', data => { output += data; });
  1138. await emittedOnce(appProcess, 'exit');
  1139. expect(output).to.include('some-text');
  1140. expect(output).to.include('obj-prop');
  1141. expect(output).to.include('1,2,3,4');
  1142. });
  1143. });