api-protocol-spec.ts 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070
  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 './window-helpers';
  13. import { emittedOnce } from './events-helpers';
  14. import { WebmGenerator } from './video-helpers';
  15. import { delay } from './spec-helpers';
  16. const fixturesPath = path.resolve(__dirname, '..', 'spec', '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 any).create({ sandbox: true }); });
  69. after(() => (contents as any).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 = (webContents as any).create({ nodeIntegration: true, contextIsolation: false });
  857. const consoleMessages: string[] = [];
  858. newContents.on('console-message', (e, level, message) => consoleMessages.push(message));
  859. try {
  860. newContents.loadURL(standardScheme + '://fake-host');
  861. const [, response] = await emittedOnce(ipcMain, 'response');
  862. expect(response).to.deep.equal(expected);
  863. expect(consoleMessages.join('\n')).to.match(expectedConsole);
  864. } finally {
  865. // This is called in a timeout to avoid a crash that happens when
  866. // calling destroy() in a microtask.
  867. setTimeout(() => {
  868. (newContents as any).destroy();
  869. });
  870. }
  871. }
  872. });
  873. describe('protocol.registerSchemesAsPrivileged stream', async function () {
  874. const pagePath = path.join(fixturesPath, 'pages', 'video.html');
  875. const videoSourceImagePath = path.join(fixturesPath, 'video-source-image.webp');
  876. const videoPath = path.join(fixturesPath, 'video.webm');
  877. let w: BrowserWindow = null as unknown as BrowserWindow;
  878. before(async () => {
  879. // generate test video
  880. const imageBase64 = await fs.promises.readFile(videoSourceImagePath, 'base64');
  881. const imageDataUrl = `data:image/webp;base64,${imageBase64}`;
  882. const encoder = new WebmGenerator(15);
  883. for (let i = 0; i < 30; i++) {
  884. encoder.add(imageDataUrl);
  885. }
  886. await new Promise((resolve, reject) => {
  887. encoder.compile((output:Uint8Array) => {
  888. fs.promises.writeFile(videoPath, output).then(resolve, reject);
  889. });
  890. });
  891. });
  892. after(async () => {
  893. await fs.promises.unlink(videoPath);
  894. });
  895. beforeEach(async function () {
  896. w = new BrowserWindow({ show: false });
  897. await w.loadURL('about:blank');
  898. if (!await w.webContents.executeJavaScript('document.createElement(\'video\').canPlayType(\'video/webm; codecs="vp8.0"\')')) {
  899. this.skip();
  900. }
  901. });
  902. afterEach(async () => {
  903. await closeWindow(w);
  904. w = null as unknown as BrowserWindow;
  905. await protocol.unregisterProtocol(standardScheme);
  906. await protocol.unregisterProtocol('stream');
  907. });
  908. it('successfully plays videos when content is buffered (stream: false)', async () => {
  909. await streamsResponses(standardScheme, 'play');
  910. });
  911. it('successfully plays videos when streaming content (stream: true)', async () => {
  912. await streamsResponses('stream', 'play');
  913. });
  914. async function streamsResponses (testingScheme: string, expected: any) {
  915. const protocolHandler = (request: any, callback: Function) => {
  916. if (request.url.includes('/video.webm')) {
  917. const stat = fs.statSync(videoPath);
  918. const fileSize = stat.size;
  919. const range = request.headers.Range;
  920. if (range) {
  921. const parts = range.replace(/bytes=/, '').split('-');
  922. const start = parseInt(parts[0], 10);
  923. const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
  924. const chunksize = (end - start) + 1;
  925. const headers = {
  926. 'Content-Range': `bytes ${start}-${end}/${fileSize}`,
  927. 'Accept-Ranges': 'bytes',
  928. 'Content-Length': String(chunksize),
  929. 'Content-Type': 'video/webm'
  930. };
  931. callback({ statusCode: 206, headers, data: fs.createReadStream(videoPath, { start, end }) });
  932. } else {
  933. callback({
  934. statusCode: 200,
  935. headers: {
  936. 'Content-Length': String(fileSize),
  937. 'Content-Type': 'video/webm'
  938. },
  939. data: fs.createReadStream(videoPath)
  940. });
  941. }
  942. } else {
  943. callback({ data: fs.createReadStream(pagePath), headers: { 'Content-Type': 'text/html' }, statusCode: 200 });
  944. }
  945. };
  946. await registerStreamProtocol(standardScheme, protocolHandler);
  947. await registerStreamProtocol('stream', protocolHandler);
  948. const newContents: WebContents = (webContents as any).create({ nodeIntegration: true, contextIsolation: false });
  949. try {
  950. newContents.loadURL(testingScheme + '://fake-host');
  951. const [, response] = await emittedOnce(ipcMain, 'result');
  952. expect(response).to.deep.equal(expected);
  953. } finally {
  954. // This is called in a timeout to avoid a crash that happens when
  955. // calling destroy() in a microtask.
  956. setTimeout(() => {
  957. (newContents as any).destroy();
  958. });
  959. }
  960. }
  961. });
  962. });