api-protocol-spec.ts 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783
  1. import { protocol, webContents, WebContents, session, BrowserWindow, ipcMain, net } from 'electron/main';
  2. import { expect } from 'chai';
  3. import { v4 } from 'uuid';
  4. import * as ChildProcess from 'node:child_process';
  5. import { EventEmitter, once } from 'node:events';
  6. import * as fs from 'node:fs';
  7. import * as http from 'node:http';
  8. import * as path from 'node:path';
  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 { setTimeout } from 'node:timers/promises';
  14. import * as url from 'node:url';
  15. import { listen, defer, ifit } from './lib/spec-helpers';
  16. import { WebmGenerator } from './lib/video-helpers';
  17. import { closeAllWindows, closeWindow } from './lib/window-helpers';
  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,
  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, /(?:)/, () => {
  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) => consoleMessages.push(e.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('calls destroy on aborted body stream', async () => {
  1074. const abortController = new AbortController();
  1075. class TestStream extends stream.Readable {
  1076. _read () {
  1077. this.push('infinite data');
  1078. // Abort the request that reads from this stream.
  1079. abortController.abort();
  1080. }
  1081. };
  1082. const body = new TestStream();
  1083. protocol.handle('test-scheme', () => {
  1084. return new Response(stream.Readable.toWeb(body) as ReadableStream<ArrayBufferView>);
  1085. });
  1086. defer(() => { protocol.unhandle('test-scheme'); });
  1087. const res = net.fetch('test-scheme://foo', {
  1088. signal: abortController.signal
  1089. });
  1090. await expect(res).to.be.rejectedWith('This operation was aborted');
  1091. await expect(once(body, 'end')).to.be.rejectedWith('The operation was aborted');
  1092. });
  1093. it('accepts urls with no hostname in non-standard schemes', async () => {
  1094. protocol.handle('test-scheme', (req) => new Response(req.url));
  1095. defer(() => { protocol.unhandle('test-scheme'); });
  1096. {
  1097. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1098. expect(body).to.equal('test-scheme://foo');
  1099. }
  1100. {
  1101. const body = await net.fetch('test-scheme:///foo').then(r => r.text());
  1102. expect(body).to.equal('test-scheme:///foo');
  1103. }
  1104. {
  1105. const body = await net.fetch('test-scheme://').then(r => r.text());
  1106. expect(body).to.equal('test-scheme://');
  1107. }
  1108. });
  1109. it('accepts urls with a port-like component in non-standard schemes', async () => {
  1110. protocol.handle('test-scheme', (req) => new Response(req.url));
  1111. defer(() => { protocol.unhandle('test-scheme'); });
  1112. {
  1113. const body = await net.fetch('test-scheme://foo:30').then(r => r.text());
  1114. expect(body).to.equal('test-scheme://foo:30');
  1115. }
  1116. });
  1117. it('normalizes urls in standard schemes', async () => {
  1118. // NB. 'app' is registered as a standard scheme in test setup.
  1119. protocol.handle('app', (req) => new Response(req.url));
  1120. defer(() => { protocol.unhandle('app'); });
  1121. {
  1122. const body = await net.fetch('app://foo').then(r => r.text());
  1123. expect(body).to.equal('app://foo/');
  1124. }
  1125. {
  1126. const body = await net.fetch('app:///foo').then(r => r.text());
  1127. expect(body).to.equal('app://foo/');
  1128. }
  1129. // NB. 'app' is registered with the default scheme type of 'host'.
  1130. {
  1131. const body = await net.fetch('app://foo:1234').then(r => r.text());
  1132. expect(body).to.equal('app://foo/');
  1133. }
  1134. await expect(net.fetch('app://')).to.be.rejectedWith('Invalid URL');
  1135. });
  1136. it('fails on URLs with a username', async () => {
  1137. // NB. 'app' is registered as a standard scheme in test setup.
  1138. protocol.handle('http', (req) => new Response(req.url));
  1139. defer(() => { protocol.unhandle('http'); });
  1140. await expect(contents.loadURL('http://x@foo:1234')).to.be.rejectedWith(/ERR_UNEXPECTED/);
  1141. });
  1142. it('normalizes http urls', async () => {
  1143. protocol.handle('http', (req) => new Response(req.url));
  1144. defer(() => { protocol.unhandle('http'); });
  1145. {
  1146. const body = await net.fetch('http://foo').then(r => r.text());
  1147. expect(body).to.equal('http://foo/');
  1148. }
  1149. });
  1150. it('can send errors', async () => {
  1151. protocol.handle('test-scheme', () => Response.error());
  1152. defer(() => { protocol.unhandle('test-scheme'); });
  1153. await expect(net.fetch('test-scheme://foo')).to.eventually.be.rejectedWith('net::ERR_FAILED');
  1154. });
  1155. it('handles invalid protocol response status', async () => {
  1156. protocol.handle('test-scheme', () => {
  1157. return { status: [] } as any;
  1158. });
  1159. defer(() => { protocol.unhandle('test-scheme'); });
  1160. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1161. });
  1162. it('handles invalid protocol response statusText', async () => {
  1163. protocol.handle('test-scheme', () => {
  1164. return { statusText: false } as any;
  1165. });
  1166. defer(() => { protocol.unhandle('test-scheme'); });
  1167. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1168. });
  1169. it('handles invalid protocol response header parameters', async () => {
  1170. protocol.handle('test-scheme', () => {
  1171. return { headers: false } as any;
  1172. });
  1173. defer(() => { protocol.unhandle('test-scheme'); });
  1174. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1175. });
  1176. it('handles invalid protocol response body parameters', async () => {
  1177. protocol.handle('test-scheme', () => {
  1178. return { body: false } as any;
  1179. });
  1180. defer(() => { protocol.unhandle('test-scheme'); });
  1181. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1182. });
  1183. it('handles a synchronous error in the handler', async () => {
  1184. protocol.handle('test-scheme', () => { throw new Error('test'); });
  1185. defer(() => { protocol.unhandle('test-scheme'); });
  1186. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1187. });
  1188. it('handles an asynchronous error in the handler', async () => {
  1189. protocol.handle('test-scheme', () => Promise.reject(new Error('rejected promise')));
  1190. defer(() => { protocol.unhandle('test-scheme'); });
  1191. await expect(net.fetch('test-scheme://foo')).to.be.rejectedWith('net::ERR_UNEXPECTED');
  1192. });
  1193. it('correctly sets statusCode', async () => {
  1194. protocol.handle('test-scheme', () => new Response(null, { status: 201 }));
  1195. defer(() => { protocol.unhandle('test-scheme'); });
  1196. const resp = await net.fetch('test-scheme://foo');
  1197. expect(resp.status).to.equal(201);
  1198. });
  1199. it('correctly sets content-type and charset', async () => {
  1200. protocol.handle('test-scheme', () => new Response(null, { headers: { 'content-type': 'text/html; charset=testcharset' } }));
  1201. defer(() => { protocol.unhandle('test-scheme'); });
  1202. const resp = await net.fetch('test-scheme://foo');
  1203. expect(resp.headers.get('content-type')).to.equal('text/html; charset=testcharset');
  1204. });
  1205. it('can forward to http', async () => {
  1206. const server = http.createServer((req, res) => {
  1207. res.end(text);
  1208. });
  1209. defer(() => { server.close(); });
  1210. const { url } = await listen(server);
  1211. protocol.handle('test-scheme', () => net.fetch(url));
  1212. defer(() => { protocol.unhandle('test-scheme'); });
  1213. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1214. expect(body).to.equal(text);
  1215. });
  1216. it('can forward an http request with headers', async () => {
  1217. const server = http.createServer((req, res) => {
  1218. res.setHeader('foo', 'bar');
  1219. res.end(text);
  1220. });
  1221. defer(() => { server.close(); });
  1222. const { url } = await listen(server);
  1223. protocol.handle('test-scheme', (req) => net.fetch(url, { headers: req.headers }));
  1224. defer(() => { protocol.unhandle('test-scheme'); });
  1225. const resp = await net.fetch('test-scheme://foo');
  1226. expect(resp.headers.get('foo')).to.equal('bar');
  1227. });
  1228. it('can forward to file', async () => {
  1229. protocol.handle('test-scheme', () => net.fetch(url.pathToFileURL(path.join(__dirname, 'fixtures', 'hello.txt')).toString()));
  1230. defer(() => { protocol.unhandle('test-scheme'); });
  1231. const body = await net.fetch('test-scheme://foo').then(r => r.text());
  1232. expect(body.trimEnd()).to.equal('hello world');
  1233. });
  1234. it('can receive simple request body', async () => {
  1235. protocol.handle('test-scheme', (req) => new Response(req.body));
  1236. defer(() => { protocol.unhandle('test-scheme'); });
  1237. const body = await net.fetch('test-scheme://foo', {
  1238. method: 'POST',
  1239. body: 'foobar'
  1240. }).then(r => r.text());
  1241. expect(body).to.equal('foobar');
  1242. });
  1243. it('can receive stream request body', async () => {
  1244. protocol.handle('test-scheme', (req) => new Response(req.body));
  1245. defer(() => { protocol.unhandle('test-scheme'); });
  1246. const body = await net.fetch('test-scheme://foo', {
  1247. method: 'POST',
  1248. body: getWebStream(),
  1249. duplex: 'half' // https://github.com/microsoft/TypeScript/issues/53157
  1250. } as any).then(r => r.text());
  1251. expect(body).to.equal(text);
  1252. });
  1253. it('can receive stream request body asynchronously', async () => {
  1254. let done: any;
  1255. const requestReceived: Promise<Buffer[]> = new Promise(resolve => { done = resolve; });
  1256. protocol.handle('http-like', async (req) => {
  1257. const chunks = [];
  1258. for await (const chunk of (req.body as any)) {
  1259. chunks.push(chunk);
  1260. }
  1261. done(chunks);
  1262. return new Response('ok');
  1263. });
  1264. defer(() => { protocol.unhandle('http-like'); });
  1265. const w = new BrowserWindow({ show: false });
  1266. w.loadURL('about:blank');
  1267. const expectedHashChunks = await w.webContents.executeJavaScript(`
  1268. const dataStream = () =>
  1269. new ReadableStream({
  1270. async start(controller) {
  1271. for (let i = 0; i < 10; i++) { controller.enqueue(Array(1024 * 128).fill(+i).join("\\n")); }
  1272. controller.close();
  1273. },
  1274. }).pipeThrough(new TextEncoderStream());
  1275. fetch(
  1276. new Request("http-like://host", {
  1277. method: "POST",
  1278. body: dataStream(),
  1279. duplex: "half",
  1280. })
  1281. );
  1282. (async () => {
  1283. const chunks = []
  1284. for await (const chunk of dataStream()) {
  1285. chunks.push(chunk);
  1286. }
  1287. return chunks;
  1288. })()
  1289. `);
  1290. const expectedHash = Buffer.from(await crypto.subtle.digest('SHA-256', Buffer.concat(expectedHashChunks))).toString('hex');
  1291. const body = Buffer.concat(await requestReceived);
  1292. const actualHash = Buffer.from(await crypto.subtle.digest('SHA-256', Buffer.from(body))).toString('hex');
  1293. expect(actualHash).to.equal(expectedHash);
  1294. });
  1295. it('can receive multi-part postData from loadURL', async () => {
  1296. protocol.handle('test-scheme', (req) => new Response(req.body));
  1297. defer(() => { protocol.unhandle('test-scheme'); });
  1298. await contents.loadURL('test-scheme://foo', { postData: [{ type: 'rawData', bytes: Buffer.from('a') }, { type: 'rawData', bytes: Buffer.from('b') }] });
  1299. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('ab');
  1300. });
  1301. it('can receive file postData from loadURL', async () => {
  1302. protocol.handle('test-scheme', (req) => new Response(req.body));
  1303. defer(() => { protocol.unhandle('test-scheme'); });
  1304. await contents.loadURL('test-scheme://foo', { postData: [{ type: 'file', filePath: path.join(fixturesPath, 'hello.txt'), length: 'hello world\n'.length, offset: 0, modificationTime: 0 }] });
  1305. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello world\n');
  1306. });
  1307. it('can receive file postData from a form', async () => {
  1308. protocol.handle('test-scheme', (req) => new Response(req.body));
  1309. defer(() => { protocol.unhandle('test-scheme'); });
  1310. await contents.loadURL('data:text/html,<form action="test-scheme://foo" method=POST enctype="multipart/form-data"><input name=foo type=file>');
  1311. const { debugger: dbg } = contents;
  1312. dbg.attach();
  1313. const { root } = await dbg.sendCommand('DOM.getDocument');
  1314. const { nodeId: fileInputNodeId } = await dbg.sendCommand('DOM.querySelector', { nodeId: root.nodeId, selector: 'input' });
  1315. await dbg.sendCommand('DOM.setFileInputFiles', {
  1316. nodeId: fileInputNodeId,
  1317. files: [
  1318. path.join(fixturesPath, 'hello.txt')
  1319. ]
  1320. });
  1321. const navigated = once(contents, 'did-finish-load');
  1322. await contents.executeJavaScript('document.querySelector("form").submit()');
  1323. await navigated;
  1324. 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/);
  1325. });
  1326. it('can receive streaming fetch upload', async () => {
  1327. protocol.handle('no-cors', (req) => new Response(req.body));
  1328. defer(() => { protocol.unhandle('no-cors'); });
  1329. await contents.loadURL('no-cors://foo');
  1330. const fetchBodyResult = await contents.executeJavaScript(`
  1331. const stream = new ReadableStream({
  1332. async start(controller) {
  1333. controller.enqueue('hello world');
  1334. controller.close();
  1335. },
  1336. }).pipeThrough(new TextEncoderStream());
  1337. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
  1338. `);
  1339. expect(fetchBodyResult).to.equal('hello world');
  1340. });
  1341. it('can receive streaming fetch upload when a webRequest handler is present', async () => {
  1342. session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
  1343. console.log('webRequest', details.url, details.method);
  1344. cb({});
  1345. });
  1346. defer(() => {
  1347. session.defaultSession.webRequest.onBeforeRequest(null);
  1348. });
  1349. protocol.handle('no-cors', (req) => {
  1350. console.log('handle', req.url, req.method);
  1351. return new Response(req.body);
  1352. });
  1353. defer(() => { protocol.unhandle('no-cors'); });
  1354. await contents.loadURL('no-cors://foo');
  1355. const fetchBodyResult = await contents.executeJavaScript(`
  1356. const stream = new ReadableStream({
  1357. async start(controller) {
  1358. controller.enqueue('hello world');
  1359. controller.close();
  1360. },
  1361. }).pipeThrough(new TextEncoderStream());
  1362. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
  1363. `);
  1364. expect(fetchBodyResult).to.equal('hello world');
  1365. });
  1366. it('can receive an error from streaming fetch upload', async () => {
  1367. protocol.handle('no-cors', (req) => new Response(req.body));
  1368. defer(() => { protocol.unhandle('no-cors'); });
  1369. await contents.loadURL('no-cors://foo');
  1370. const fetchBodyResult = await contents.executeJavaScript(`
  1371. const stream = new ReadableStream({
  1372. async start(controller) {
  1373. controller.error('test')
  1374. },
  1375. });
  1376. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
  1377. `);
  1378. expect(fetchBodyResult).to.be.an.instanceOf(Error);
  1379. });
  1380. it('gets an error from streaming fetch upload when the renderer dies', async () => {
  1381. let gotRequest: Function;
  1382. const receivedRequest = new Promise<Request>(resolve => { gotRequest = resolve; });
  1383. protocol.handle('no-cors', (req) => {
  1384. if (/fetch/.test(req.url)) gotRequest(req);
  1385. return new Response();
  1386. });
  1387. defer(() => { protocol.unhandle('no-cors'); });
  1388. await contents.loadURL('no-cors://foo');
  1389. contents.executeJavaScript(`
  1390. const stream = new ReadableStream({
  1391. async start(controller) {
  1392. window.controller = controller // no GC
  1393. },
  1394. });
  1395. fetch(location.href + '/fetch', {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
  1396. `);
  1397. const req = await receivedRequest;
  1398. contents.destroy();
  1399. // Undo .destroy() for the next test
  1400. contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  1401. await expect(req.body!.getReader().read()).to.eventually.be.rejectedWith('net::ERR_FAILED');
  1402. });
  1403. it('can bypass intercepeted protocol handlers', async () => {
  1404. protocol.handle('http', () => new Response('custom'));
  1405. defer(() => { protocol.unhandle('http'); });
  1406. const server = http.createServer((req, res) => {
  1407. res.end('default');
  1408. });
  1409. defer(() => server.close());
  1410. const { url } = await listen(server);
  1411. expect(await net.fetch(url, { bypassCustomProtocolHandlers: true }).then(r => r.text())).to.equal('default');
  1412. });
  1413. it('bypassing custom protocol handlers also bypasses new protocols', async () => {
  1414. protocol.handle('app', () => new Response('custom'));
  1415. defer(() => { protocol.unhandle('app'); });
  1416. await expect(net.fetch('app://foo', { bypassCustomProtocolHandlers: true })).to.be.rejectedWith('net::ERR_UNKNOWN_URL_SCHEME');
  1417. });
  1418. it('can forward to the original handler', async () => {
  1419. protocol.handle('http', (req) => net.fetch(req, { bypassCustomProtocolHandlers: true }));
  1420. defer(() => { protocol.unhandle('http'); });
  1421. const server = http.createServer((req, res) => {
  1422. res.end('hello');
  1423. server.close();
  1424. });
  1425. const { url } = await listen(server);
  1426. await contents.loadURL(url);
  1427. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello');
  1428. });
  1429. it('supports sniffing mime type', async () => {
  1430. protocol.handle('http', async (req) => {
  1431. return net.fetch(req, { bypassCustomProtocolHandlers: true });
  1432. });
  1433. defer(() => { protocol.unhandle('http'); });
  1434. const server = http.createServer((req, res) => {
  1435. if (/html/.test(req.url ?? '')) { res.end('<!doctype html><body>hi'); } else { res.end('hi'); }
  1436. });
  1437. const { url } = await listen(server);
  1438. defer(() => server.close());
  1439. {
  1440. await contents.loadURL(url);
  1441. const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
  1442. expect(doc).to.match(/white-space: pre-wrap/);
  1443. }
  1444. {
  1445. await contents.loadURL(url + '?html');
  1446. const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
  1447. expect(doc).to.equal('<html><head></head><body>hi</body></html>');
  1448. }
  1449. });
  1450. it('does not emit undefined chunks into the request body stream when uploading a stream', async () => {
  1451. protocol.handle('cors', async (request) => {
  1452. expect(request.body).to.be.an.instanceOf(webStream.ReadableStream);
  1453. for await (const value of request.body as webStream.ReadableStream<Uint8Array>) {
  1454. expect(value).to.not.be.undefined();
  1455. }
  1456. return new Response(undefined, { status: 200 });
  1457. });
  1458. defer(() => { protocol.unhandle('cors'); });
  1459. await contents.loadFile(path.resolve(fixturesPath, 'pages', 'base-page.html'));
  1460. contents.on('console-message', (e) => console.log(e.message));
  1461. const ok = await contents.executeJavaScript(`(async () => {
  1462. function wait(milliseconds) {
  1463. return new Promise((resolve) => setTimeout(resolve, milliseconds));
  1464. }
  1465. const stream = new ReadableStream({
  1466. async start(controller) {
  1467. await wait(4);
  1468. controller.enqueue('This ');
  1469. await wait(4);
  1470. controller.enqueue('is ');
  1471. await wait(4);
  1472. controller.enqueue('a ');
  1473. await wait(4);
  1474. controller.enqueue('slow ');
  1475. await wait(4);
  1476. controller.enqueue('request.');
  1477. controller.close();
  1478. }
  1479. }).pipeThrough(new TextEncoderStream());
  1480. return (await fetch('cors://url.invalid', { method: 'POST', body: stream, duplex: 'half' })).ok;
  1481. })()`);
  1482. expect(ok).to.be.true();
  1483. });
  1484. it('does not emit undefined chunks into the request body stream when uploading a file', async () => {
  1485. protocol.handle('cors', async (request) => {
  1486. expect(request.body).to.be.an.instanceOf(webStream.ReadableStream);
  1487. for await (const value of request.body as webStream.ReadableStream<Uint8Array>) {
  1488. expect(value).to.not.be.undefined();
  1489. }
  1490. return new Response(undefined, { status: 200 });
  1491. });
  1492. defer(() => { protocol.unhandle('cors'); });
  1493. await contents.loadFile(path.resolve(fixturesPath, 'pages', 'file-input.html'));
  1494. const { debugger: debug } = contents;
  1495. debug.attach();
  1496. try {
  1497. const { root: { nodeId } } = await debug.sendCommand('DOM.getDocument');
  1498. const { nodeId: inputNodeId } = await debug.sendCommand('DOM.querySelector', { nodeId, selector: 'input' });
  1499. await debug.sendCommand('DOM.setFileInputFiles', {
  1500. files: [path.join(fixturesPath, 'cat-spin.mp4')],
  1501. nodeId: inputNodeId
  1502. });
  1503. const ok = await contents.executeJavaScript(`(async () => {
  1504. const formData = new FormData();
  1505. formData.append("data", document.getElementById("file").files[0]);
  1506. return (await fetch('cors://url.invalid', { method: 'POST', body: formData })).ok;
  1507. })()`);
  1508. expect(ok).to.be.true();
  1509. } finally {
  1510. debug.detach();
  1511. }
  1512. });
  1513. it('filters an illegal "origin: null" header', async () => {
  1514. protocol.handle('http', (req) => {
  1515. expect(new Headers(req.headers).get('origin')).to.not.equal('null');
  1516. return new Response();
  1517. });
  1518. defer(() => { protocol.unhandle('http'); });
  1519. const filePath = path.join(fixturesPath, 'pages', 'form-with-data.html');
  1520. await contents.loadFile(filePath);
  1521. const loadPromise = new Promise<void>((resolve, reject) => {
  1522. contents.once('did-finish-load', resolve);
  1523. contents.once('did-fail-load', (_, errorCode, errorDescription) =>
  1524. reject(new Error(`did-fail-load: ${errorCode} ${errorDescription}. See AssertionError for details.`))
  1525. );
  1526. });
  1527. await contents.executeJavaScript(`
  1528. const form = document.querySelector('form');
  1529. form.action = 'http://cors.invalid';
  1530. form.method = 'POST';
  1531. form.submit();
  1532. `);
  1533. await loadPromise;
  1534. });
  1535. it('does forward Blob chunks', async () => {
  1536. // we register the protocol on a separate session to validate the assumption
  1537. // that `getBlobData()` indeed returns the blob data from a global variable
  1538. const s = session.fromPartition('protocol-handle-forwards-blob-chunks');
  1539. s.protocol.handle('cors', async (request) => {
  1540. expect(request.body).to.be.an.instanceOf(webStream.ReadableStream);
  1541. return new Response(
  1542. `hello to ${await streamConsumers.text(request.body as webStream.ReadableStream<Uint8Array>)}`,
  1543. { status: 200 }
  1544. );
  1545. });
  1546. defer(() => { s.protocol.unhandle('cors'); });
  1547. const w = new BrowserWindow({ show: false, webPreferences: { session: s } });
  1548. await w.webContents.loadFile(path.resolve(fixturesPath, 'pages', 'base-page.html'));
  1549. const response = await w.webContents.executeJavaScript(`(async () => {
  1550. const body = new Blob(["it's-a ", 'me! ', 'Mario!'], { type: 'text/plain' });
  1551. return await (await fetch('cors://url.invalid', { method: 'POST', body })).text();
  1552. })()`);
  1553. expect(response).to.be.string('hello to it\'s-a me! Mario!');
  1554. });
  1555. // TODO(nornagon): this test doesn't pass on Linux currently, investigate.
  1556. // test is also flaky on CI on macOS so it is currently disabled there as well.
  1557. ifit(process.platform !== 'linux' && (!process.env.CI || process.platform !== 'darwin'))('is fast', async () => {
  1558. // 128 MB of spaces.
  1559. const chunk = new Uint8Array(128 * 1024 * 1024);
  1560. chunk.fill(' '.charCodeAt(0));
  1561. const server = http.createServer((req, res) => {
  1562. // The sniffed mime type for the space-filled chunk will be
  1563. // text/plain, which chews up all its performance in the renderer
  1564. // trying to wrap lines. Setting content-type to text/html measures
  1565. // something closer to just the raw cost of getting the bytes over
  1566. // the wire.
  1567. res.setHeader('content-type', 'text/html');
  1568. res.end(chunk);
  1569. });
  1570. defer(() => server.close());
  1571. const { url } = await listen(server);
  1572. const rawTime = await (async () => {
  1573. await contents.loadURL(url); // warm
  1574. const begin = Date.now();
  1575. await contents.loadURL(url);
  1576. const end = Date.now();
  1577. return end - begin;
  1578. })();
  1579. // Fetching through an intercepted handler should not be too much slower
  1580. // than it would be if the protocol hadn't been intercepted.
  1581. protocol.handle('http', async (req) => {
  1582. return net.fetch(req, { bypassCustomProtocolHandlers: true });
  1583. });
  1584. defer(() => { protocol.unhandle('http'); });
  1585. const interceptedTime = await (async () => {
  1586. const begin = Date.now();
  1587. await contents.loadURL(url);
  1588. const end = Date.now();
  1589. return end - begin;
  1590. })();
  1591. expect(interceptedTime).to.be.lessThan(rawTime * 1.6);
  1592. });
  1593. });
  1594. });