api-protocol-spec.ts 32 KB

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