api-protocol-spec.ts 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566
  1. import { expect } from 'chai';
  2. import { v4 } from 'uuid';
  3. import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain, net } from 'electron/main';
  4. import * as ChildProcess from 'child_process';
  5. import * as path from 'path';
  6. import * as url from 'url';
  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, once } from 'events';
  12. import { closeAllWindows, closeWindow } from './lib/window-helpers';
  13. import { WebmGenerator } from './lib/video-helpers';
  14. import { listen, defer, ifit } from './lib/spec-helpers';
  15. import { setTimeout } from 'timers/promises';
  16. const fixturesPath = path.resolve(__dirname, 'fixtures');
  17. const registerStringProtocol = protocol.registerStringProtocol;
  18. const registerBufferProtocol = protocol.registerBufferProtocol;
  19. const registerFileProtocol = protocol.registerFileProtocol;
  20. const registerStreamProtocol = protocol.registerStreamProtocol;
  21. const interceptStringProtocol = protocol.interceptStringProtocol;
  22. const interceptBufferProtocol = protocol.interceptBufferProtocol;
  23. const interceptHttpProtocol = protocol.interceptHttpProtocol;
  24. const interceptStreamProtocol = protocol.interceptStreamProtocol;
  25. const unregisterProtocol = protocol.unregisterProtocol;
  26. const uninterceptProtocol = protocol.uninterceptProtocol;
  27. const text = 'valar morghulis';
  28. const protocolName = 'no-cors';
  29. const postData = {
  30. name: 'post test',
  31. type: 'string'
  32. };
  33. function getStream (chunkSize = text.length, data: Buffer | string = text) {
  34. // allowHalfOpen required, otherwise Readable.toWeb gets confused and thinks
  35. // the stream isn't done when the readable half ends.
  36. const body = new stream.PassThrough({ allowHalfOpen: false });
  37. async function sendChunks () {
  38. await setTimeout(0); // the stream protocol API breaks if you send data immediately.
  39. let buf = Buffer.from(data as any); // nodejs typings are wrong, Buffer.from can take a Buffer
  40. for (;;) {
  41. body.push(buf.slice(0, chunkSize));
  42. buf = buf.slice(chunkSize);
  43. if (!buf.length) {
  44. break;
  45. }
  46. // emulate some network delay
  47. await setTimeout(10);
  48. }
  49. body.push(null);
  50. }
  51. sendChunks();
  52. return body;
  53. }
  54. function getWebStream (chunkSize = text.length, data: Buffer | string = text): ReadableStream<ArrayBufferView> {
  55. return stream.Readable.toWeb(getStream(chunkSize, data)) as ReadableStream<ArrayBufferView>;
  56. }
  57. // A promise that can be resolved externally.
  58. function deferPromise (): Promise<any> & {resolve: Function, reject: Function} {
  59. let promiseResolve: Function = null as unknown as Function;
  60. let promiseReject: Function = null as unknown as Function;
  61. const promise: any = new Promise((resolve, reject) => {
  62. promiseResolve = resolve;
  63. promiseReject = reject;
  64. });
  65. promise.resolve = promiseResolve;
  66. promise.reject = promiseReject;
  67. return promise;
  68. }
  69. describe('protocol module', () => {
  70. let contents: WebContents;
  71. // NB. sandbox: true is used because it makes navigations much (~8x) faster.
  72. before(() => { contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true }); });
  73. after(() => contents.destroy());
  74. async function ajax (url: string, options = {}) {
  75. // Note that we need to do navigation every time after a protocol is
  76. // registered or unregistered, otherwise the new protocol won't be
  77. // recognized by current page when NetworkService is used.
  78. await contents.loadFile(path.join(__dirname, 'fixtures', 'pages', 'fetch.html'));
  79. return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`);
  80. }
  81. afterEach(() => {
  82. protocol.unregisterProtocol(protocolName);
  83. protocol.uninterceptProtocol('http');
  84. });
  85. describe('protocol.register(Any)Protocol', () => {
  86. it('fails when scheme is already registered', () => {
  87. expect(registerStringProtocol(protocolName, (req, cb) => cb(''))).to.equal(true);
  88. expect(registerBufferProtocol(protocolName, (req, cb) => cb(Buffer.from('')))).to.equal(false);
  89. });
  90. it('does not crash when handler is called twice', async () => {
  91. registerStringProtocol(protocolName, (request, callback) => {
  92. try {
  93. callback(text);
  94. callback('');
  95. } catch (error) {
  96. // Ignore error
  97. }
  98. });
  99. const r = await ajax(protocolName + '://fake-host');
  100. expect(r.data).to.equal(text);
  101. });
  102. it('sends error when callback is called with nothing', async () => {
  103. registerBufferProtocol(protocolName, (req, cb: any) => cb());
  104. await expect(ajax(protocolName + '://fake-host')).to.eventually.be.rejected();
  105. });
  106. it('does not crash when callback is called in next tick', async () => {
  107. registerStringProtocol(protocolName, (request, callback) => {
  108. setImmediate(() => callback(text));
  109. });
  110. const r = await ajax(protocolName + '://fake-host');
  111. expect(r.data).to.equal(text);
  112. });
  113. it('can redirect to the same scheme', async () => {
  114. registerStringProtocol(protocolName, (request, callback) => {
  115. if (request.url === `${protocolName}://fake-host/redirect`) {
  116. callback({
  117. statusCode: 302,
  118. headers: {
  119. Location: `${protocolName}://fake-host`
  120. }
  121. });
  122. } else {
  123. expect(request.url).to.equal(`${protocolName}://fake-host`);
  124. callback('redirected');
  125. }
  126. });
  127. const r = await ajax(`${protocolName}://fake-host/redirect`);
  128. expect(r.data).to.equal('redirected');
  129. });
  130. });
  131. describe('protocol.unregisterProtocol', () => {
  132. it('returns false when scheme does not exist', () => {
  133. expect(unregisterProtocol('not-exist')).to.equal(false);
  134. });
  135. });
  136. for (const [registerStringProtocol, name] of [
  137. [protocol.registerStringProtocol, 'protocol.registerStringProtocol'] as const,
  138. [(protocol as any).registerProtocol as typeof protocol.registerStringProtocol, 'protocol.registerProtocol'] as const
  139. ]) {
  140. describe(name, () => {
  141. it('sends string as response', async () => {
  142. registerStringProtocol(protocolName, (request, callback) => callback(text));
  143. const r = await ajax(protocolName + '://fake-host');
  144. expect(r.data).to.equal(text);
  145. });
  146. it('sets Access-Control-Allow-Origin', async () => {
  147. registerStringProtocol(protocolName, (request, callback) => callback(text));
  148. const r = await ajax(protocolName + '://fake-host');
  149. expect(r.data).to.equal(text);
  150. expect(r.headers).to.have.property('access-control-allow-origin', '*');
  151. });
  152. it('sends object as response', async () => {
  153. registerStringProtocol(protocolName, (request, callback) => {
  154. callback({
  155. data: text,
  156. mimeType: 'text/html'
  157. });
  158. });
  159. const r = await ajax(protocolName + '://fake-host');
  160. expect(r.data).to.equal(text);
  161. });
  162. it('fails when sending object other than string', async () => {
  163. const notAString = () => {};
  164. registerStringProtocol(protocolName, (request, callback) => callback(notAString as any));
  165. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  166. });
  167. });
  168. }
  169. for (const [registerBufferProtocol, name] of [
  170. [protocol.registerBufferProtocol, 'protocol.registerBufferProtocol'] as const,
  171. [(protocol as any).registerProtocol as typeof protocol.registerBufferProtocol, 'protocol.registerProtocol'] as const
  172. ]) {
  173. describe(name, () => {
  174. const buffer = Buffer.from(text);
  175. it('sends Buffer as response', async () => {
  176. registerBufferProtocol(protocolName, (request, callback) => callback(buffer));
  177. const r = await ajax(protocolName + '://fake-host');
  178. expect(r.data).to.equal(text);
  179. });
  180. it('sets Access-Control-Allow-Origin', async () => {
  181. registerBufferProtocol(protocolName, (request, callback) => callback(buffer));
  182. const r = await ajax(protocolName + '://fake-host');
  183. expect(r.data).to.equal(text);
  184. expect(r.headers).to.have.property('access-control-allow-origin', '*');
  185. });
  186. it('sends object as response', async () => {
  187. registerBufferProtocol(protocolName, (request, callback) => {
  188. callback({
  189. data: buffer,
  190. mimeType: 'text/html'
  191. });
  192. });
  193. const r = await ajax(protocolName + '://fake-host');
  194. expect(r.data).to.equal(text);
  195. });
  196. if (name !== 'protocol.registerProtocol') {
  197. it('fails when sending string', async () => {
  198. registerBufferProtocol(protocolName, (request, callback) => callback(text as any));
  199. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  200. });
  201. }
  202. });
  203. }
  204. for (const [registerFileProtocol, name] of [
  205. [protocol.registerFileProtocol, 'protocol.registerFileProtocol'] as const,
  206. [(protocol as any).registerProtocol as typeof protocol.registerFileProtocol, 'protocol.registerProtocol'] as const
  207. ]) {
  208. describe(name, () => {
  209. const filePath = path.join(fixturesPath, 'test.asar', 'a.asar', 'file1');
  210. const fileContent = fs.readFileSync(filePath);
  211. const normalPath = path.join(fixturesPath, 'pages', 'a.html');
  212. const normalContent = fs.readFileSync(normalPath);
  213. afterEach(closeAllWindows);
  214. if (name === 'protocol.registerFileProtocol') {
  215. it('sends file path as response', async () => {
  216. registerFileProtocol(protocolName, (request, callback) => callback(filePath));
  217. const r = await ajax(protocolName + '://fake-host');
  218. expect(r.data).to.equal(String(fileContent));
  219. });
  220. }
  221. it('sets Access-Control-Allow-Origin', async () => {
  222. registerFileProtocol(protocolName, (request, callback) => callback({ path: filePath }));
  223. const r = await ajax(protocolName + '://fake-host');
  224. expect(r.data).to.equal(String(fileContent));
  225. expect(r.headers).to.have.property('access-control-allow-origin', '*');
  226. });
  227. it('sets custom headers', async () => {
  228. registerFileProtocol(protocolName, (request, callback) => callback({
  229. path: filePath,
  230. headers: { 'X-Great-Header': 'sogreat' }
  231. }));
  232. const r = await ajax(protocolName + '://fake-host');
  233. expect(r.data).to.equal(String(fileContent));
  234. expect(r.headers).to.have.property('x-great-header', 'sogreat');
  235. });
  236. it('can load iframes with custom protocols', (done) => {
  237. registerFileProtocol('custom', (request, callback) => {
  238. const filename = request.url.substring(9);
  239. const p = path.join(__dirname, 'fixtures', 'pages', filename);
  240. callback({ path: p });
  241. });
  242. const w = new BrowserWindow({
  243. show: false,
  244. webPreferences: {
  245. nodeIntegration: true,
  246. contextIsolation: false
  247. }
  248. });
  249. w.loadFile(path.join(__dirname, 'fixtures', 'pages', 'iframe-protocol.html'));
  250. ipcMain.once('loaded-iframe-custom-protocol', () => done());
  251. });
  252. it('sends object as response', async () => {
  253. registerFileProtocol(protocolName, (request, callback) => callback({ path: filePath }));
  254. const r = await ajax(protocolName + '://fake-host');
  255. expect(r.data).to.equal(String(fileContent));
  256. });
  257. it('can send normal file', async () => {
  258. registerFileProtocol(protocolName, (request, callback) => callback({ path: normalPath }));
  259. const r = await ajax(protocolName + '://fake-host');
  260. expect(r.data).to.equal(String(normalContent));
  261. });
  262. it('fails when sending unexist-file', async () => {
  263. const fakeFilePath = path.join(fixturesPath, 'test.asar', 'a.asar', 'not-exist');
  264. registerFileProtocol(protocolName, (request, callback) => callback({ path: fakeFilePath }));
  265. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  266. });
  267. it('fails when sending unsupported content', async () => {
  268. registerFileProtocol(protocolName, (request, callback) => callback(new Date() as any));
  269. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  270. });
  271. });
  272. }
  273. for (const [registerHttpProtocol, name] of [
  274. [protocol.registerHttpProtocol, 'protocol.registerHttpProtocol'] as const,
  275. [(protocol as any).registerProtocol as typeof protocol.registerHttpProtocol, 'protocol.registerProtocol'] as const
  276. ]) {
  277. describe(name, () => {
  278. it('sends url as response', async () => {
  279. const server = http.createServer((req, res) => {
  280. expect(req.headers.accept).to.not.equal('');
  281. res.end(text);
  282. server.close();
  283. });
  284. const { url } = await listen(server);
  285. registerHttpProtocol(protocolName, (request, callback) => callback({ url }));
  286. const r = await ajax(protocolName + '://fake-host');
  287. expect(r.data).to.equal(text);
  288. });
  289. it('fails when sending invalid url', async () => {
  290. registerHttpProtocol(protocolName, (request, callback) => callback({ url: 'url' }));
  291. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  292. });
  293. it('fails when sending unsupported content', async () => {
  294. registerHttpProtocol(protocolName, (request, callback) => callback(new Date() as any));
  295. await expect(ajax(protocolName + '://fake-host')).to.be.eventually.rejected();
  296. });
  297. it('works when target URL redirects', async () => {
  298. const server = http.createServer((req, res) => {
  299. if (req.url === '/serverRedirect') {
  300. res.statusCode = 301;
  301. res.setHeader('Location', `http://${req.rawHeaders[1]}`);
  302. res.end();
  303. } else {
  304. res.end(text);
  305. }
  306. });
  307. after(() => server.close());
  308. const { port } = await listen(server);
  309. const url = `${protocolName}://fake-host`;
  310. const redirectURL = `http://127.0.0.1:${port}/serverRedirect`;
  311. registerHttpProtocol(protocolName, (request, callback) => callback({ url: redirectURL }));
  312. const r = await ajax(url);
  313. expect(r.data).to.equal(text);
  314. });
  315. it('can access request headers', (done) => {
  316. protocol.registerHttpProtocol(protocolName, (request) => {
  317. try {
  318. expect(request).to.have.property('headers');
  319. done();
  320. } catch (e) {
  321. done(e);
  322. }
  323. });
  324. ajax(protocolName + '://fake-host').catch(() => {});
  325. });
  326. });
  327. }
  328. for (const [registerStreamProtocol, name] of [
  329. [protocol.registerStreamProtocol, 'protocol.registerStreamProtocol'] as const,
  330. [(protocol as any).registerProtocol as typeof protocol.registerStreamProtocol, 'protocol.registerProtocol'] as const
  331. ]) {
  332. describe(name, () => {
  333. it('sends Stream as response', async () => {
  334. registerStreamProtocol(protocolName, (request, callback) => callback(getStream()));
  335. const r = await ajax(protocolName + '://fake-host');
  336. expect(r.data).to.equal(text);
  337. });
  338. it('sends object as response', async () => {
  339. registerStreamProtocol(protocolName, (request, callback) => callback({ data: getStream() }));
  340. const r = await ajax(protocolName + '://fake-host');
  341. expect(r.data).to.equal(text);
  342. expect(r.status).to.equal(200);
  343. });
  344. it('sends custom response headers', async () => {
  345. registerStreamProtocol(protocolName, (request, callback) => callback({
  346. data: getStream(3),
  347. headers: {
  348. 'x-electron': ['a', 'b']
  349. }
  350. }));
  351. const r = await ajax(protocolName + '://fake-host');
  352. expect(r.data).to.equal(text);
  353. expect(r.status).to.equal(200);
  354. expect(r.headers).to.have.property('x-electron', 'a, b');
  355. });
  356. it('sends custom status code', async () => {
  357. registerStreamProtocol(protocolName, (request, callback) => callback({
  358. statusCode: 204,
  359. data: null as any
  360. }));
  361. const r = await ajax(protocolName + '://fake-host');
  362. expect(r.data).to.be.empty('data');
  363. expect(r.status).to.equal(204);
  364. });
  365. it('receives request headers', async () => {
  366. registerStreamProtocol(protocolName, (request, callback) => {
  367. callback({
  368. headers: {
  369. 'content-type': 'application/json'
  370. },
  371. data: getStream(5, JSON.stringify(Object.assign({}, request.headers)))
  372. });
  373. });
  374. const r = await ajax(protocolName + '://fake-host', { headers: { 'x-return-headers': 'yes' } });
  375. expect(JSON.parse(r.data)['x-return-headers']).to.equal('yes');
  376. });
  377. it('returns response multiple response headers with the same name', async () => {
  378. registerStreamProtocol(protocolName, (request, callback) => {
  379. callback({
  380. headers: {
  381. header1: ['value1', 'value2'],
  382. header2: 'value3'
  383. },
  384. data: getStream()
  385. });
  386. });
  387. const r = await ajax(protocolName + '://fake-host');
  388. // SUBTLE: when the response headers have multiple values it
  389. // separates values by ", ". When the response headers are incorrectly
  390. // converting an array to a string it separates values by ",".
  391. expect(r.headers).to.have.property('header1', 'value1, value2');
  392. expect(r.headers).to.have.property('header2', 'value3');
  393. });
  394. it('can handle large responses', async () => {
  395. const data = Buffer.alloc(128 * 1024);
  396. registerStreamProtocol(protocolName, (request, callback) => {
  397. callback(getStream(data.length, data));
  398. });
  399. const r = await ajax(protocolName + '://fake-host');
  400. expect(r.data).to.have.lengthOf(data.length);
  401. });
  402. it('can handle a stream completing while writing', async () => {
  403. function dumbPassthrough () {
  404. return new stream.Transform({
  405. async transform (chunk, encoding, cb) {
  406. cb(null, chunk);
  407. }
  408. });
  409. }
  410. registerStreamProtocol(protocolName, (request, callback) => {
  411. callback({
  412. statusCode: 200,
  413. headers: { 'Content-Type': 'text/plain' },
  414. data: getStream(1024 * 1024, Buffer.alloc(1024 * 1024 * 2)).pipe(dumbPassthrough())
  415. });
  416. });
  417. const r = await ajax(protocolName + '://fake-host');
  418. expect(r.data).to.have.lengthOf(1024 * 1024 * 2);
  419. });
  420. it('can handle next-tick scheduling during read calls', async () => {
  421. const events = new EventEmitter();
  422. function createStream () {
  423. const buffers = [
  424. Buffer.alloc(65536),
  425. Buffer.alloc(65537),
  426. Buffer.alloc(39156)
  427. ];
  428. const e = new stream.Readable({ highWaterMark: 0 });
  429. e.push(buffers.shift());
  430. e._read = function () {
  431. process.nextTick(() => this.push(buffers.shift() || null));
  432. };
  433. e.on('end', function () {
  434. events.emit('end');
  435. });
  436. return e;
  437. }
  438. registerStreamProtocol(protocolName, (request, callback) => {
  439. callback({
  440. statusCode: 200,
  441. headers: { 'Content-Type': 'text/plain' },
  442. data: createStream()
  443. });
  444. });
  445. const hasEndedPromise = once(events, 'end');
  446. ajax(protocolName + '://fake-host').catch(() => {});
  447. await hasEndedPromise;
  448. });
  449. it('destroys response streams when aborted before completion', async () => {
  450. const events = new EventEmitter();
  451. registerStreamProtocol(protocolName, (request, callback) => {
  452. const responseStream = new stream.PassThrough();
  453. responseStream.push('data\r\n');
  454. responseStream.on('close', () => {
  455. events.emit('close');
  456. });
  457. callback({
  458. statusCode: 200,
  459. headers: { 'Content-Type': 'text/plain' },
  460. data: responseStream
  461. });
  462. events.emit('respond');
  463. });
  464. const hasRespondedPromise = once(events, 'respond');
  465. const hasClosedPromise = once(events, 'close');
  466. ajax(protocolName + '://fake-host').catch(() => {});
  467. await hasRespondedPromise;
  468. await contents.loadFile(path.join(__dirname, 'fixtures', 'pages', 'fetch.html'));
  469. await hasClosedPromise;
  470. });
  471. });
  472. }
  473. describe('protocol.isProtocolRegistered', () => {
  474. it('returns false when scheme is not registered', () => {
  475. const result = protocol.isProtocolRegistered('no-exist');
  476. expect(result).to.be.false('no-exist: is handled');
  477. });
  478. it('returns true for custom protocol', () => {
  479. registerStringProtocol(protocolName, (request, callback) => callback(''));
  480. const result = protocol.isProtocolRegistered(protocolName);
  481. expect(result).to.be.true('custom protocol is handled');
  482. });
  483. });
  484. describe('protocol.isProtocolIntercepted', () => {
  485. it('returns true for intercepted protocol', () => {
  486. interceptStringProtocol('http', (request, callback) => callback(''));
  487. const result = protocol.isProtocolIntercepted('http');
  488. expect(result).to.be.true('intercepted protocol is handled');
  489. });
  490. });
  491. describe('protocol.intercept(Any)Protocol', () => {
  492. it('returns false when scheme is already intercepted', () => {
  493. expect(protocol.interceptStringProtocol('http', (request, callback) => callback(''))).to.equal(true);
  494. expect(protocol.interceptBufferProtocol('http', (request, callback) => callback(Buffer.from('')))).to.equal(false);
  495. });
  496. it('does not crash when handler is called twice', async () => {
  497. interceptStringProtocol('http', (request, callback) => {
  498. try {
  499. callback(text);
  500. callback('');
  501. } catch (error) {
  502. // Ignore error
  503. }
  504. });
  505. const r = await ajax('http://fake-host');
  506. expect(r.data).to.be.equal(text);
  507. });
  508. it('sends error when callback is called with nothing', async () => {
  509. interceptStringProtocol('http', (request, callback: any) => callback());
  510. await expect(ajax('http://fake-host')).to.be.eventually.rejected();
  511. });
  512. });
  513. describe('protocol.interceptStringProtocol', () => {
  514. it('can intercept http protocol', async () => {
  515. interceptStringProtocol('http', (request, callback) => callback(text));
  516. const r = await ajax('http://fake-host');
  517. expect(r.data).to.equal(text);
  518. });
  519. it('can set content-type', async () => {
  520. interceptStringProtocol('http', (request, callback) => {
  521. callback({
  522. mimeType: 'application/json',
  523. data: '{"value": 1}'
  524. });
  525. });
  526. const r = await ajax('http://fake-host');
  527. expect(JSON.parse(r.data)).to.have.property('value').that.is.equal(1);
  528. });
  529. it('can set content-type with charset', async () => {
  530. interceptStringProtocol('http', (request, callback) => {
  531. callback({
  532. mimeType: 'application/json; charset=UTF-8',
  533. data: '{"value": 1}'
  534. });
  535. });
  536. const r = await ajax('http://fake-host');
  537. expect(JSON.parse(r.data)).to.have.property('value').that.is.equal(1);
  538. });
  539. it('can receive post data', async () => {
  540. interceptStringProtocol('http', (request, callback) => {
  541. const uploadData = request.uploadData![0].bytes.toString();
  542. callback({ data: uploadData });
  543. });
  544. const r = await ajax('http://fake-host', { method: 'POST', body: qs.stringify(postData) });
  545. expect({ ...qs.parse(r.data) }).to.deep.equal(postData);
  546. });
  547. });
  548. describe('protocol.interceptBufferProtocol', () => {
  549. it('can intercept http protocol', async () => {
  550. interceptBufferProtocol('http', (request, callback) => callback(Buffer.from(text)));
  551. const r = await ajax('http://fake-host');
  552. expect(r.data).to.equal(text);
  553. });
  554. it('can receive post data', async () => {
  555. interceptBufferProtocol('http', (request, callback) => {
  556. const uploadData = request.uploadData![0].bytes;
  557. callback(uploadData);
  558. });
  559. const r = await ajax('http://fake-host', { method: 'POST', body: qs.stringify(postData) });
  560. expect(qs.parse(r.data)).to.deep.equal({ name: 'post test', type: 'string' });
  561. });
  562. });
  563. describe('protocol.interceptHttpProtocol', () => {
  564. // FIXME(zcbenz): This test was passing because the test itself was wrong,
  565. // I don't know whether it ever passed before and we should take a look at
  566. // it in future.
  567. xit('can send POST request', async () => {
  568. const server = http.createServer((req, res) => {
  569. let body = '';
  570. req.on('data', (chunk) => {
  571. body += chunk;
  572. });
  573. req.on('end', () => {
  574. res.end(body);
  575. });
  576. server.close();
  577. });
  578. after(() => server.close());
  579. const { url } = await listen(server);
  580. interceptHttpProtocol('http', (request, callback) => {
  581. const data: Electron.ProtocolResponse = {
  582. url: url,
  583. method: 'POST',
  584. uploadData: {
  585. contentType: 'application/x-www-form-urlencoded',
  586. data: request.uploadData![0].bytes
  587. },
  588. session: undefined
  589. };
  590. callback(data);
  591. });
  592. const r = await ajax('http://fake-host', { type: 'POST', data: postData });
  593. expect({ ...qs.parse(r.data) }).to.deep.equal(postData);
  594. });
  595. it('can use custom session', async () => {
  596. const customSession = session.fromPartition('custom-ses', { cache: false });
  597. customSession.webRequest.onBeforeRequest((details, callback) => {
  598. expect(details.url).to.equal('http://fake-host/');
  599. callback({ cancel: true });
  600. });
  601. after(() => customSession.webRequest.onBeforeRequest(null));
  602. interceptHttpProtocol('http', (request, callback) => {
  603. callback({
  604. url: request.url,
  605. session: customSession
  606. });
  607. });
  608. await expect(ajax('http://fake-host')).to.be.eventually.rejectedWith(Error);
  609. });
  610. it('can access request headers', (done) => {
  611. protocol.interceptHttpProtocol('http', (request) => {
  612. try {
  613. expect(request).to.have.property('headers');
  614. done();
  615. } catch (e) {
  616. done(e);
  617. }
  618. });
  619. ajax('http://fake-host').catch(() => {});
  620. });
  621. });
  622. describe('protocol.interceptStreamProtocol', () => {
  623. it('can intercept http protocol', async () => {
  624. interceptStreamProtocol('http', (request, callback) => callback(getStream()));
  625. const r = await ajax('http://fake-host');
  626. expect(r.data).to.equal(text);
  627. });
  628. it('can receive post data', async () => {
  629. interceptStreamProtocol('http', (request, callback) => {
  630. callback(getStream(3, request.uploadData![0].bytes.toString()));
  631. });
  632. const r = await ajax('http://fake-host', { method: 'POST', body: qs.stringify(postData) });
  633. expect({ ...qs.parse(r.data) }).to.deep.equal(postData);
  634. });
  635. it('can execute redirects', async () => {
  636. interceptStreamProtocol('http', (request, callback) => {
  637. if (request.url.indexOf('http://fake-host') === 0) {
  638. setTimeout(300).then(() => {
  639. callback({
  640. data: '',
  641. statusCode: 302,
  642. headers: {
  643. Location: 'http://fake-redirect'
  644. }
  645. });
  646. });
  647. } else {
  648. expect(request.url.indexOf('http://fake-redirect')).to.equal(0);
  649. callback(getStream(1, 'redirect'));
  650. }
  651. });
  652. const r = await ajax('http://fake-host');
  653. expect(r.data).to.equal('redirect');
  654. });
  655. it('should discard post data after redirection', async () => {
  656. interceptStreamProtocol('http', (request, callback) => {
  657. if (request.url.indexOf('http://fake-host') === 0) {
  658. setTimeout(300).then(() => {
  659. callback({
  660. statusCode: 302,
  661. headers: {
  662. Location: 'http://fake-redirect'
  663. }
  664. });
  665. });
  666. } else {
  667. expect(request.url.indexOf('http://fake-redirect')).to.equal(0);
  668. callback(getStream(3, request.method));
  669. }
  670. });
  671. const r = await ajax('http://fake-host', { type: 'POST', data: postData });
  672. expect(r.data).to.equal('GET');
  673. });
  674. });
  675. describe('protocol.uninterceptProtocol', () => {
  676. it('returns false when scheme does not exist', () => {
  677. expect(uninterceptProtocol('not-exist')).to.equal(false);
  678. });
  679. it('returns false when scheme is not intercepted', () => {
  680. expect(uninterceptProtocol('http')).to.equal(false);
  681. });
  682. });
  683. describe('protocol.registerSchemeAsPrivileged', () => {
  684. it('does not crash on exit', async () => {
  685. const appPath = path.join(__dirname, 'fixtures', 'api', 'custom-protocol-shutdown.js');
  686. const appProcess = ChildProcess.spawn(process.execPath, ['--enable-logging', appPath]);
  687. let stdout = '';
  688. let stderr = '';
  689. appProcess.stdout.on('data', data => { process.stdout.write(data); stdout += data; });
  690. appProcess.stderr.on('data', data => { process.stderr.write(data); stderr += data; });
  691. const [code] = await once(appProcess, 'exit');
  692. if (code !== 0) {
  693. console.log('Exit code : ', code);
  694. console.log('stdout : ', stdout);
  695. console.log('stderr : ', stderr);
  696. }
  697. expect(code).to.equal(0);
  698. expect(stdout).to.not.contain('VALIDATION_ERROR_DESERIALIZATION_FAILED');
  699. expect(stderr).to.not.contain('VALIDATION_ERROR_DESERIALIZATION_FAILED');
  700. });
  701. });
  702. describe('protocol.registerSchemesAsPrivileged allowServiceWorkers', () => {
  703. protocol.registerStringProtocol(serviceWorkerScheme, (request, cb) => {
  704. if (request.url.endsWith('.js')) {
  705. cb({
  706. mimeType: 'text/javascript',
  707. charset: 'utf-8',
  708. data: 'console.log("Loaded")'
  709. });
  710. } else {
  711. cb({
  712. mimeType: 'text/html',
  713. charset: 'utf-8',
  714. data: '<!DOCTYPE html>'
  715. });
  716. }
  717. });
  718. after(() => protocol.unregisterProtocol(serviceWorkerScheme));
  719. it('should fail when registering invalid service worker', async () => {
  720. await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
  721. await expect(contents.executeJavaScript(`navigator.serviceWorker.register('${v4()}.notjs', {scope: './'})`)).to.be.rejected();
  722. });
  723. it('should be able to register service worker for custom scheme', async () => {
  724. await contents.loadURL(`${serviceWorkerScheme}://${v4()}.com`);
  725. await contents.executeJavaScript(`navigator.serviceWorker.register('${v4()}.js', {scope: './'})`);
  726. });
  727. });
  728. describe('protocol.registerSchemesAsPrivileged standard', () => {
  729. const origin = `${standardScheme}://fake-host`;
  730. const imageURL = `${origin}/test.png`;
  731. const filePath = path.join(fixturesPath, 'pages', 'b.html');
  732. const fileContent = '<img src="/test.png" />';
  733. let w: BrowserWindow;
  734. beforeEach(() => {
  735. w = new BrowserWindow({
  736. show: false,
  737. webPreferences: {
  738. nodeIntegration: true,
  739. contextIsolation: false
  740. }
  741. });
  742. });
  743. afterEach(async () => {
  744. await closeWindow(w);
  745. unregisterProtocol(standardScheme);
  746. w = null as unknown as BrowserWindow;
  747. });
  748. it('resolves relative resources', async () => {
  749. registerFileProtocol(standardScheme, (request, callback) => {
  750. if (request.url === imageURL) {
  751. callback('');
  752. } else {
  753. callback(filePath);
  754. }
  755. });
  756. await w.loadURL(origin);
  757. });
  758. it('resolves absolute resources', async () => {
  759. registerStringProtocol(standardScheme, (request, callback) => {
  760. if (request.url === imageURL) {
  761. callback('');
  762. } else {
  763. callback({
  764. data: fileContent,
  765. mimeType: 'text/html'
  766. });
  767. }
  768. });
  769. await w.loadURL(origin);
  770. });
  771. it('can have fetch working in it', async () => {
  772. const requestReceived = deferPromise();
  773. const server = http.createServer((req, res) => {
  774. res.end();
  775. server.close();
  776. requestReceived.resolve();
  777. });
  778. const { url } = await listen(server);
  779. const content = `<script>fetch(${JSON.stringify(url)})</script>`;
  780. registerStringProtocol(standardScheme, (request, callback) => callback({ data: content, mimeType: 'text/html' }));
  781. await w.loadURL(origin);
  782. await requestReceived;
  783. });
  784. it('can access files through the FileSystem API', (done) => {
  785. const filePath = path.join(fixturesPath, 'pages', 'filesystem.html');
  786. protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
  787. w.loadURL(origin);
  788. ipcMain.once('file-system-error', (event, err) => done(err));
  789. ipcMain.once('file-system-write-end', () => done());
  790. });
  791. it('registers secure, when {secure: true}', (done) => {
  792. const filePath = path.join(fixturesPath, 'pages', 'cache-storage.html');
  793. ipcMain.once('success', () => done());
  794. ipcMain.once('failure', (event, err) => done(err));
  795. protocol.registerFileProtocol(standardScheme, (request, callback) => callback({ path: filePath }));
  796. w.loadURL(origin);
  797. });
  798. });
  799. describe('protocol.registerSchemesAsPrivileged cors-fetch', function () {
  800. let w: BrowserWindow;
  801. beforeEach(async () => {
  802. w = new BrowserWindow({ show: false });
  803. });
  804. afterEach(async () => {
  805. await closeWindow(w);
  806. w = null as unknown as BrowserWindow;
  807. for (const scheme of [standardScheme, 'cors', 'no-cors', 'no-fetch']) {
  808. protocol.unregisterProtocol(scheme);
  809. }
  810. });
  811. it('supports fetch api by default', async () => {
  812. const url = `file://${fixturesPath}/assets/logo.png`;
  813. await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
  814. const ok = await w.webContents.executeJavaScript(`fetch(${JSON.stringify(url)}).then(r => r.ok)`);
  815. expect(ok).to.be.true('response ok');
  816. });
  817. it('allows CORS requests by default', async () => {
  818. await allowsCORSRequests('cors', 200, new RegExp(''), () => {
  819. const { ipcRenderer } = require('electron');
  820. fetch('cors://myhost').then(function (response) {
  821. ipcRenderer.send('response', response.status);
  822. }).catch(function () {
  823. ipcRenderer.send('response', 'failed');
  824. });
  825. });
  826. });
  827. // DISABLED-FIXME: Figure out why this test is failing
  828. it('disallows CORS and fetch requests when only supportFetchAPI is specified', async () => {
  829. await allowsCORSRequests('no-cors', ['failed xhr', 'failed fetch'], /has been blocked by CORS policy/, () => {
  830. const { ipcRenderer } = require('electron');
  831. Promise.all([
  832. new Promise(resolve => {
  833. const req = new XMLHttpRequest();
  834. req.onload = () => resolve('loaded xhr');
  835. req.onerror = () => resolve('failed xhr');
  836. req.open('GET', 'no-cors://myhost');
  837. req.send();
  838. }),
  839. fetch('no-cors://myhost')
  840. .then(() => 'loaded fetch')
  841. .catch(() => 'failed fetch')
  842. ]).then(([xhr, fetch]) => {
  843. ipcRenderer.send('response', [xhr, fetch]);
  844. });
  845. });
  846. });
  847. it('allows CORS, but disallows fetch requests, when specified', async () => {
  848. await allowsCORSRequests('no-fetch', ['loaded xhr', 'failed fetch'], /Fetch API cannot load/, () => {
  849. const { ipcRenderer } = require('electron');
  850. Promise.all([
  851. new Promise(resolve => {
  852. const req = new XMLHttpRequest();
  853. req.onload = () => resolve('loaded xhr');
  854. req.onerror = () => resolve('failed xhr');
  855. req.open('GET', 'no-fetch://myhost');
  856. req.send();
  857. }),
  858. fetch('no-fetch://myhost')
  859. .then(() => 'loaded fetch')
  860. .catch(() => 'failed fetch')
  861. ]).then(([xhr, fetch]) => {
  862. ipcRenderer.send('response', [xhr, fetch]);
  863. });
  864. });
  865. });
  866. async function allowsCORSRequests (corsScheme: string, expected: any, expectedConsole: RegExp, content: Function) {
  867. registerStringProtocol(standardScheme, (request, callback) => {
  868. callback({ data: `<script>(${content})()</script>`, mimeType: 'text/html' });
  869. });
  870. registerStringProtocol(corsScheme, (request, callback) => {
  871. callback('');
  872. });
  873. const newContents = (webContents as typeof ElectronInternal.WebContents).create({
  874. nodeIntegration: true,
  875. contextIsolation: false
  876. });
  877. const consoleMessages: string[] = [];
  878. newContents.on('console-message', (e, level, message) => consoleMessages.push(message));
  879. try {
  880. newContents.loadURL(standardScheme + '://fake-host');
  881. const [, response] = await once(ipcMain, 'response');
  882. expect(response).to.deep.equal(expected);
  883. expect(consoleMessages.join('\n')).to.match(expectedConsole);
  884. } finally {
  885. // This is called in a timeout to avoid a crash that happens when
  886. // calling destroy() in a microtask.
  887. setTimeout().then(() => {
  888. newContents.destroy();
  889. });
  890. }
  891. }
  892. });
  893. describe('protocol.registerSchemesAsPrivileged stream', async function () {
  894. const pagePath = path.join(fixturesPath, 'pages', 'video.html');
  895. const videoSourceImagePath = path.join(fixturesPath, 'video-source-image.webp');
  896. const videoPath = path.join(fixturesPath, 'video.webm');
  897. let w: BrowserWindow;
  898. before(async () => {
  899. // generate test video
  900. const imageBase64 = await fs.promises.readFile(videoSourceImagePath, 'base64');
  901. const imageDataUrl = `data:image/webp;base64,${imageBase64}`;
  902. const encoder = new WebmGenerator(15);
  903. for (let i = 0; i < 30; i++) {
  904. encoder.add(imageDataUrl);
  905. }
  906. await new Promise((resolve, reject) => {
  907. encoder.compile((output:Uint8Array) => {
  908. fs.promises.writeFile(videoPath, output).then(resolve, reject);
  909. });
  910. });
  911. });
  912. after(async () => {
  913. await fs.promises.unlink(videoPath);
  914. });
  915. beforeEach(async function () {
  916. w = new BrowserWindow({ show: false });
  917. await w.loadURL('about:blank');
  918. if (!await w.webContents.executeJavaScript('document.createElement(\'video\').canPlayType(\'video/webm; codecs="vp8.0"\')')) {
  919. this.skip();
  920. }
  921. });
  922. afterEach(async () => {
  923. await closeWindow(w);
  924. w = null as unknown as BrowserWindow;
  925. await protocol.unregisterProtocol(standardScheme);
  926. await protocol.unregisterProtocol('stream');
  927. });
  928. it('successfully plays videos when content is buffered (stream: false)', async () => {
  929. await streamsResponses(standardScheme, 'play');
  930. });
  931. it('successfully plays videos when streaming content (stream: true)', async () => {
  932. await streamsResponses('stream', 'play');
  933. });
  934. async function streamsResponses (testingScheme: string, expected: any) {
  935. const protocolHandler = (request: any, callback: Function) => {
  936. if (request.url.includes('/video.webm')) {
  937. const stat = fs.statSync(videoPath);
  938. const fileSize = stat.size;
  939. const range = request.headers.Range;
  940. if (range) {
  941. const parts = range.replace(/bytes=/, '').split('-');
  942. const start = parseInt(parts[0], 10);
  943. const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
  944. const chunksize = (end - start) + 1;
  945. const headers = {
  946. 'Content-Range': `bytes ${start}-${end}/${fileSize}`,
  947. 'Accept-Ranges': 'bytes',
  948. 'Content-Length': String(chunksize),
  949. 'Content-Type': 'video/webm'
  950. };
  951. callback({ statusCode: 206, headers, data: fs.createReadStream(videoPath, { start, end }) });
  952. } else {
  953. callback({
  954. statusCode: 200,
  955. headers: {
  956. 'Content-Length': String(fileSize),
  957. 'Content-Type': 'video/webm'
  958. },
  959. data: fs.createReadStream(videoPath)
  960. });
  961. }
  962. } else {
  963. callback({ data: fs.createReadStream(pagePath), headers: { 'Content-Type': 'text/html' }, statusCode: 200 });
  964. }
  965. };
  966. await registerStreamProtocol(standardScheme, protocolHandler);
  967. await registerStreamProtocol('stream', protocolHandler);
  968. const newContents = (webContents as typeof ElectronInternal.WebContents).create({
  969. nodeIntegration: true,
  970. contextIsolation: false
  971. });
  972. try {
  973. newContents.loadURL(testingScheme + '://fake-host');
  974. const [, response] = await once(ipcMain, 'result');
  975. expect(response).to.deep.equal(expected);
  976. } finally {
  977. // This is called in a timeout to avoid a crash that happens when
  978. // calling destroy() in a microtask.
  979. setTimeout().then(() => {
  980. newContents.destroy();
  981. });
  982. }
  983. }
  984. });
  985. describe('handle', () => {
  986. afterEach(closeAllWindows);
  987. it('receives requests to a custom scheme', async () => {
  988. protocol.handle('test-scheme', (req) => new Response('hello ' + req.url));
  989. defer(() => { protocol.unhandle('test-scheme'); });
  990. const resp = await net.fetch('test-scheme://foo');
  991. expect(resp.status).to.equal(200);
  992. });
  993. it('can be unhandled', async () => {
  994. protocol.handle('test-scheme', (req) => new Response('hello ' + req.url));
  995. defer(() => {
  996. try {
  997. // In case of failure, make sure we unhandle. But we should succeed
  998. // :)
  999. protocol.unhandle('test-scheme');
  1000. } catch (_ignored) { /* ignore */ }
  1001. });
  1002. const resp1 = await net.fetch('test-scheme://foo');
  1003. expect(resp1.status).to.equal(200);
  1004. protocol.unhandle('test-scheme');
  1005. await expect(net.fetch('test-scheme://foo')).to.eventually.be.rejectedWith(/ERR_UNKNOWN_URL_SCHEME/);
  1006. });
  1007. it('receives requests to the existing https scheme', async () => {
  1008. protocol.handle('https', (req) => new Response('hello ' + req.url));
  1009. defer(() => { protocol.unhandle('https'); });
  1010. const body = await net.fetch('https://foo').then(r => r.text());
  1011. expect(body).to.equal('hello https://foo/');
  1012. });
  1013. it('receives requests to the existing file scheme', (done) => {
  1014. const filePath = path.join(__dirname, 'fixtures', 'pages', 'a.html');
  1015. protocol.handle('file', (req) => {
  1016. let file;
  1017. if (process.platform === 'win32') {
  1018. file = `file:///${filePath.replace(/\\/g, '/')}`;
  1019. } else {
  1020. file = `file://${filePath}`;
  1021. }
  1022. if (req.url === file) done();
  1023. return new Response(req.url);
  1024. });
  1025. defer(() => { protocol.unhandle('file'); });
  1026. const w = new BrowserWindow();
  1027. w.loadFile(filePath);
  1028. });
  1029. it('receives requests to an existing scheme when navigating', async () => {
  1030. protocol.handle('https', (req) => new Response('hello ' + req.url));
  1031. defer(() => { protocol.unhandle('https'); });
  1032. const w = new BrowserWindow({ show: false });
  1033. await w.loadURL('https://localhost');
  1034. expect(await w.webContents.executeJavaScript('document.body.textContent')).to.equal('hello https://localhost/');
  1035. });
  1036. it('can send buffer body', async () => {
  1037. protocol.handle('test-scheme', (req) => new Response(Buffer.from('hello ' + req.url)));
  1038. defer(() => { protocol.unhandle('test-scheme'); });
  1039. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1040. expect(body).to.equal('hello test-scheme://foo');
  1041. });
  1042. it('can send stream body', async () => {
  1043. protocol.handle('test-scheme', () => new Response(getWebStream()));
  1044. defer(() => { protocol.unhandle('test-scheme'); });
  1045. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1046. expect(body).to.equal(text);
  1047. });
  1048. it('accepts urls with no hostname in non-standard schemes', async () => {
  1049. protocol.handle('test-scheme', (req) => new Response(req.url));
  1050. defer(() => { protocol.unhandle('test-scheme'); });
  1051. {
  1052. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1053. expect(body).to.equal('test-scheme://foo');
  1054. }
  1055. {
  1056. const body = await net.fetch('test-scheme:///foo').then(r => r.text());
  1057. expect(body).to.equal('test-scheme:///foo');
  1058. }
  1059. {
  1060. const body = await net.fetch('test-scheme://').then(r => r.text());
  1061. expect(body).to.equal('test-scheme://');
  1062. }
  1063. });
  1064. it('accepts urls with a port-like component in non-standard schemes', async () => {
  1065. protocol.handle('test-scheme', (req) => new Response(req.url));
  1066. defer(() => { protocol.unhandle('test-scheme'); });
  1067. {
  1068. const body = await net.fetch('test-scheme://foo:30').then(r => r.text());
  1069. expect(body).to.equal('test-scheme://foo:30');
  1070. }
  1071. });
  1072. it('normalizes urls in standard schemes', async () => {
  1073. // NB. 'app' is registered as a standard scheme in test setup.
  1074. protocol.handle('app', (req) => new Response(req.url));
  1075. defer(() => { protocol.unhandle('app'); });
  1076. {
  1077. const body = await net.fetch('app://foo').then(r => r.text());
  1078. expect(body).to.equal('app://foo/');
  1079. }
  1080. {
  1081. const body = await net.fetch('app:///foo').then(r => r.text());
  1082. expect(body).to.equal('app://foo/');
  1083. }
  1084. // NB. 'app' is registered with the default scheme type of 'host'.
  1085. {
  1086. const body = await net.fetch('app://foo:1234').then(r => r.text());
  1087. expect(body).to.equal('app://foo/');
  1088. }
  1089. await expect(net.fetch('app://')).to.be.rejectedWith('Invalid URL');
  1090. });
  1091. it('fails on URLs with a username', async () => {
  1092. // NB. 'app' is registered as a standard scheme in test setup.
  1093. protocol.handle('http', (req) => new Response(req.url));
  1094. defer(() => { protocol.unhandle('http'); });
  1095. await expect(contents.loadURL('http://x@foo:1234')).to.be.rejectedWith(/ERR_UNEXPECTED/);
  1096. });
  1097. it('normalizes http urls', async () => {
  1098. protocol.handle('http', (req) => new Response(req.url));
  1099. defer(() => { protocol.unhandle('http'); });
  1100. {
  1101. const body = await net.fetch('http://foo').then(r => r.text());
  1102. expect(body).to.equal('http://foo/');
  1103. }
  1104. });
  1105. it('can send errors', async () => {
  1106. protocol.handle('test-scheme', () => Response.error());
  1107. defer(() => { protocol.unhandle('test-scheme'); });
  1108. await expect(net.fetch('test-scheme://foo')).to.eventually.be.rejectedWith('net::ERR_FAILED');
  1109. });
  1110. it('handles invalid protocol response status', async () => {
  1111. protocol.handle('test-scheme', () => {
  1112. return { status: [] } as any;
  1113. });
  1114. defer(() => { protocol.unhandle('test-scheme'); });
  1115. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1116. });
  1117. it('handles invalid protocol response statusText', async () => {
  1118. protocol.handle('test-scheme', () => {
  1119. return { statusText: false } as any;
  1120. });
  1121. defer(() => { protocol.unhandle('test-scheme'); });
  1122. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1123. });
  1124. it('handles invalid protocol response header parameters', async () => {
  1125. protocol.handle('test-scheme', () => {
  1126. return { headers: false } as any;
  1127. });
  1128. defer(() => { protocol.unhandle('test-scheme'); });
  1129. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1130. });
  1131. it('handles invalid protocol response body parameters', async () => {
  1132. protocol.handle('test-scheme', () => {
  1133. return { body: false } as any;
  1134. });
  1135. defer(() => { protocol.unhandle('test-scheme'); });
  1136. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1137. });
  1138. it('handles a synchronous error in the handler', async () => {
  1139. protocol.handle('test-scheme', () => { throw new Error('test'); });
  1140. defer(() => { protocol.unhandle('test-scheme'); });
  1141. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1142. });
  1143. it('handles an asynchronous error in the handler', async () => {
  1144. protocol.handle('test-scheme', () => Promise.reject(new Error('rejected promise')));
  1145. defer(() => { protocol.unhandle('test-scheme'); });
  1146. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1147. });
  1148. it('correctly sets statusCode', async () => {
  1149. protocol.handle('test-scheme', () => new Response(null, { status: 201 }));
  1150. defer(() => { protocol.unhandle('test-scheme'); });
  1151. const resp = await net.fetch('test-scheme://foo');
  1152. expect(resp.status).to.equal(201);
  1153. });
  1154. it('correctly sets content-type and charset', async () => {
  1155. protocol.handle('test-scheme', () => new Response(null, { headers: { 'content-type': 'text/html; charset=testcharset' } }));
  1156. defer(() => { protocol.unhandle('test-scheme'); });
  1157. const resp = await net.fetch('test-scheme://foo');
  1158. expect(resp.headers.get('content-type')).to.equal('text/html; charset=testcharset');
  1159. });
  1160. it('can forward to http', async () => {
  1161. const server = http.createServer((req, res) => {
  1162. res.end(text);
  1163. });
  1164. defer(() => { server.close(); });
  1165. const { url } = await listen(server);
  1166. protocol.handle('test-scheme', () => net.fetch(url));
  1167. defer(() => { protocol.unhandle('test-scheme'); });
  1168. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1169. expect(body).to.equal(text);
  1170. });
  1171. it('can forward an http request with headers', async () => {
  1172. const server = http.createServer((req, res) => {
  1173. res.setHeader('foo', 'bar');
  1174. res.end(text);
  1175. });
  1176. defer(() => { server.close(); });
  1177. const { url } = await listen(server);
  1178. protocol.handle('test-scheme', (req) => net.fetch(url, { headers: req.headers }));
  1179. defer(() => { protocol.unhandle('test-scheme'); });
  1180. const resp = await net.fetch('test-scheme://foo');
  1181. expect(resp.headers.get('foo')).to.equal('bar');
  1182. });
  1183. it('can forward to file', async () => {
  1184. protocol.handle('test-scheme', () => net.fetch(url.pathToFileURL(path.join(__dirname, 'fixtures', 'hello.txt')).toString()));
  1185. defer(() => { protocol.unhandle('test-scheme'); });
  1186. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1187. expect(body.trimEnd()).to.equal('hello world');
  1188. });
  1189. it('can receive simple request body', async () => {
  1190. protocol.handle('test-scheme', (req) => new Response(req.body));
  1191. defer(() => { protocol.unhandle('test-scheme'); });
  1192. const body = await net.fetch('test-scheme://foo', {
  1193. method: 'POST',
  1194. body: 'foobar'
  1195. }).then(r => r.text());
  1196. expect(body).to.equal('foobar');
  1197. });
  1198. it('can receive stream request body', async () => {
  1199. protocol.handle('test-scheme', (req) => new Response(req.body));
  1200. defer(() => { protocol.unhandle('test-scheme'); });
  1201. const body = await net.fetch('test-scheme://foo', {
  1202. method: 'POST',
  1203. body: getWebStream(),
  1204. duplex: 'half' // https://github.com/microsoft/TypeScript/issues/53157
  1205. } as any).then(r => r.text());
  1206. expect(body).to.equal(text);
  1207. });
  1208. it('can receive multi-part postData from loadURL', async () => {
  1209. protocol.handle('test-scheme', (req) => new Response(req.body));
  1210. defer(() => { protocol.unhandle('test-scheme'); });
  1211. await contents.loadURL('test-scheme://foo', { postData: [{ type: 'rawData', bytes: Buffer.from('a') }, { type: 'rawData', bytes: Buffer.from('b') }] });
  1212. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('ab');
  1213. });
  1214. it('can receive file postData from loadURL', async () => {
  1215. protocol.handle('test-scheme', (req) => new Response(req.body));
  1216. defer(() => { protocol.unhandle('test-scheme'); });
  1217. await contents.loadURL('test-scheme://foo', { postData: [{ type: 'file', filePath: path.join(fixturesPath, 'hello.txt'), length: 'hello world\n'.length, offset: 0, modificationTime: 0 }] });
  1218. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello world\n');
  1219. });
  1220. it('can receive file postData from a form', async () => {
  1221. protocol.handle('test-scheme', (req) => new Response(req.body));
  1222. defer(() => { protocol.unhandle('test-scheme'); });
  1223. await contents.loadURL('data:text/html,<form action="test-scheme://foo" method=POST enctype="multipart/form-data"><input name=foo type=file>');
  1224. const { debugger: dbg } = contents;
  1225. dbg.attach();
  1226. const { root } = await dbg.sendCommand('DOM.getDocument');
  1227. const { nodeId: fileInputNodeId } = await dbg.sendCommand('DOM.querySelector', { nodeId: root.nodeId, selector: 'input' });
  1228. await dbg.sendCommand('DOM.setFileInputFiles', {
  1229. nodeId: fileInputNodeId,
  1230. files: [
  1231. path.join(fixturesPath, 'hello.txt')
  1232. ]
  1233. });
  1234. const navigated = once(contents, 'did-finish-load');
  1235. await contents.executeJavaScript('document.querySelector("form").submit()');
  1236. await navigated;
  1237. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.match(/------WebKitFormBoundary.*\nContent-Disposition: form-data; name="foo"; filename="hello.txt"\nContent-Type: text\/plain\n\nhello world\n\n------WebKitFormBoundary.*--\n/);
  1238. });
  1239. it('can receive streaming fetch upload', async () => {
  1240. protocol.handle('no-cors', (req) => new Response(req.body));
  1241. defer(() => { protocol.unhandle('no-cors'); });
  1242. await contents.loadURL('no-cors://foo');
  1243. const fetchBodyResult = await contents.executeJavaScript(`
  1244. const stream = new ReadableStream({
  1245. async start(controller) {
  1246. controller.enqueue('hello world');
  1247. controller.close();
  1248. },
  1249. }).pipeThrough(new TextEncoderStream());
  1250. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
  1251. `);
  1252. expect(fetchBodyResult).to.equal('hello world');
  1253. });
  1254. it('can receive streaming fetch upload when a webRequest handler is present', async () => {
  1255. session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
  1256. console.log('webRequest', details.url, details.method);
  1257. cb({});
  1258. });
  1259. defer(() => {
  1260. session.defaultSession.webRequest.onBeforeRequest(null);
  1261. });
  1262. protocol.handle('no-cors', (req) => {
  1263. console.log('handle', req.url, req.method);
  1264. return new Response(req.body);
  1265. });
  1266. defer(() => { protocol.unhandle('no-cors'); });
  1267. await contents.loadURL('no-cors://foo');
  1268. const fetchBodyResult = await contents.executeJavaScript(`
  1269. const stream = new ReadableStream({
  1270. async start(controller) {
  1271. controller.enqueue('hello world');
  1272. controller.close();
  1273. },
  1274. }).pipeThrough(new TextEncoderStream());
  1275. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
  1276. `);
  1277. expect(fetchBodyResult).to.equal('hello world');
  1278. });
  1279. it('can receive an error from streaming fetch upload', async () => {
  1280. protocol.handle('no-cors', (req) => new Response(req.body));
  1281. defer(() => { protocol.unhandle('no-cors'); });
  1282. await contents.loadURL('no-cors://foo');
  1283. const fetchBodyResult = await contents.executeJavaScript(`
  1284. const stream = new ReadableStream({
  1285. async start(controller) {
  1286. controller.error('test')
  1287. },
  1288. });
  1289. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
  1290. `);
  1291. expect(fetchBodyResult).to.be.an.instanceOf(Error);
  1292. });
  1293. it('gets an error from streaming fetch upload when the renderer dies', async () => {
  1294. let gotRequest: Function;
  1295. const receivedRequest = new Promise<Request>(resolve => { gotRequest = resolve; });
  1296. protocol.handle('no-cors', (req) => {
  1297. if (/fetch/.test(req.url)) gotRequest(req);
  1298. return new Response();
  1299. });
  1300. defer(() => { protocol.unhandle('no-cors'); });
  1301. await contents.loadURL('no-cors://foo');
  1302. contents.executeJavaScript(`
  1303. const stream = new ReadableStream({
  1304. async start(controller) {
  1305. window.controller = controller // no GC
  1306. },
  1307. });
  1308. fetch(location.href + '/fetch', {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
  1309. `);
  1310. const req = await receivedRequest;
  1311. contents.destroy();
  1312. // Undo .destroy() for the next test
  1313. contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  1314. await expect(req.body!.getReader().read()).to.eventually.be.rejectedWith('net::ERR_FAILED');
  1315. });
  1316. it('can bypass intercepeted protocol handlers', async () => {
  1317. protocol.handle('http', () => new Response('custom'));
  1318. defer(() => { protocol.unhandle('http'); });
  1319. const server = http.createServer((req, res) => {
  1320. res.end('default');
  1321. });
  1322. defer(() => server.close());
  1323. const { url } = await listen(server);
  1324. expect(await net.fetch(url, { bypassCustomProtocolHandlers: true }).then(r => r.text())).to.equal('default');
  1325. });
  1326. it('bypassing custom protocol handlers also bypasses new protocols', async () => {
  1327. protocol.handle('app', () => new Response('custom'));
  1328. defer(() => { protocol.unhandle('app'); });
  1329. await expect(net.fetch('app://foo', { bypassCustomProtocolHandlers: true })).to.be.rejectedWith('net::ERR_UNKNOWN_URL_SCHEME');
  1330. });
  1331. it('can forward to the original handler', async () => {
  1332. protocol.handle('http', (req) => net.fetch(req, { bypassCustomProtocolHandlers: true }));
  1333. defer(() => { protocol.unhandle('http'); });
  1334. const server = http.createServer((req, res) => {
  1335. res.end('hello');
  1336. server.close();
  1337. });
  1338. const { url } = await listen(server);
  1339. await contents.loadURL(url);
  1340. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello');
  1341. });
  1342. it('supports sniffing mime type', async () => {
  1343. protocol.handle('http', async (req) => {
  1344. return net.fetch(req, { bypassCustomProtocolHandlers: true });
  1345. });
  1346. defer(() => { protocol.unhandle('http'); });
  1347. const server = http.createServer((req, res) => {
  1348. if (/html/.test(req.url ?? '')) { res.end('<!doctype html><body>hi'); } else { res.end('hi'); }
  1349. });
  1350. const { url } = await listen(server);
  1351. defer(() => server.close());
  1352. {
  1353. await contents.loadURL(url);
  1354. const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
  1355. expect(doc).to.match(/white-space: pre-wrap/);
  1356. }
  1357. {
  1358. await contents.loadURL(url + '?html');
  1359. const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
  1360. expect(doc).to.equal('<html><head></head><body>hi</body></html>');
  1361. }
  1362. });
  1363. // TODO(nornagon): this test doesn't pass on Linux currently, investigate.
  1364. ifit(process.platform !== 'linux')('is fast', async () => {
  1365. // 128 MB of spaces.
  1366. const chunk = new Uint8Array(128 * 1024 * 1024);
  1367. chunk.fill(' '.charCodeAt(0));
  1368. const server = http.createServer((req, res) => {
  1369. // The sniffed mime type for the space-filled chunk will be
  1370. // text/plain, which chews up all its performance in the renderer
  1371. // trying to wrap lines. Setting content-type to text/html measures
  1372. // something closer to just the raw cost of getting the bytes over
  1373. // the wire.
  1374. res.setHeader('content-type', 'text/html');
  1375. res.end(chunk);
  1376. });
  1377. defer(() => server.close());
  1378. const { url } = await listen(server);
  1379. const rawTime = await (async () => {
  1380. await contents.loadURL(url); // warm
  1381. const begin = Date.now();
  1382. await contents.loadURL(url);
  1383. const end = Date.now();
  1384. return end - begin;
  1385. })();
  1386. // Fetching through an intercepted handler should not be too much slower
  1387. // than it would be if the protocol hadn't been intercepted.
  1388. protocol.handle('http', async (req) => {
  1389. return net.fetch(req, { bypassCustomProtocolHandlers: true });
  1390. });
  1391. defer(() => { protocol.unhandle('http'); });
  1392. const interceptedTime = await (async () => {
  1393. const begin = Date.now();
  1394. await contents.loadURL(url);
  1395. const end = Date.now();
  1396. return end - begin;
  1397. })();
  1398. expect(interceptedTime).to.be.lessThan(rawTime * 1.5);
  1399. });
  1400. });
  1401. });