api-protocol-spec.ts 61 KB

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