api-protocol-spec.ts 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712
  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 multi-part postData from loadURL', async () => {
  1234. protocol.handle('test-scheme', (req) => new Response(req.body));
  1235. defer(() => { protocol.unhandle('test-scheme'); });
  1236. await contents.loadURL('test-scheme://foo', { postData: [{ type: 'rawData', bytes: Buffer.from('a') }, { type: 'rawData', bytes: Buffer.from('b') }] });
  1237. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('ab');
  1238. });
  1239. it('can receive file postData from loadURL', async () => {
  1240. protocol.handle('test-scheme', (req) => new Response(req.body));
  1241. defer(() => { protocol.unhandle('test-scheme'); });
  1242. await contents.loadURL('test-scheme://foo', { postData: [{ type: 'file', filePath: path.join(fixturesPath, 'hello.txt'), length: 'hello world\n'.length, offset: 0, modificationTime: 0 }] });
  1243. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello world\n');
  1244. });
  1245. it('can receive file postData from a form', async () => {
  1246. protocol.handle('test-scheme', (req) => new Response(req.body));
  1247. defer(() => { protocol.unhandle('test-scheme'); });
  1248. await contents.loadURL('data:text/html,<form action="test-scheme://foo" method=POST enctype="multipart/form-data"><input name=foo type=file>');
  1249. const { debugger: dbg } = contents;
  1250. dbg.attach();
  1251. const { root } = await dbg.sendCommand('DOM.getDocument');
  1252. const { nodeId: fileInputNodeId } = await dbg.sendCommand('DOM.querySelector', { nodeId: root.nodeId, selector: 'input' });
  1253. await dbg.sendCommand('DOM.setFileInputFiles', {
  1254. nodeId: fileInputNodeId,
  1255. files: [
  1256. path.join(fixturesPath, 'hello.txt')
  1257. ]
  1258. });
  1259. const navigated = once(contents, 'did-finish-load');
  1260. await contents.executeJavaScript('document.querySelector("form").submit()');
  1261. await navigated;
  1262. 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/);
  1263. });
  1264. it('can receive streaming fetch upload', async () => {
  1265. protocol.handle('no-cors', (req) => new Response(req.body));
  1266. defer(() => { protocol.unhandle('no-cors'); });
  1267. await contents.loadURL('no-cors://foo');
  1268. const fetchBodyResult = await contents.executeJavaScript(`
  1269. const stream = new ReadableStream({
  1270. async start(controller) {
  1271. controller.enqueue('hello world');
  1272. controller.close();
  1273. },
  1274. }).pipeThrough(new TextEncoderStream());
  1275. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
  1276. `);
  1277. expect(fetchBodyResult).to.equal('hello world');
  1278. });
  1279. it('can receive streaming fetch upload when a webRequest handler is present', async () => {
  1280. session.defaultSession.webRequest.onBeforeRequest((details, cb) => {
  1281. console.log('webRequest', details.url, details.method);
  1282. cb({});
  1283. });
  1284. defer(() => {
  1285. session.defaultSession.webRequest.onBeforeRequest(null);
  1286. });
  1287. protocol.handle('no-cors', (req) => {
  1288. console.log('handle', req.url, req.method);
  1289. return new Response(req.body);
  1290. });
  1291. defer(() => { protocol.unhandle('no-cors'); });
  1292. await contents.loadURL('no-cors://foo');
  1293. const fetchBodyResult = await contents.executeJavaScript(`
  1294. const stream = new ReadableStream({
  1295. async start(controller) {
  1296. controller.enqueue('hello world');
  1297. controller.close();
  1298. },
  1299. }).pipeThrough(new TextEncoderStream());
  1300. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text())
  1301. `);
  1302. expect(fetchBodyResult).to.equal('hello world');
  1303. });
  1304. it('can receive an error from streaming fetch upload', async () => {
  1305. protocol.handle('no-cors', (req) => new Response(req.body));
  1306. defer(() => { protocol.unhandle('no-cors'); });
  1307. await contents.loadURL('no-cors://foo');
  1308. const fetchBodyResult = await contents.executeJavaScript(`
  1309. const stream = new ReadableStream({
  1310. async start(controller) {
  1311. controller.error('test')
  1312. },
  1313. });
  1314. fetch(location.href, {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
  1315. `);
  1316. expect(fetchBodyResult).to.be.an.instanceOf(Error);
  1317. });
  1318. it('gets an error from streaming fetch upload when the renderer dies', async () => {
  1319. let gotRequest: Function;
  1320. const receivedRequest = new Promise<Request>(resolve => { gotRequest = resolve; });
  1321. protocol.handle('no-cors', (req) => {
  1322. if (/fetch/.test(req.url)) gotRequest(req);
  1323. return new Response();
  1324. });
  1325. defer(() => { protocol.unhandle('no-cors'); });
  1326. await contents.loadURL('no-cors://foo');
  1327. contents.executeJavaScript(`
  1328. const stream = new ReadableStream({
  1329. async start(controller) {
  1330. window.controller = controller // no GC
  1331. },
  1332. });
  1333. fetch(location.href + '/fetch', {method: 'POST', body: stream, duplex: 'half'}).then(x => x.text()).catch(err => err)
  1334. `);
  1335. const req = await receivedRequest;
  1336. contents.destroy();
  1337. // Undo .destroy() for the next test
  1338. contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  1339. await expect(req.body!.getReader().read()).to.eventually.be.rejectedWith('net::ERR_FAILED');
  1340. });
  1341. it('can bypass intercepeted protocol handlers', async () => {
  1342. protocol.handle('http', () => new Response('custom'));
  1343. defer(() => { protocol.unhandle('http'); });
  1344. const server = http.createServer((req, res) => {
  1345. res.end('default');
  1346. });
  1347. defer(() => server.close());
  1348. const { url } = await listen(server);
  1349. expect(await net.fetch(url, { bypassCustomProtocolHandlers: true }).then(r => r.text())).to.equal('default');
  1350. });
  1351. it('bypassing custom protocol handlers also bypasses new protocols', async () => {
  1352. protocol.handle('app', () => new Response('custom'));
  1353. defer(() => { protocol.unhandle('app'); });
  1354. await expect(net.fetch('app://foo', { bypassCustomProtocolHandlers: true })).to.be.rejectedWith('net::ERR_UNKNOWN_URL_SCHEME');
  1355. });
  1356. it('can forward to the original handler', async () => {
  1357. protocol.handle('http', (req) => net.fetch(req, { bypassCustomProtocolHandlers: true }));
  1358. defer(() => { protocol.unhandle('http'); });
  1359. const server = http.createServer((req, res) => {
  1360. res.end('hello');
  1361. server.close();
  1362. });
  1363. const { url } = await listen(server);
  1364. await contents.loadURL(url);
  1365. expect(await contents.executeJavaScript('document.documentElement.textContent')).to.equal('hello');
  1366. });
  1367. it('supports sniffing mime type', async () => {
  1368. protocol.handle('http', async (req) => {
  1369. return net.fetch(req, { bypassCustomProtocolHandlers: true });
  1370. });
  1371. defer(() => { protocol.unhandle('http'); });
  1372. const server = http.createServer((req, res) => {
  1373. if (/html/.test(req.url ?? '')) { res.end('<!doctype html><body>hi'); } else { res.end('hi'); }
  1374. });
  1375. const { url } = await listen(server);
  1376. defer(() => server.close());
  1377. {
  1378. await contents.loadURL(url);
  1379. const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
  1380. expect(doc).to.match(/white-space: pre-wrap/);
  1381. }
  1382. {
  1383. await contents.loadURL(url + '?html');
  1384. const doc = await contents.executeJavaScript('document.documentElement.outerHTML');
  1385. expect(doc).to.equal('<html><head></head><body>hi</body></html>');
  1386. }
  1387. });
  1388. it('does not emit undefined chunks into the request body stream when uploading a stream', async () => {
  1389. protocol.handle('cors', async (request) => {
  1390. expect(request.body).to.be.an.instanceOf(webStream.ReadableStream);
  1391. for await (const value of request.body as webStream.ReadableStream<Uint8Array>) {
  1392. expect(value).to.not.be.undefined();
  1393. }
  1394. return new Response(undefined, { status: 200 });
  1395. });
  1396. defer(() => { protocol.unhandle('cors'); });
  1397. await contents.loadFile(path.resolve(fixturesPath, 'pages', 'base-page.html'));
  1398. contents.on('console-message', (e, level, message) => console.log(message));
  1399. const ok = await contents.executeJavaScript(`(async () => {
  1400. function wait(milliseconds) {
  1401. return new Promise((resolve) => setTimeout(resolve, milliseconds));
  1402. }
  1403. const stream = new ReadableStream({
  1404. async start(controller) {
  1405. await wait(4);
  1406. controller.enqueue('This ');
  1407. await wait(4);
  1408. controller.enqueue('is ');
  1409. await wait(4);
  1410. controller.enqueue('a ');
  1411. await wait(4);
  1412. controller.enqueue('slow ');
  1413. await wait(4);
  1414. controller.enqueue('request.');
  1415. controller.close();
  1416. }
  1417. }).pipeThrough(new TextEncoderStream());
  1418. return (await fetch('cors://url.invalid', { method: 'POST', body: stream, duplex: 'half' })).ok;
  1419. })()`);
  1420. expect(ok).to.be.true();
  1421. });
  1422. it('does not emit undefined chunks into the request body stream when uploading a file', async () => {
  1423. protocol.handle('cors', async (request) => {
  1424. expect(request.body).to.be.an.instanceOf(webStream.ReadableStream);
  1425. for await (const value of request.body as webStream.ReadableStream<Uint8Array>) {
  1426. expect(value).to.not.be.undefined();
  1427. }
  1428. return new Response(undefined, { status: 200 });
  1429. });
  1430. defer(() => { protocol.unhandle('cors'); });
  1431. await contents.loadFile(path.resolve(fixturesPath, 'pages', 'file-input.html'));
  1432. const { debugger: debug } = contents;
  1433. debug.attach();
  1434. try {
  1435. const { root: { nodeId } } = await debug.sendCommand('DOM.getDocument');
  1436. const { nodeId: inputNodeId } = await debug.sendCommand('DOM.querySelector', { nodeId, selector: 'input' });
  1437. await debug.sendCommand('DOM.setFileInputFiles', {
  1438. files: [path.join(fixturesPath, 'cat-spin.mp4')],
  1439. nodeId: inputNodeId
  1440. });
  1441. const ok = await contents.executeJavaScript(`(async () => {
  1442. const formData = new FormData();
  1443. formData.append("data", document.getElementById("file").files[0]);
  1444. return (await fetch('cors://url.invalid', { method: 'POST', body: formData })).ok;
  1445. })()`);
  1446. expect(ok).to.be.true();
  1447. } finally {
  1448. debug.detach();
  1449. }
  1450. });
  1451. it('filters an illegal "origin: null" header', async () => {
  1452. protocol.handle('http', (req) => {
  1453. expect(new Headers(req.headers).get('origin')).to.not.equal('null');
  1454. return new Response();
  1455. });
  1456. defer(() => { protocol.unhandle('http'); });
  1457. const filePath = path.join(fixturesPath, 'pages', 'form-with-data.html');
  1458. await contents.loadFile(filePath);
  1459. const loadPromise = new Promise((resolve, reject) => {
  1460. contents.once('did-finish-load', resolve);
  1461. contents.once('did-fail-load', (_, errorCode, errorDescription) =>
  1462. reject(new Error(`did-fail-load: ${errorCode} ${errorDescription}. See AssertionError for details.`))
  1463. );
  1464. });
  1465. await contents.executeJavaScript(`
  1466. const form = document.querySelector('form');
  1467. form.action = 'http://cors.invalid';
  1468. form.method = 'POST';
  1469. form.submit();
  1470. `);
  1471. await loadPromise;
  1472. });
  1473. it('does forward Blob chunks', async () => {
  1474. // we register the protocol on a separate session to validate the assumption
  1475. // that `getBlobData()` indeed returns the blob data from a global variable
  1476. const s = session.fromPartition('protocol-handle-forwards-blob-chunks');
  1477. s.protocol.handle('cors', async (request) => {
  1478. expect(request.body).to.be.an.instanceOf(webStream.ReadableStream);
  1479. return new Response(
  1480. `hello to ${await streamConsumers.text(request.body as webStream.ReadableStream<Uint8Array>)}`,
  1481. { status: 200 }
  1482. );
  1483. });
  1484. defer(() => { s.protocol.unhandle('cors'); });
  1485. const w = new BrowserWindow({ show: false, webPreferences: { session: s } });
  1486. await w.webContents.loadFile(path.resolve(fixturesPath, 'pages', 'base-page.html'));
  1487. const response = await w.webContents.executeJavaScript(`(async () => {
  1488. const body = new Blob(["it's-a ", 'me! ', 'Mario!'], { type: 'text/plain' });
  1489. return await (await fetch('cors://url.invalid', { method: 'POST', body })).text();
  1490. })()`);
  1491. expect(response).to.be.string('hello to it\'s-a me! Mario!');
  1492. });
  1493. // TODO(nornagon): this test doesn't pass on Linux currently, investigate.
  1494. ifit(process.platform !== 'linux')('is fast', async () => {
  1495. // 128 MB of spaces.
  1496. const chunk = new Uint8Array(128 * 1024 * 1024);
  1497. chunk.fill(' '.charCodeAt(0));
  1498. const server = http.createServer((req, res) => {
  1499. // The sniffed mime type for the space-filled chunk will be
  1500. // text/plain, which chews up all its performance in the renderer
  1501. // trying to wrap lines. Setting content-type to text/html measures
  1502. // something closer to just the raw cost of getting the bytes over
  1503. // the wire.
  1504. res.setHeader('content-type', 'text/html');
  1505. res.end(chunk);
  1506. });
  1507. defer(() => server.close());
  1508. const { url } = await listen(server);
  1509. const rawTime = await (async () => {
  1510. await contents.loadURL(url); // warm
  1511. const begin = Date.now();
  1512. await contents.loadURL(url);
  1513. const end = Date.now();
  1514. return end - begin;
  1515. })();
  1516. // Fetching through an intercepted handler should not be too much slower
  1517. // than it would be if the protocol hadn't been intercepted.
  1518. protocol.handle('http', async (req) => {
  1519. return net.fetch(req, { bypassCustomProtocolHandlers: true });
  1520. });
  1521. defer(() => { protocol.unhandle('http'); });
  1522. const interceptedTime = await (async () => {
  1523. const begin = Date.now();
  1524. await contents.loadURL(url);
  1525. const end = Date.now();
  1526. return end - begin;
  1527. })();
  1528. expect(interceptedTime).to.be.lessThan(rawTime * 1.6);
  1529. });
  1530. });
  1531. });