api-protocol-spec.ts 33 KB

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