api-protocol-spec.ts 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077
  1. import { expect } from 'chai';
  2. import { v4 } from 'uuid';
  3. import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain } from 'electron/main';
  4. import { AddressInfo } from 'net';
  5. import * as ChildProcess from 'child_process';
  6. import * as path from 'path';
  7. import * as http from 'http';
  8. import * as fs from 'fs';
  9. import * as qs from 'querystring';
  10. import * as stream from 'stream';
  11. import { EventEmitter } from 'events';
  12. import { closeAllWindows, closeWindow } from './lib/window-helpers';
  13. import { emittedOnce } from './lib/events-helpers';
  14. import { WebmGenerator } from './lib/video-helpers';
  15. import { delay } from './lib/spec-helpers';
  16. const fixturesPath = path.resolve(__dirname, 'fixtures');
  17. const registerStringProtocol = protocol.registerStringProtocol;
  18. const registerBufferProtocol = protocol.registerBufferProtocol;
  19. const registerFileProtocol = protocol.registerFileProtocol;
  20. const registerHttpProtocol = protocol.registerHttpProtocol;
  21. const registerStreamProtocol = protocol.registerStreamProtocol;
  22. const interceptStringProtocol = protocol.interceptStringProtocol;
  23. const interceptBufferProtocol = protocol.interceptBufferProtocol;
  24. const interceptHttpProtocol = protocol.interceptHttpProtocol;
  25. const interceptStreamProtocol = protocol.interceptStreamProtocol;
  26. const unregisterProtocol = protocol.unregisterProtocol;
  27. const uninterceptProtocol = protocol.uninterceptProtocol;
  28. const text = 'valar morghulis';
  29. const protocolName = 'no-cors';
  30. const postData = {
  31. name: 'post test',
  32. type: 'string'
  33. };
  34. function getStream (chunkSize = text.length, data: Buffer | string = text) {
  35. const body = new stream.PassThrough();
  36. async function sendChunks () {
  37. await delay(0); // the stream protocol API breaks if you send data immediately.
  38. let buf = Buffer.from(data as any); // nodejs typings are wrong, Buffer.from can take a Buffer
  39. for (;;) {
  40. body.push(buf.slice(0, chunkSize));
  41. buf = buf.slice(chunkSize);
  42. if (!buf.length) {
  43. break;
  44. }
  45. // emulate some network delay
  46. await delay(10);
  47. }
  48. body.push(null);
  49. }
  50. sendChunks();
  51. return body;
  52. }
  53. // A promise that can be resolved externally.
  54. function defer (): Promise<any> & {resolve: Function, reject: Function} {
  55. let promiseResolve: Function = null as unknown as Function;
  56. let promiseReject: Function = null as unknown as Function;
  57. const promise: any = new Promise((resolve, reject) => {
  58. promiseResolve = resolve;
  59. promiseReject = reject;
  60. });
  61. promise.resolve = promiseResolve;
  62. promise.reject = promiseReject;
  63. return promise;
  64. }
  65. describe('protocol module', () => {
  66. let contents: WebContents = null as unknown as WebContents;
  67. // NB. sandbox: true is used because it makes navigations much (~8x) faster.
  68. before(() => { contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true }); });
  69. after(() => contents.destroy());
  70. async function ajax (url: string, options = {}) {
  71. // Note that we need to do navigation every time after a protocol is
  72. // registered or unregistered, otherwise the new protocol won't be
  73. // recognized by current page when NetworkService is used.
  74. await contents.loadFile(path.join(__dirname, 'fixtures', 'pages', 'fetch.html'));
  75. return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`);
  76. }
  77. afterEach(() => {
  78. protocol.unregisterProtocol(protocolName);
  79. protocol.uninterceptProtocol('http');
  80. });
  81. describe('protocol.register(Any)Protocol', () => {
  82. it('fails when scheme is already registered', () => {
  83. expect(registerStringProtocol(protocolName, (req, cb) => cb(''))).to.equal(true);
  84. expect(registerBufferProtocol(protocolName, (req, cb) => cb(Buffer.from('')))).to.equal(false);
  85. });
  86. it('does not crash when handler is called twice', async () => {
  87. registerStringProtocol(protocolName, (request, callback) => {
  88. try {
  89. callback(text);
  90. callback('');
  91. } catch (error) {
  92. // Ignore error
  93. }
  94. });
  95. const r = await ajax(protocolName + '://fake-host');
  96. expect(r.data).to.equal(text);
  97. });
  98. it('sends error when callback is called with nothing', async () => {
  99. registerBufferProtocol(protocolName, (req, cb: any) => cb());
  100. await expect(ajax(protocolName + '://fake-host')).to.eventually.be.rejected();
  101. });
  102. it('does not crash when callback is called in next tick', async () => {
  103. registerStringProtocol(protocolName, (request, callback) => {
  104. setImmediate(() => callback(text));
  105. });
  106. const r = await ajax(protocolName + '://fake-host');
  107. expect(r.data).to.equal(text);
  108. });
  109. it('can redirect to the same scheme', async () => {
  110. registerStringProtocol(protocolName, (request, callback) => {
  111. if (request.url === `${protocolName}://fake-host/redirect`) {
  112. callback({
  113. statusCode: 302,
  114. headers: {
  115. Location: `${protocolName}://fake-host`
  116. }
  117. });
  118. } else {
  119. expect(request.url).to.equal(`${protocolName}://fake-host`);
  120. callback('redirected');
  121. }
  122. });
  123. const r = await ajax(`${protocolName}://fake-host/redirect`);
  124. expect(r.data).to.equal('redirected');
  125. });
  126. });
  127. describe('protocol.unregisterProtocol', () => {
  128. it('returns false when scheme does not exist', () => {
  129. expect(unregisterProtocol('not-exist')).to.equal(false);
  130. });
  131. });
  132. describe('protocol.registerStringProtocol', () => {
  133. it('sends string as response', async () => {
  134. registerStringProtocol(protocolName, (request, callback) => callback(text));
  135. const r = await ajax(protocolName + '://fake-host');
  136. expect(r.data).to.equal(text);
  137. });
  138. it('sets Access-Control-Allow-Origin', async () => {
  139. registerStringProtocol(protocolName, (request, callback) => callback(text));
  140. const r = await ajax(protocolName + '://fake-host');
  141. expect(r.data).to.equal(text);
  142. expect(r.headers).to.have.property('access-control-allow-origin', '*');
  143. });
  144. it('sends object as response', async () => {
  145. registerStringProtocol(protocolName, (request, callback) => {
  146. callback({
  147. data: text,
  148. mimeType: 'text/html'
  149. });
  150. });
  151. const r = await ajax(protocolName + '://fake-host');
  152. expect(r.data).to.equal(text);
  153. });
  154. it('fails when sending object other than string', async () => {
  155. const notAString = () => {};
  156. registerStringProtocol(protocolName, (request, callback) => callback(notAString as any));
  157. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  158. });
  159. });
  160. describe('protocol.registerBufferProtocol', () => {
  161. const buffer = Buffer.from(text);
  162. it('sends Buffer as response', async () => {
  163. registerBufferProtocol(protocolName, (request, callback) => callback(buffer));
  164. const r = await ajax(protocolName + '://fake-host');
  165. expect(r.data).to.equal(text);
  166. });
  167. it('sets Access-Control-Allow-Origin', async () => {
  168. registerBufferProtocol(protocolName, (request, callback) => callback(buffer));
  169. const r = await ajax(protocolName + '://fake-host');
  170. expect(r.data).to.equal(text);
  171. expect(r.headers).to.have.property('access-control-allow-origin', '*');
  172. });
  173. it('sends object as response', async () => {
  174. registerBufferProtocol(protocolName, (request, callback) => {
  175. callback({
  176. data: buffer,
  177. mimeType: 'text/html'
  178. });
  179. });
  180. const r = await ajax(protocolName + '://fake-host');
  181. expect(r.data).to.equal(text);
  182. });
  183. it('fails when sending string', async () => {
  184. registerBufferProtocol(protocolName, (request, callback) => callback(text as any));
  185. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  186. });
  187. });
  188. describe('protocol.registerFileProtocol', () => {
  189. const filePath = path.join(fixturesPath, 'test.asar', 'a.asar', 'file1');
  190. const fileContent = fs.readFileSync(filePath);
  191. const normalPath = path.join(fixturesPath, 'pages', 'a.html');
  192. const normalContent = fs.readFileSync(normalPath);
  193. afterEach(closeAllWindows);
  194. it('sends file path as response', async () => {
  195. registerFileProtocol(protocolName, (request, callback) => callback(filePath));
  196. const r = await ajax(protocolName + '://fake-host');
  197. expect(r.data).to.equal(String(fileContent));
  198. });
  199. it('sets Access-Control-Allow-Origin', async () => {
  200. registerFileProtocol(protocolName, (request, callback) => callback(filePath));
  201. const r = await ajax(protocolName + '://fake-host');
  202. expect(r.data).to.equal(String(fileContent));
  203. expect(r.headers).to.have.property('access-control-allow-origin', '*');
  204. });
  205. it('sets custom headers', async () => {
  206. registerFileProtocol(protocolName, (request, callback) => callback({
  207. path: filePath,
  208. headers: { 'X-Great-Header': 'sogreat' }
  209. }));
  210. const r = await ajax(protocolName + '://fake-host');
  211. expect(r.data).to.equal(String(fileContent));
  212. expect(r.headers).to.have.property('x-great-header', 'sogreat');
  213. });
  214. it('can load iframes with custom protocols', (done) => {
  215. registerFileProtocol('custom', (request, callback) => {
  216. const filename = request.url.substring(9);
  217. const p = path.join(__dirname, 'fixtures', 'pages', filename);
  218. callback({ path: p });
  219. });
  220. const w = new BrowserWindow({
  221. show: false,
  222. webPreferences: {
  223. nodeIntegration: true,
  224. contextIsolation: false
  225. }
  226. });
  227. w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'iframe-protocol.html'));
  228. ipcMain.once('loaded-iframe-custom-protocol', () => done());
  229. });
  230. it.skip('throws an error when custom headers are invalid', (done) => {
  231. registerFileProtocol(protocolName, (request, callback) => {
  232. expect(() => callback({
  233. path: filePath,
  234. headers: { 'X-Great-Header': (42 as any) }
  235. })).to.throw(Error, 'Value of \'X-Great-Header\' header has to be a string');
  236. done();
  237. });
  238. ajax(protocolName + '://fake-host').catch(() => {});
  239. });
  240. it('sends object as response', async () => {
  241. registerFileProtocol(protocolName, (request, callback) => callback({ path: filePath }));
  242. const r = await ajax(protocolName + '://fake-host');
  243. expect(r.data).to.equal(String(fileContent));
  244. });
  245. it('can send normal file', async () => {
  246. registerFileProtocol(protocolName, (request, callback) => callback(normalPath));
  247. const r = await ajax(protocolName + '://fake-host');
  248. expect(r.data).to.equal(String(normalContent));
  249. });
  250. it('fails when sending unexist-file', async () => {
  251. const fakeFilePath = path.join(fixturesPath, 'test.asar', 'a.asar', 'not-exist');
  252. registerFileProtocol(protocolName, (request, callback) => callback(fakeFilePath));
  253. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  254. });
  255. it('fails when sending unsupported content', async () => {
  256. registerFileProtocol(protocolName, (request, callback) => callback(new Date() as any));
  257. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  258. });
  259. });
  260. describe('protocol.registerHttpProtocol', () => {
  261. it('sends url as response', async () => {
  262. const server = http.createServer((req, res) => {
  263. expect(req.headers.accept).to.not.equal('');
  264. res.end(text);
  265. server.close();
  266. });
  267. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  268. const port = (server.address() as AddressInfo).port;
  269. const url = 'http://127.0.0.1:' + port;
  270. registerHttpProtocol(protocolName, (request, callback) => callback({ url }));
  271. const r = await ajax(protocolName + '://fake-host');
  272. expect(r.data).to.equal(text);
  273. });
  274. it('fails when sending invalid url', async () => {
  275. registerHttpProtocol(protocolName, (request, callback) => callback({ url: 'url' }));
  276. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  277. });
  278. it('fails when sending unsupported content', async () => {
  279. registerHttpProtocol(protocolName, (request, callback) => callback(new Date() as any));
  280. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  281. });
  282. it('works when target URL redirects', async () => {
  283. const server = http.createServer((req, res) => {
  284. if (req.url === '/serverRedirect') {
  285. res.statusCode = 301;
  286. res.setHeader('Location', `http://${req.rawHeaders[1]}`);
  287. res.end();
  288. } else {
  289. res.end(text);
  290. }
  291. });
  292. after(() => server.close());
  293. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  294. const port = (server.address() as AddressInfo).port;
  295. const url = `${protocolName}://fake-host`;
  296. const redirectURL = `http://127.0.0.1:${port}/serverRedirect`;
  297. registerHttpProtocol(protocolName, (request, callback) => callback({ url: redirectURL }));
  298. const r = await ajax(url);
  299. expect(r.data).to.equal(text);
  300. });
  301. it('can access request headers', (done) => {
  302. protocol.registerHttpProtocol(protocolName, (request) => {
  303. try {
  304. expect(request).to.have.property('headers');
  305. done();
  306. } catch (e) {
  307. done(e);
  308. }
  309. });
  310. ajax(protocolName + '://fake-host').catch(() => {});
  311. });
  312. });
  313. describe('protocol.registerStreamProtocol', () => {
  314. it('sends Stream as response', async () => {
  315. registerStreamProtocol(protocolName, (request, callback) => callback(getStream()));
  316. const r = await ajax(protocolName + '://fake-host');
  317. expect(r.data).to.equal(text);
  318. });
  319. it('sends object as response', async () => {
  320. registerStreamProtocol(protocolName, (request, callback) => callback({ data: getStream() }));
  321. const r = await ajax(protocolName + '://fake-host');
  322. expect(r.data).to.equal(text);
  323. expect(r.status).to.equal(200);
  324. });
  325. it('sends custom response headers', async () => {
  326. registerStreamProtocol(protocolName, (request, callback) => callback({
  327. data: getStream(3),
  328. headers: {
  329. 'x-electron': ['a', 'b']
  330. }
  331. }));
  332. const r = await ajax(protocolName + '://fake-host');
  333. expect(r.data).to.equal(text);
  334. expect(r.status).to.equal(200);
  335. expect(r.headers).to.have.property('x-electron', 'a, b');
  336. });
  337. it('sends custom status code', async () => {
  338. registerStreamProtocol(protocolName, (request, callback) => callback({
  339. statusCode: 204,
  340. data: null as any
  341. }));
  342. const r = await ajax(protocolName + '://fake-host');
  343. expect(r.data).to.be.empty('data');
  344. expect(r.status).to.equal(204);
  345. });
  346. it('receives request headers', async () => {
  347. registerStreamProtocol(protocolName, (request, callback) => {
  348. callback({
  349. headers: {
  350. 'content-type': 'application/json'
  351. },
  352. data: getStream(5, JSON.stringify(Object.assign({}, request.headers)))
  353. });
  354. });
  355. const r = await ajax(protocolName + '://fake-host', { headers: { 'x-return-headers': 'yes' } });
  356. expect(JSON.parse(r.data)['x-return-headers']).to.equal('yes');
  357. });
  358. it('returns response multiple response headers with the same name', async () => {
  359. registerStreamProtocol(protocolName, (request, callback) => {
  360. callback({
  361. headers: {
  362. header1: ['value1', 'value2'],
  363. header2: 'value3'
  364. },
  365. data: getStream()
  366. });
  367. });
  368. const r = await ajax(protocolName + '://fake-host');
  369. // SUBTLE: when the response headers have multiple values it
  370. // separates values by ", ". When the response headers are incorrectly
  371. // converting an array to a string it separates values by ",".
  372. expect(r.headers).to.have.property('header1', 'value1, value2');
  373. expect(r.headers).to.have.property('header2', 'value3');
  374. });
  375. it('can handle large responses', async () => {
  376. const data = Buffer.alloc(128 * 1024);
  377. registerStreamProtocol(protocolName, (request, callback) => {
  378. callback(getStream(data.length, data));
  379. });
  380. const r = await ajax(protocolName + '://fake-host');
  381. expect(r.data).to.have.lengthOf(data.length);
  382. });
  383. it('can handle a stream completing while writing', async () => {
  384. function dumbPassthrough () {
  385. return new stream.Transform({
  386. async transform (chunk, encoding, cb) {
  387. cb(null, chunk);
  388. }
  389. });
  390. }
  391. registerStreamProtocol(protocolName, (request, callback) => {
  392. callback({
  393. statusCode: 200,
  394. headers: { 'Content-Type': 'text/plain' },
  395. data: getStream(1024 * 1024, Buffer.alloc(1024 * 1024 * 2)).pipe(dumbPassthrough())
  396. });
  397. });
  398. const r = await ajax(protocolName + '://fake-host');
  399. expect(r.data).to.have.lengthOf(1024 * 1024 * 2);
  400. });
  401. it('can handle next-tick scheduling during read calls', async () => {
  402. const events = new EventEmitter();
  403. function createStream () {
  404. const buffers = [
  405. Buffer.alloc(65536),
  406. Buffer.alloc(65537),
  407. Buffer.alloc(39156)
  408. ];
  409. const e = new stream.Readable({ highWaterMark: 0 });
  410. e.push(buffers.shift());
  411. e._read = function () {
  412. process.nextTick(() => this.push(buffers.shift() || null));
  413. };
  414. e.on('end', function () {
  415. events.emit('end');
  416. });
  417. return e;
  418. }
  419. registerStreamProtocol(protocolName, (request, callback) => {
  420. callback({
  421. statusCode: 200,
  422. headers: { 'Content-Type': 'text/plain' },
  423. data: createStream()
  424. });
  425. });
  426. const hasEndedPromise = emittedOnce(events, 'end');
  427. ajax(protocolName + '://fake-host').catch(() => {});
  428. await hasEndedPromise;
  429. });
  430. it('destroys response streams when aborted before completion', async () => {
  431. const events = new EventEmitter();
  432. registerStreamProtocol(protocolName, (request, callback) => {
  433. const responseStream = new stream.PassThrough();
  434. responseStream.push('data\r\n');
  435. responseStream.on('close', () => {
  436. events.emit('close');
  437. });
  438. callback({
  439. statusCode: 200,
  440. headers: { 'Content-Type': 'text/plain' },
  441. data: responseStream
  442. });
  443. events.emit('respond');
  444. });
  445. const hasRespondedPromise = emittedOnce(events, 'respond');
  446. const hasClosedPromise = emittedOnce(events, 'close');
  447. ajax(protocolName + '://fake-host').catch(() => {});
  448. await hasRespondedPromise;
  449. await contents.loadFile(path.join(__dirname, 'fixtures', 'pages', 'fetch.html'));
  450. await hasClosedPromise;
  451. });
  452. });
  453. describe('protocol.isProtocolRegistered', () => {
  454. it('returns false when scheme is not registered', () => {
  455. const result = protocol.isProtocolRegistered('no-exist');
  456. expect(result).to.be.false('no-exist: is handled');
  457. });
  458. it('returns true for custom protocol', () => {
  459. registerStringProtocol(protocolName, (request, callback) => callback(''));
  460. const result = protocol.isProtocolRegistered(protocolName);
  461. expect(result).to.be.true('custom protocol is handled');
  462. });
  463. });
  464. describe('protocol.isProtocolIntercepted', () => {
  465. it('returns true for intercepted protocol', () => {
  466. interceptStringProtocol('http', (request, callback) => callback(''));
  467. const result = protocol.isProtocolIntercepted('http');
  468. expect(result).to.be.true('intercepted protocol is handled');
  469. });
  470. });
  471. describe('protocol.intercept(Any)Protocol', () => {
  472. it('returns false when scheme is already intercepted', () => {
  473. expect(protocol.interceptStringProtocol('http', (request, callback) => callback(''))).to.equal(true);
  474. expect(protocol.interceptBufferProtocol('http', (request, callback) => callback(Buffer.from('')))).to.equal(false);
  475. });
  476. it('does not crash when handler is called twice', async () => {
  477. interceptStringProtocol('http', (request, callback) => {
  478. try {
  479. callback(text);
  480. callback('');
  481. } catch (error) {
  482. // Ignore error
  483. }
  484. });
  485. const r = await ajax('http://fake-host');
  486. expect(r.data).to.be.equal(text);
  487. });
  488. it('sends error when callback is called with nothing', async () => {
  489. interceptStringProtocol('http', (request, callback: any) => callback());
  490. await expect(ajax('http://fake-host')).to.be.eventually.rejected();
  491. });
  492. });
  493. describe('protocol.interceptStringProtocol', () => {
  494. it('can intercept http protocol', async () => {
  495. interceptStringProtocol('http', (request, callback) => callback(text));
  496. const r = await ajax('http://fake-host');
  497. expect(r.data).to.equal(text);
  498. });
  499. it('can set content-type', async () => {
  500. interceptStringProtocol('http', (request, callback) => {
  501. callback({
  502. mimeType: 'application/json',
  503. data: '{"value": 1}'
  504. });
  505. });
  506. const r = await ajax('http://fake-host');
  507. expect(JSON.parse(r.data)).to.have.property('value').that.is.equal(1);
  508. });
  509. it('can set content-type with charset', async () => {
  510. interceptStringProtocol('http', (request, callback) => {
  511. callback({
  512. mimeType: 'application/json; charset=UTF-8',
  513. data: '{"value": 1}'
  514. });
  515. });
  516. const r = await ajax('http://fake-host');
  517. expect(JSON.parse(r.data)).to.have.property('value').that.is.equal(1);
  518. });
  519. it('can receive post data', async () => {
  520. interceptStringProtocol('http', (request, callback) => {
  521. const uploadData = request.uploadData![0].bytes.toString();
  522. callback({ data: uploadData });
  523. });
  524. const r = await ajax('http://fake-host', { method: 'POST', body: qs.stringify(postData) });
  525. expect({ ...qs.parse(r.data) }).to.deep.equal(postData);
  526. });
  527. });
  528. describe('protocol.interceptBufferProtocol', () => {
  529. it('can intercept http protocol', async () => {
  530. interceptBufferProtocol('http', (request, callback) => callback(Buffer.from(text)));
  531. const r = await ajax('http://fake-host');
  532. expect(r.data).to.equal(text);
  533. });
  534. it('can receive post data', async () => {
  535. interceptBufferProtocol('http', (request, callback) => {
  536. const uploadData = request.uploadData![0].bytes;
  537. callback(uploadData);
  538. });
  539. const r = await ajax('http://fake-host', { method: 'POST', body: qs.stringify(postData) });
  540. expect(qs.parse(r.data)).to.deep.equal({ name: 'post test', type: 'string' });
  541. });
  542. });
  543. describe('protocol.interceptHttpProtocol', () => {
  544. // FIXME(zcbenz): This test was passing because the test itself was wrong,
  545. // I don't know whether it ever passed before and we should take a look at
  546. // it in future.
  547. xit('can send POST request', async () => {
  548. const server = http.createServer((req, res) => {
  549. let body = '';
  550. req.on('data', (chunk) => {
  551. body += chunk;
  552. });
  553. req.on('end', () => {
  554. res.end(body);
  555. });
  556. server.close();
  557. });
  558. after(() => server.close());
  559. server.listen(0, '127.0.0.1');
  560. const port = (server.address() as AddressInfo).port;
  561. const url = `http://127.0.0.1:${port}`;
  562. interceptHttpProtocol('http', (request, callback) => {
  563. const data: Electron.ProtocolResponse = {
  564. url: url,
  565. method: 'POST',
  566. uploadData: {
  567. contentType: 'application/x-www-form-urlencoded',
  568. data: request.uploadData![0].bytes
  569. },
  570. session: undefined
  571. };
  572. callback(data);
  573. });
  574. const r = await ajax('http://fake-host', { type: 'POST', data: postData });
  575. expect({ ...qs.parse(r.data) }).to.deep.equal(postData);
  576. });
  577. it('can use custom session', async () => {
  578. const customSession = session.fromPartition('custom-ses', { cache: false });
  579. customSession.webRequest.onBeforeRequest((details, callback) => {
  580. expect(details.url).to.equal('http://fake-host/');
  581. callback({ cancel: true });
  582. });
  583. after(() => customSession.webRequest.onBeforeRequest(null));
  584. interceptHttpProtocol('http', (request, callback) => {
  585. callback({
  586. url: request.url,
  587. session: customSession
  588. });
  589. });
  590. await expect(ajax('http://fake-host')).to.be.eventually.rejectedWith(Error);
  591. });
  592. it('can access request headers', (done) => {
  593. protocol.interceptHttpProtocol('http', (request) => {
  594. try {
  595. expect(request).to.have.property('headers');
  596. done();
  597. } catch (e) {
  598. done(e);
  599. }
  600. });
  601. ajax('http://fake-host').catch(() => {});
  602. });
  603. });
  604. describe('protocol.interceptStreamProtocol', () => {
  605. it('can intercept http protocol', async () => {
  606. interceptStreamProtocol('http', (request, callback) => callback(getStream()));
  607. const r = await ajax('http://fake-host');
  608. expect(r.data).to.equal(text);
  609. });
  610. it('can receive post data', async () => {
  611. interceptStreamProtocol('http', (request, callback) => {
  612. callback(getStream(3, request.uploadData![0].bytes.toString()));
  613. });
  614. const r = await ajax('http://fake-host', { method: 'POST', body: qs.stringify(postData) });
  615. expect({ ...qs.parse(r.data) }).to.deep.equal(postData);
  616. });
  617. it('can execute redirects', async () => {
  618. interceptStreamProtocol('http', (request, callback) => {
  619. if (request.url.indexOf('http://fake-host') === 0) {
  620. setTimeout(() => {
  621. callback({
  622. data: '',
  623. statusCode: 302,
  624. headers: {
  625. Location: 'http://fake-redirect'
  626. }
  627. });
  628. }, 300);
  629. } else {
  630. expect(request.url.indexOf('http://fake-redirect')).to.equal(0);
  631. callback(getStream(1, 'redirect'));
  632. }
  633. });
  634. const r = await ajax('http://fake-host');
  635. expect(r.data).to.equal('redirect');
  636. });
  637. it('should discard post data after redirection', async () => {
  638. interceptStreamProtocol('http', (request, callback) => {
  639. if (request.url.indexOf('http://fake-host') === 0) {
  640. setTimeout(() => {
  641. callback({
  642. statusCode: 302,
  643. headers: {
  644. Location: 'http://fake-redirect'
  645. }
  646. });
  647. }, 300);
  648. } else {
  649. expect(request.url.indexOf('http://fake-redirect')).to.equal(0);
  650. callback(getStream(3, request.method));
  651. }
  652. });
  653. const r = await ajax('http://fake-host', { type: 'POST', data: postData });
  654. expect(r.data).to.equal('GET');
  655. });
  656. });
  657. describe('protocol.uninterceptProtocol', () => {
  658. it('returns false when scheme does not exist', () => {
  659. expect(uninterceptProtocol('not-exist')).to.equal(false);
  660. });
  661. it('returns false when scheme is not intercepted', () => {
  662. expect(uninterceptProtocol('http')).to.equal(false);
  663. });
  664. });
  665. describe('protocol.registerSchemeAsPrivileged', () => {
  666. it('does not crash on exit', async () => {
  667. const appPath = path.join(__dirname, 'fixtures', 'api', 'custom-protocol-shutdown.js');
  668. const appProcess = ChildProcess.spawn(process.execPath, ['--enable-logging', appPath]);
  669. let stdout = '';
  670. let stderr = '';
  671. appProcess.stdout.on('data', data => { process.stdout.write(data); stdout += data; });
  672. appProcess.stderr.on('data', data => { process.stderr.write(data); stderr += data; });
  673. const [code] = await emittedOnce(appProcess, 'exit');
  674. if (code !== 0) {
  675. console.log('Exit code : ', code);
  676. console.log('stdout : ', stdout);
  677. console.log('stderr : ', stderr);
  678. }
  679. expect(code).to.equal(0);
  680. expect(stdout).to.not.contain('VALIDATION_ERROR_DESERIALIZATION_FAILED');
  681. expect(stderr).to.not.contain('VALIDATION_ERROR_DESERIALIZATION_FAILED');
  682. });
  683. });
  684. describe('protocol.registerSchemesAsPrivileged allowServiceWorkers', () => {
  685. protocol.registerStringProtocol(serviceWorkerScheme, (request, cb) => {
  686. if (request.url.endsWith('.js')) {
  687. cb({
  688. mimeType: 'text/javascript',
  689. charset: 'utf-8',
  690. data: 'console.log("Loaded")'
  691. });
  692. } else {
  693. cb({
  694. mimeType: 'text/html',
  695. charset: 'utf-8',
  696. data: '<!DOCTYPE html>'
  697. });
  698. }
  699. });
  700. after(() => protocol.unregisterProtocol(serviceWorkerScheme));
  701. it('should fail when registering invalid service worker', async () => {
  702. await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
  703. await expect(contents.executeJavaScript(`navigator.serviceWorker.register('${v4()}.notjs', {scope: './'})`)).to.be.rejected();
  704. });
  705. it('should be able to register service worker for custom scheme', async () => {
  706. await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
  707. await contents.executeJavaScript(`navigator.serviceWorker.register('${v4()}.js', {scope: './'})`);
  708. });
  709. });
  710. describe('protocol.registerSchemesAsPrivileged standard', () => {
  711. const origin = `${standardScheme}://fake-host`;
  712. const imageURL = `${origin}/test.png`;
  713. const filePath = path.join(fixturesPath, 'pages', 'b.html');
  714. const fileContent = '<img src="/test.png" />';
  715. let w: BrowserWindow = null as unknown as BrowserWindow;
  716. beforeEach(() => {
  717. w = new BrowserWindow({
  718. show: false,
  719. webPreferences: {
  720. nodeIntegration: true,
  721. contextIsolation: false
  722. }
  723. });
  724. });
  725. afterEach(async () => {
  726. await closeWindow(w);
  727. unregisterProtocol(standardScheme);
  728. w = null as unknown as BrowserWindow;
  729. });
  730. it('resolves relative resources', async () => {
  731. registerFileProtocol(standardScheme, (request, callback) => {
  732. if (request.url === imageURL) {
  733. callback('');
  734. } else {
  735. callback(filePath);
  736. }
  737. });
  738. await w.loadURL(origin);
  739. });
  740. it('resolves absolute resources', async () => {
  741. registerStringProtocol(standardScheme, (request, callback) => {
  742. if (request.url === imageURL) {
  743. callback('');
  744. } else {
  745. callback({
  746. data: fileContent,
  747. mimeType: 'text/html'
  748. });
  749. }
  750. });
  751. await w.loadURL(origin);
  752. });
  753. it('can have fetch working in it', async () => {
  754. const requestReceived = defer();
  755. const server = http.createServer((req, res) => {
  756. res.end();
  757. server.close();
  758. requestReceived.resolve();
  759. });
  760. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
  761. const port = (server.address() as AddressInfo).port;
  762. const content = `<script>fetch("http://127.0.0.1:${port}")</script>`;
  763. registerStringProtocol(standardScheme, (request, callback) => callback({ data: content, mimeType: 'text/html' }));
  764. await w.loadURL(origin);
  765. await requestReceived;
  766. });
  767. it.skip('can access files through the FileSystem API', (done) => {
  768. const filePath = path.join(fixturesPath, 'pages', 'filesystem.html');
  769. protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
  770. w.loadURL(origin);
  771. ipcMain.once('file-system-error', (event, err) => done(err));
  772. ipcMain.once('file-system-write-end', () => done());
  773. });
  774. it('registers secure, when {secure: true}', (done) => {
  775. const filePath = path.join(fixturesPath, 'pages', 'cache-storage.html');
  776. ipcMain.once('success', () => done());
  777. ipcMain.once('failure', (event, err) => done(err));
  778. protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
  779. w.loadURL(origin);
  780. });
  781. });
  782. describe('protocol.registerSchemesAsPrivileged cors-fetch', function () {
  783. let w: BrowserWindow = null as unknown as BrowserWindow;
  784. beforeEach(async () => {
  785. w = new BrowserWindow({ show: false });
  786. });
  787. afterEach(async () => {
  788. await closeWindow(w);
  789. w = null as unknown as BrowserWindow;
  790. for (const scheme of [standardScheme, 'cors', 'no-cors', 'no-fetch']) {
  791. protocol.unregisterProtocol(scheme);
  792. }
  793. });
  794. it('supports fetch api by default', async () => {
  795. const url = `file://${fixturesPath}/assets/logo.png`;
  796. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  797. const ok = await w.webContents.executeJavaScript(`fetch(${JSON.stringify(url)}).then(r => r.ok)`);
  798. expect(ok).to.be.true('response ok');
  799. });
  800. it('allows CORS requests by default', async () => {
  801. await allowsCORSRequests('cors', 200, new RegExp(''), () => {
  802. const { ipcRenderer } = require('electron');
  803. fetch('cors://myhost').then(function (response) {
  804. ipcRenderer.send('response', response.status);
  805. }).catch(function () {
  806. ipcRenderer.send('response', 'failed');
  807. });
  808. });
  809. });
  810. // FIXME: Figure out why this test is failing
  811. it.skip('disallows CORS and fetch requests when only supportFetchAPI is specified', async () => {
  812. await allowsCORSRequests('no-cors', ['failed xhr', 'failed fetch'], /has been blocked by CORS policy/, () => {
  813. const { ipcRenderer } = require('electron');
  814. Promise.all([
  815. new Promise(resolve => {
  816. const req = new XMLHttpRequest();
  817. req.onload = () => resolve('loaded xhr');
  818. req.onerror = () => resolve('failed xhr');
  819. req.open('GET', 'no-cors://myhost');
  820. req.send();
  821. }),
  822. fetch('no-cors://myhost')
  823. .then(() => 'loaded fetch')
  824. .catch(() => 'failed fetch')
  825. ]).then(([xhr, fetch]) => {
  826. ipcRenderer.send('response', [xhr, fetch]);
  827. });
  828. });
  829. });
  830. it('allows CORS, but disallows fetch requests, when specified', async () => {
  831. await allowsCORSRequests('no-fetch', ['loaded xhr', 'failed fetch'], /Fetch API cannot load/, () => {
  832. const { ipcRenderer } = require('electron');
  833. Promise.all([
  834. new Promise(resolve => {
  835. const req = new XMLHttpRequest();
  836. req.onload = () => resolve('loaded xhr');
  837. req.onerror = () => resolve('failed xhr');
  838. req.open('GET', 'no-fetch://myhost');
  839. req.send();
  840. }),
  841. fetch('no-fetch://myhost')
  842. .then(() => 'loaded fetch')
  843. .catch(() => 'failed fetch')
  844. ]).then(([xhr, fetch]) => {
  845. ipcRenderer.send('response', [xhr, fetch]);
  846. });
  847. });
  848. });
  849. async function allowsCORSRequests (corsScheme: string, expected: any, expectedConsole: RegExp, content: Function) {
  850. registerStringProtocol(standardScheme, (request, callback) => {
  851. callback({ data: `<script>(${content})()</script>`, mimeType: 'text/html' });
  852. });
  853. registerStringProtocol(corsScheme, (request, callback) => {
  854. callback('');
  855. });
  856. const newContents = (webContents as typeof ElectronInternal.WebContents).create({
  857. nodeIntegration: true,
  858. contextIsolation: false
  859. });
  860. const consoleMessages: string[] = [];
  861. newContents.on('console-message', (e, level, message) => consoleMessages.push(message));
  862. try {
  863. newContents.loadURL(standardScheme + '://fake-host');
  864. const [, response] = await emittedOnce(ipcMain, 'response');
  865. expect(response).to.deep.equal(expected);
  866. expect(consoleMessages.join('\n')).to.match(expectedConsole);
  867. } finally {
  868. // This is called in a timeout to avoid a crash that happens when
  869. // calling destroy() in a microtask.
  870. setTimeout(() => {
  871. newContents.destroy();
  872. });
  873. }
  874. }
  875. });
  876. describe('protocol.registerSchemesAsPrivileged stream', async function () {
  877. const pagePath = path.join(fixturesPath, 'pages', 'video.html');
  878. const videoSourceImagePath = path.join(fixturesPath, 'video-source-image.webp');
  879. const videoPath = path.join(fixturesPath, 'video.webm');
  880. let w: BrowserWindow = null as unknown as BrowserWindow;
  881. before(async () => {
  882. // generate test video
  883. const imageBase64 = await fs.promises.readFile(videoSourceImagePath, 'base64');
  884. const imageDataUrl = `data:image/webp;base64,${imageBase64}`;
  885. const encoder = new WebmGenerator(15);
  886. for (let i = 0; i < 30; i++) {
  887. encoder.add(imageDataUrl);
  888. }
  889. await new Promise((resolve, reject) => {
  890. encoder.compile((output:Uint8Array) => {
  891. fs.promises.writeFile(videoPath, output).then(resolve, reject);
  892. });
  893. });
  894. });
  895. after(async () => {
  896. await fs.promises.unlink(videoPath);
  897. });
  898. beforeEach(async function () {
  899. w = new BrowserWindow({ show: false });
  900. await w.loadURL('about:blank');
  901. if (!await w.webContents.executeJavaScript('document.createElement(\'video\').canPlayType(\'video/webm; codecs="vp8.0"\')')) {
  902. this.skip();
  903. }
  904. });
  905. afterEach(async () => {
  906. await closeWindow(w);
  907. w = null as unknown as BrowserWindow;
  908. await protocol.unregisterProtocol(standardScheme);
  909. await protocol.unregisterProtocol('stream');
  910. });
  911. it('successfully plays videos when content is buffered (stream: false)', async () => {
  912. await streamsResponses(standardScheme, 'play');
  913. });
  914. it('successfully plays videos when streaming content (stream: true)', async () => {
  915. await streamsResponses('stream', 'play');
  916. });
  917. async function streamsResponses (testingScheme: string, expected: any) {
  918. const protocolHandler = (request: any, callback: Function) => {
  919. if (request.url.includes('/video.webm')) {
  920. const stat = fs.statSync(videoPath);
  921. const fileSize = stat.size;
  922. const range = request.headers.Range;
  923. if (range) {
  924. const parts = range.replace(/bytes=/, '').split('-');
  925. const start = parseInt(parts[0], 10);
  926. const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
  927. const chunksize = (end - start) + 1;
  928. const headers = {
  929. 'Content-Range': `bytes ${start}-${end}/${fileSize}`,
  930. 'Accept-Ranges': 'bytes',
  931. 'Content-Length': String(chunksize),
  932. 'Content-Type': 'video/webm'
  933. };
  934. callback({ statusCode: 206, headers, data: fs.createReadStream(videoPath, { start, end }) });
  935. } else {
  936. callback({
  937. statusCode: 200,
  938. headers: {
  939. 'Content-Length': String(fileSize),
  940. 'Content-Type': 'video/webm'
  941. },
  942. data: fs.createReadStream(videoPath)
  943. });
  944. }
  945. } else {
  946. callback({ data: fs.createReadStream(pagePath), headers: { 'Content-Type': 'text/html' }, statusCode: 200 });
  947. }
  948. };
  949. await registerStreamProtocol(standardScheme, protocolHandler);
  950. await registerStreamProtocol('stream', protocolHandler);
  951. const newContents = (webContents as typeof ElectronInternal.WebContents).create({
  952. nodeIntegration: true,
  953. contextIsolation: false
  954. });
  955. try {
  956. newContents.loadURL(testingScheme + '://fake-host');
  957. const [, response] = await emittedOnce(ipcMain, 'result');
  958. expect(response).to.deep.equal(expected);
  959. } finally {
  960. // This is called in a timeout to avoid a crash that happens when
  961. // calling destroy() in a microtask.
  962. setTimeout(() => {
  963. newContents.destroy();
  964. });
  965. }
  966. }
  967. });
  968. });