api-protocol-spec.ts 68 KB

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