api-web-request-spec.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. import { ipcMain, protocol, session, WebContents, webContents } from 'electron/main';
  2. import { expect } from 'chai';
  3. import * as WebSocket from 'ws';
  4. import { once } from 'node:events';
  5. import * as fs from 'node:fs';
  6. import * as http from 'node:http';
  7. import * as http2 from 'node:http2';
  8. import { Socket } from 'node:net';
  9. import * as path from 'node:path';
  10. import * as qs from 'node:querystring';
  11. import { ReadableStream } from 'node:stream/web';
  12. import * as url from 'node:url';
  13. import { listen, defer } from './lib/spec-helpers';
  14. const fixturesPath = path.resolve(__dirname, 'fixtures');
  15. describe('webRequest module', () => {
  16. const ses = session.defaultSession;
  17. const server = http.createServer((req, res) => {
  18. if (req.url === '/serverRedirect') {
  19. res.statusCode = 301;
  20. res.setHeader('Location', 'http://' + req.rawHeaders[1]);
  21. res.end();
  22. } else if (req.url === '/contentDisposition') {
  23. res.writeHead(200, [
  24. 'content-disposition',
  25. Buffer.from('attachment; filename=aa中aa.txt').toString('binary')
  26. ]);
  27. const content = req.url;
  28. res.end(content);
  29. } else {
  30. res.setHeader('Custom', ['Header']);
  31. let content = req.url;
  32. if (req.headers.accept === '*/*;test/header') {
  33. content += 'header/received';
  34. }
  35. if (req.headers.origin === 'http://new-origin') {
  36. content += 'new/origin';
  37. }
  38. res.end(content);
  39. }
  40. });
  41. let defaultURL: string;
  42. let http2URL: string;
  43. const certPath = path.join(fixturesPath, 'certificates');
  44. const h2server = http2.createSecureServer({
  45. key: fs.readFileSync(path.join(certPath, 'server.key')),
  46. cert: fs.readFileSync(path.join(certPath, 'server.pem'))
  47. }, async (req, res) => {
  48. if (req.method === 'POST') {
  49. const chunks = [];
  50. for await (const chunk of req) chunks.push(chunk);
  51. res.end(Buffer.concat(chunks).toString('utf8'));
  52. } else {
  53. res.end('<html></html>');
  54. }
  55. });
  56. before(async () => {
  57. protocol.registerStringProtocol('cors', (req, cb) => cb(''));
  58. defaultURL = (await listen(server)).url + '/';
  59. http2URL = (await listen(h2server)).url + '/';
  60. console.log(http2URL);
  61. });
  62. after(() => {
  63. server.close();
  64. h2server.close();
  65. protocol.unregisterProtocol('cors');
  66. });
  67. let contents: WebContents;
  68. // NB. sandbox: true is used because it makes navigations much (~8x) faster.
  69. before(async () => {
  70. contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  71. // const w = new BrowserWindow({webPreferences: {sandbox: true}})
  72. // contents = w.webContents
  73. await contents.loadFile(path.join(fixturesPath, 'pages', 'fetch.html'));
  74. });
  75. after(() => contents.destroy());
  76. async function ajax (url: string, options = {}) {
  77. return contents.executeJavaScript(`ajax("${url}", ${JSON.stringify(options)})`);
  78. }
  79. describe('webRequest.onBeforeRequest', () => {
  80. afterEach(() => {
  81. ses.webRequest.onBeforeRequest(null);
  82. });
  83. const cancel = (details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => {
  84. callback({ cancel: true });
  85. };
  86. it('can cancel the request', async () => {
  87. ses.webRequest.onBeforeRequest(cancel);
  88. await expect(ajax(defaultURL)).to.eventually.be.rejected();
  89. });
  90. it('matches all requests when no filters are defined', async () => {
  91. ses.webRequest.onBeforeRequest(cancel);
  92. await expect(ajax(`${defaultURL}nofilter/test`)).to.eventually.be.rejected();
  93. await expect(ajax(`${defaultURL}nofilter2/test`)).to.eventually.be.rejected();
  94. });
  95. it('can filter URLs', async () => {
  96. const filter = { urls: [defaultURL + 'filter/*'] };
  97. ses.webRequest.onBeforeRequest(filter, cancel);
  98. const { data } = await ajax(`${defaultURL}nofilter/test`);
  99. expect(data).to.equal('/nofilter/test');
  100. await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
  101. });
  102. it('can filter all URLs with syntax <all_urls>', async () => {
  103. const filter = { urls: ['<all_urls>'] };
  104. ses.webRequest.onBeforeRequest(filter, cancel);
  105. await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
  106. await expect(ajax(`${defaultURL}nofilter/test`)).to.eventually.be.rejected();
  107. });
  108. it('can filter URLs with overlapping patterns of urls and excludeUrls', async () => {
  109. // If filter matches both urls and excludeUrls, it should be excluded.
  110. const filter = { urls: [defaultURL + 'filter/*'], excludeUrls: [defaultURL + 'filter/test'] };
  111. ses.webRequest.onBeforeRequest(filter, cancel);
  112. const { data } = await ajax(`${defaultURL}filter/test`);
  113. expect(data).to.equal('/filter/test');
  114. });
  115. it('can filter URLs with multiple excludeUrls patterns', async () => {
  116. const filter = { urls: [defaultURL + 'filter/*'], excludeUrls: [defaultURL + 'filter/exclude1/*', defaultURL + 'filter/exclude2/*'] };
  117. ses.webRequest.onBeforeRequest(filter, cancel);
  118. expect((await ajax(`${defaultURL}filter/exclude1/test`)).data).to.equal('/filter/exclude1/test');
  119. expect((await ajax(`${defaultURL}filter/exclude2/test`)).data).to.equal('/filter/exclude2/test');
  120. // expect non-excluded URL to pass filter
  121. await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
  122. });
  123. it('can filter URLs with empty excludeUrls', async () => {
  124. const filter = { urls: [defaultURL + 'filter/*'], excludeUrls: [] };
  125. ses.webRequest.onBeforeRequest(filter, cancel);
  126. await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
  127. });
  128. it('can filter URLs and types', async () => {
  129. const filter1: Electron.WebRequestFilter = { urls: [defaultURL + 'filter/*'], types: ['xhr'] };
  130. ses.webRequest.onBeforeRequest(filter1, cancel);
  131. const { data } = await ajax(`${defaultURL}nofilter/test`);
  132. expect(data).to.equal('/nofilter/test');
  133. await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
  134. const filter2: Electron.WebRequestFilter = { urls: [defaultURL + 'filter/*'], types: ['stylesheet'] };
  135. ses.webRequest.onBeforeRequest(filter2, cancel);
  136. expect((await ajax(`${defaultURL}nofilter/test`)).data).to.equal('/nofilter/test');
  137. expect((await ajax(`${defaultURL}filter/test`)).data).to.equal('/filter/test');
  138. });
  139. it('can filter URLs, excludeUrls and types', async () => {
  140. const filter1: Electron.WebRequestFilter = { urls: [defaultURL + 'filter/*'], excludeUrls: [defaultURL + 'exclude/*'], types: ['xhr'] };
  141. ses.webRequest.onBeforeRequest(filter1, cancel);
  142. expect((await ajax(`${defaultURL}nofilter/test`)).data).to.equal('/nofilter/test');
  143. expect((await ajax(`${defaultURL}exclude/test`)).data).to.equal('/exclude/test');
  144. await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
  145. const filter2: Electron.WebRequestFilter = { urls: [defaultURL + 'filter/*'], excludeUrls: [defaultURL + 'exclude/*'], types: ['stylesheet'] };
  146. ses.webRequest.onBeforeRequest(filter2, cancel);
  147. expect((await ajax(`${defaultURL}nofilter/test`)).data).to.equal('/nofilter/test');
  148. expect((await ajax(`${defaultURL}filter/test`)).data).to.equal('/filter/test');
  149. expect((await ajax(`${defaultURL}exclude/test`)).data).to.equal('/exclude/test');
  150. });
  151. it('receives details object', async () => {
  152. ses.webRequest.onBeforeRequest((details, callback) => {
  153. expect(details.id).to.be.a('number');
  154. expect(details.timestamp).to.be.a('number');
  155. expect(details.webContentsId).to.be.a('number');
  156. expect(details.webContents).to.be.an('object');
  157. expect(details.webContents!.id).to.equal(details.webContentsId);
  158. expect(details.frame).to.be.an('object');
  159. expect(details.url).to.be.a('string').that.is.equal(defaultURL);
  160. expect(details.method).to.be.a('string').that.is.equal('GET');
  161. expect(details.resourceType).to.be.a('string').that.is.equal('xhr');
  162. expect(details.uploadData).to.be.undefined();
  163. callback({});
  164. });
  165. const { data } = await ajax(defaultURL);
  166. expect(data).to.equal('/');
  167. });
  168. it('receives post data in details object', async () => {
  169. const postData = {
  170. name: 'post test',
  171. type: 'string'
  172. };
  173. ses.webRequest.onBeforeRequest((details, callback) => {
  174. expect(details.url).to.equal(defaultURL);
  175. expect(details.method).to.equal('POST');
  176. expect(details.uploadData).to.have.lengthOf(1);
  177. const data = qs.parse(details.uploadData[0].bytes.toString());
  178. expect(data).to.deep.equal(postData);
  179. callback({ cancel: true });
  180. });
  181. await expect(ajax(defaultURL, {
  182. method: 'POST',
  183. body: qs.stringify(postData)
  184. })).to.eventually.be.rejected();
  185. });
  186. it('can redirect the request', async () => {
  187. ses.webRequest.onBeforeRequest((details, callback) => {
  188. if (details.url === defaultURL) {
  189. callback({ redirectURL: `${defaultURL}redirect` });
  190. } else {
  191. callback({});
  192. }
  193. });
  194. const { data } = await ajax(defaultURL);
  195. expect(data).to.equal('/redirect');
  196. });
  197. it('does not crash for redirects', async () => {
  198. ses.webRequest.onBeforeRequest((details, callback) => {
  199. callback({ cancel: false });
  200. });
  201. await ajax(defaultURL + 'serverRedirect');
  202. await ajax(defaultURL + 'serverRedirect');
  203. });
  204. it('works with file:// protocol', async () => {
  205. ses.webRequest.onBeforeRequest((details, callback) => {
  206. callback({ cancel: true });
  207. });
  208. const fileURL = url.format({
  209. pathname: path.join(fixturesPath, 'blank.html').replaceAll('\\', '/'),
  210. protocol: 'file',
  211. slashes: true
  212. });
  213. await expect(ajax(fileURL)).to.eventually.be.rejected();
  214. });
  215. it('can handle a streaming upload', async () => {
  216. // Streaming fetch uploads are only supported on HTTP/2, which is only
  217. // supported over TLS, so...
  218. session.defaultSession.setCertificateVerifyProc((req, cb) => cb(0));
  219. defer(() => {
  220. session.defaultSession.setCertificateVerifyProc(null);
  221. });
  222. const contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  223. defer(() => contents.close());
  224. await contents.loadURL(http2URL);
  225. ses.webRequest.onBeforeRequest((details, callback) => {
  226. callback({});
  227. });
  228. const result = await contents.executeJavaScript(`
  229. const stream = new ReadableStream({
  230. async start(controller) {
  231. controller.enqueue('hello world');
  232. controller.close();
  233. },
  234. }).pipeThrough(new TextEncoderStream());
  235. fetch("${http2URL}", {
  236. method: 'POST',
  237. body: stream,
  238. duplex: 'half',
  239. }).then(r => r.text())
  240. `);
  241. expect(result).to.equal('hello world');
  242. });
  243. it('can handle a streaming upload if the uploadData is read', async () => {
  244. // Streaming fetch uploads are only supported on HTTP/2, which is only
  245. // supported over TLS, so...
  246. session.defaultSession.setCertificateVerifyProc((req, cb) => cb(0));
  247. defer(() => {
  248. session.defaultSession.setCertificateVerifyProc(null);
  249. });
  250. const contents = (webContents as typeof ElectronInternal.WebContents).create({ sandbox: true });
  251. defer(() => contents.close());
  252. await contents.loadURL(http2URL);
  253. function makeStreamFromPipe (pipe: any): ReadableStream {
  254. const buf = new Uint8Array(1024 * 1024 /* 1 MB */);
  255. return new ReadableStream({
  256. async pull (controller) {
  257. try {
  258. const rv = await pipe.read(buf);
  259. if (rv > 0) {
  260. controller.enqueue(buf.subarray(0, rv));
  261. } else {
  262. controller.close();
  263. }
  264. } catch (e) {
  265. controller.error(e);
  266. }
  267. }
  268. });
  269. }
  270. ses.webRequest.onBeforeRequest(async (details, callback) => {
  271. const chunks = [];
  272. for await (const chunk of makeStreamFromPipe((details.uploadData[0] as any).body)) { chunks.push(chunk); }
  273. callback({});
  274. });
  275. const result = await contents.executeJavaScript(`
  276. const stream = new ReadableStream({
  277. async start(controller) {
  278. controller.enqueue('hello world');
  279. controller.close();
  280. },
  281. }).pipeThrough(new TextEncoderStream());
  282. fetch("${http2URL}", {
  283. method: 'POST',
  284. body: stream,
  285. duplex: 'half',
  286. }).then(r => r.text())
  287. `);
  288. // NOTE: since the upload stream was consumed by the onBeforeRequest
  289. // handler, it can't be used again to upload to the actual server.
  290. // This is a limitation of the WebRequest API.
  291. expect(result).to.equal('');
  292. });
  293. });
  294. describe('webRequest.onBeforeSendHeaders', () => {
  295. afterEach(() => {
  296. ses.webRequest.onBeforeSendHeaders(null);
  297. ses.webRequest.onSendHeaders(null);
  298. });
  299. it('receives details object', async () => {
  300. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  301. expect(details.requestHeaders).to.be.an('object');
  302. expect(details.requestHeaders['Foo.Bar']).to.equal('baz');
  303. callback({});
  304. });
  305. const { data } = await ajax(defaultURL, { headers: { 'Foo.Bar': 'baz' } });
  306. expect(data).to.equal('/');
  307. });
  308. it('can change the request headers', async () => {
  309. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  310. const requestHeaders = details.requestHeaders;
  311. requestHeaders.Accept = '*/*;test/header';
  312. callback({ requestHeaders });
  313. });
  314. const { data } = await ajax(defaultURL);
  315. expect(data).to.equal('/header/received');
  316. });
  317. it('can change the request headers on a custom protocol redirect', async () => {
  318. protocol.registerStringProtocol('no-cors', (req, callback) => {
  319. if (req.url === 'no-cors://fake-host/redirect') {
  320. callback({
  321. statusCode: 302,
  322. headers: {
  323. Location: 'no-cors://fake-host'
  324. }
  325. });
  326. } else {
  327. let content = '';
  328. if (req.headers.Accept === '*/*;test/header') {
  329. content = 'header-received';
  330. }
  331. callback(content);
  332. }
  333. });
  334. // Note that we need to do navigation every time after a protocol is
  335. // registered or unregistered, otherwise the new protocol won't be
  336. // recognized by current page when NetworkService is used.
  337. await contents.loadFile(path.join(__dirname, 'fixtures', 'pages', 'fetch.html'));
  338. try {
  339. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  340. const requestHeaders = details.requestHeaders;
  341. requestHeaders.Accept = '*/*;test/header';
  342. callback({ requestHeaders });
  343. });
  344. const { data } = await ajax('no-cors://fake-host/redirect');
  345. expect(data).to.equal('header-received');
  346. } finally {
  347. protocol.unregisterProtocol('no-cors');
  348. }
  349. });
  350. it('can change request origin', async () => {
  351. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  352. const requestHeaders = details.requestHeaders;
  353. requestHeaders.Origin = 'http://new-origin';
  354. callback({ requestHeaders });
  355. });
  356. const { data } = await ajax(defaultURL);
  357. expect(data).to.equal('/new/origin');
  358. });
  359. it('can capture CORS requests', async () => {
  360. let called = false;
  361. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  362. called = true;
  363. callback({ requestHeaders: details.requestHeaders });
  364. });
  365. await ajax('cors://host');
  366. expect(called).to.be.true();
  367. });
  368. it('resets the whole headers', async () => {
  369. const requestHeaders = {
  370. Test: 'header'
  371. };
  372. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  373. callback({ requestHeaders });
  374. });
  375. ses.webRequest.onSendHeaders((details) => {
  376. expect(details.requestHeaders).to.deep.equal(requestHeaders);
  377. });
  378. await ajax(defaultURL);
  379. });
  380. it('leaves headers unchanged when no requestHeaders in callback', async () => {
  381. let originalRequestHeaders: Record<string, string>;
  382. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  383. originalRequestHeaders = details.requestHeaders;
  384. callback({});
  385. });
  386. ses.webRequest.onSendHeaders((details) => {
  387. expect(details.requestHeaders).to.deep.equal(originalRequestHeaders);
  388. });
  389. await ajax(defaultURL);
  390. });
  391. it('works with file:// protocol', async () => {
  392. const requestHeaders = {
  393. Test: 'header'
  394. };
  395. let onSendHeadersCalled = false;
  396. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  397. callback({ requestHeaders });
  398. });
  399. ses.webRequest.onSendHeaders((details) => {
  400. expect(details.requestHeaders).to.deep.equal(requestHeaders);
  401. onSendHeadersCalled = true;
  402. });
  403. await ajax(url.format({
  404. pathname: path.join(fixturesPath, 'blank.html').replaceAll('\\', '/'),
  405. protocol: 'file',
  406. slashes: true
  407. }));
  408. expect(onSendHeadersCalled).to.be.true();
  409. });
  410. });
  411. describe('webRequest.onSendHeaders', () => {
  412. afterEach(() => {
  413. ses.webRequest.onSendHeaders(null);
  414. });
  415. it('receives details object', async () => {
  416. ses.webRequest.onSendHeaders((details) => {
  417. expect(details.requestHeaders).to.be.an('object');
  418. });
  419. const { data } = await ajax(defaultURL);
  420. expect(data).to.equal('/');
  421. });
  422. });
  423. describe('webRequest.onHeadersReceived', () => {
  424. afterEach(() => {
  425. ses.webRequest.onHeadersReceived(null);
  426. });
  427. it('receives details object', async () => {
  428. ses.webRequest.onHeadersReceived((details, callback) => {
  429. expect(details.statusLine).to.equal('HTTP/1.1 200 OK');
  430. expect(details.statusCode).to.equal(200);
  431. expect(details.responseHeaders!.Custom).to.deep.equal(['Header']);
  432. callback({});
  433. });
  434. const { data } = await ajax(defaultURL);
  435. expect(data).to.equal('/');
  436. });
  437. it('can change the response header', async () => {
  438. ses.webRequest.onHeadersReceived((details, callback) => {
  439. const responseHeaders = details.responseHeaders!;
  440. responseHeaders.Custom = ['Changed'] as any;
  441. callback({ responseHeaders });
  442. });
  443. const { headers } = await ajax(defaultURL);
  444. expect(headers).to.to.have.property('custom', 'Changed');
  445. });
  446. it('can change response origin', async () => {
  447. ses.webRequest.onHeadersReceived((details, callback) => {
  448. const responseHeaders = details.responseHeaders!;
  449. responseHeaders['access-control-allow-origin'] = ['http://new-origin'] as any;
  450. callback({ responseHeaders });
  451. });
  452. const { headers } = await ajax(defaultURL);
  453. expect(headers).to.to.have.property('access-control-allow-origin', 'http://new-origin');
  454. });
  455. it('can change headers of CORS responses', async () => {
  456. ses.webRequest.onHeadersReceived((details, callback) => {
  457. const responseHeaders = details.responseHeaders!;
  458. responseHeaders.Custom = ['Changed'] as any;
  459. callback({ responseHeaders });
  460. });
  461. const { headers } = await ajax('cors://host');
  462. expect(headers).to.to.have.property('custom', 'Changed');
  463. });
  464. it('does not change header by default', async () => {
  465. ses.webRequest.onHeadersReceived((details, callback) => {
  466. callback({});
  467. });
  468. const { data, headers } = await ajax(defaultURL);
  469. expect(headers).to.to.have.property('custom', 'Header');
  470. expect(data).to.equal('/');
  471. });
  472. it('does not change content-disposition header by default', async () => {
  473. ses.webRequest.onHeadersReceived((details, callback) => {
  474. expect(details.responseHeaders!['content-disposition']).to.deep.equal(['attachment; filename=aa中aa.txt']);
  475. callback({});
  476. });
  477. const { data, headers } = await ajax(defaultURL + 'contentDisposition');
  478. const disposition = Buffer.from('attachment; filename=aa中aa.txt').toString('binary');
  479. expect(headers).to.to.have.property('content-disposition', disposition);
  480. expect(data).to.equal('/contentDisposition');
  481. });
  482. it('follows server redirect', async () => {
  483. ses.webRequest.onHeadersReceived((details, callback) => {
  484. const responseHeaders = details.responseHeaders;
  485. callback({ responseHeaders });
  486. });
  487. const { headers } = await ajax(defaultURL + 'serverRedirect');
  488. expect(headers).to.to.have.property('custom', 'Header');
  489. });
  490. it('can change the header status', async () => {
  491. ses.webRequest.onHeadersReceived((details, callback) => {
  492. const responseHeaders = details.responseHeaders;
  493. callback({
  494. responseHeaders,
  495. statusLine: 'HTTP/1.1 404 Not Found'
  496. });
  497. });
  498. const { headers } = await ajax(defaultURL);
  499. expect(headers).to.to.have.property('custom', 'Header');
  500. });
  501. });
  502. describe('webRequest.onResponseStarted', () => {
  503. afterEach(() => {
  504. ses.webRequest.onResponseStarted(null);
  505. });
  506. it('receives details object', async () => {
  507. ses.webRequest.onResponseStarted((details) => {
  508. expect(details.fromCache).to.be.a('boolean');
  509. expect(details.statusLine).to.equal('HTTP/1.1 200 OK');
  510. expect(details.statusCode).to.equal(200);
  511. expect(details.responseHeaders!.Custom).to.deep.equal(['Header']);
  512. });
  513. const { data, headers } = await ajax(defaultURL);
  514. expect(headers).to.to.have.property('custom', 'Header');
  515. expect(data).to.equal('/');
  516. });
  517. });
  518. describe('webRequest.onBeforeRedirect', () => {
  519. afterEach(() => {
  520. ses.webRequest.onBeforeRedirect(null);
  521. ses.webRequest.onBeforeRequest(null);
  522. });
  523. it('receives details object', async () => {
  524. const redirectURL = defaultURL + 'redirect';
  525. ses.webRequest.onBeforeRequest((details, callback) => {
  526. if (details.url === defaultURL) {
  527. callback({ redirectURL });
  528. } else {
  529. callback({});
  530. }
  531. });
  532. ses.webRequest.onBeforeRedirect((details) => {
  533. expect(details.fromCache).to.be.a('boolean');
  534. expect(details.statusLine).to.equal('HTTP/1.1 307 Internal Redirect');
  535. expect(details.statusCode).to.equal(307);
  536. expect(details.redirectURL).to.equal(redirectURL);
  537. });
  538. const { data } = await ajax(defaultURL);
  539. expect(data).to.equal('/redirect');
  540. });
  541. });
  542. describe('webRequest.onCompleted', () => {
  543. afterEach(() => {
  544. ses.webRequest.onCompleted(null);
  545. });
  546. it('receives details object', async () => {
  547. ses.webRequest.onCompleted((details) => {
  548. expect(details.fromCache).to.be.a('boolean');
  549. expect(details.statusLine).to.equal('HTTP/1.1 200 OK');
  550. expect(details.statusCode).to.equal(200);
  551. });
  552. const { data } = await ajax(defaultURL);
  553. expect(data).to.equal('/');
  554. });
  555. });
  556. describe('webRequest.onErrorOccurred', () => {
  557. afterEach(() => {
  558. ses.webRequest.onErrorOccurred(null);
  559. ses.webRequest.onBeforeRequest(null);
  560. });
  561. it('receives details object', async () => {
  562. ses.webRequest.onBeforeRequest((details, callback) => {
  563. callback({ cancel: true });
  564. });
  565. ses.webRequest.onErrorOccurred((details) => {
  566. expect(details.error).to.equal('net::ERR_BLOCKED_BY_CLIENT');
  567. });
  568. await expect(ajax(defaultURL)).to.eventually.be.rejected();
  569. });
  570. });
  571. describe('WebSocket connections', () => {
  572. it('can be proxyed', async () => {
  573. // Setup server.
  574. const reqHeaders : { [key: string] : any } = {};
  575. let server = http.createServer((req, res) => {
  576. reqHeaders[req.url!] = req.headers;
  577. res.setHeader('foo1', 'bar1');
  578. res.end('ok');
  579. });
  580. let wss = new WebSocket.Server({ noServer: true });
  581. wss.on('connection', function connection (ws) {
  582. ws.on('message', function incoming (message) {
  583. if (message === 'foo') {
  584. ws.send('bar');
  585. }
  586. });
  587. });
  588. server.on('upgrade', function upgrade (request, socket, head) {
  589. const pathname = new URL(request.url!, `http://${request.headers.host}`).pathname;
  590. if (pathname === '/websocket') {
  591. reqHeaders[request.url!] = request.headers;
  592. wss.handleUpgrade(request, socket as Socket, head, function done (ws) {
  593. wss.emit('connection', ws, request);
  594. });
  595. }
  596. });
  597. // Start server.
  598. const { port } = await listen(server);
  599. // Use a separate session for testing.
  600. const ses = session.fromPartition('WebRequestWebSocket');
  601. // Setup listeners.
  602. const receivedHeaders : { [key: string] : any } = {};
  603. ses.webRequest.onBeforeSendHeaders((details, callback) => {
  604. details.requestHeaders.foo = 'bar';
  605. callback({ requestHeaders: details.requestHeaders });
  606. });
  607. ses.webRequest.onHeadersReceived((details, callback) => {
  608. const pathname = new URL(details.url).pathname;
  609. receivedHeaders[pathname] = details.responseHeaders;
  610. callback({ cancel: false });
  611. });
  612. ses.webRequest.onResponseStarted((details) => {
  613. if (details.url.startsWith('ws://')) {
  614. expect(details.responseHeaders!.Connection[0]).be.equal('Upgrade');
  615. } else if (details.url.startsWith('http')) {
  616. expect(details.responseHeaders!.foo1[0]).be.equal('bar1');
  617. }
  618. });
  619. ses.webRequest.onSendHeaders((details) => {
  620. if (details.url.startsWith('ws://')) {
  621. expect(details.requestHeaders.foo).be.equal('bar');
  622. expect(details.requestHeaders.Upgrade).be.equal('websocket');
  623. } else if (details.url.startsWith('http')) {
  624. expect(details.requestHeaders.foo).be.equal('bar');
  625. }
  626. });
  627. ses.webRequest.onCompleted((details) => {
  628. if (details.url.startsWith('ws://')) {
  629. expect(details.error).be.equal('net::ERR_WS_UPGRADE');
  630. } else if (details.url.startsWith('http')) {
  631. expect(details.error).be.equal('net::OK');
  632. }
  633. });
  634. const contents = (webContents as typeof ElectronInternal.WebContents).create({
  635. session: ses,
  636. nodeIntegration: true,
  637. webSecurity: false,
  638. contextIsolation: false
  639. });
  640. // Cleanup.
  641. defer(() => {
  642. contents.destroy();
  643. server.close();
  644. server = null as unknown as http.Server;
  645. wss.close();
  646. wss = null as unknown as WebSocket.Server;
  647. ses.webRequest.onBeforeRequest(null);
  648. ses.webRequest.onBeforeSendHeaders(null);
  649. ses.webRequest.onHeadersReceived(null);
  650. ses.webRequest.onResponseStarted(null);
  651. ses.webRequest.onSendHeaders(null);
  652. ses.webRequest.onCompleted(null);
  653. });
  654. contents.loadFile(path.join(fixturesPath, 'api', 'webrequest.html'), { query: { port: `${port}` } });
  655. await once(ipcMain, 'websocket-success');
  656. expect(receivedHeaders['/websocket'].Upgrade[0]).to.equal('websocket');
  657. expect(receivedHeaders['/'].foo1[0]).to.equal('bar1');
  658. expect(reqHeaders['/websocket'].foo).to.equal('bar');
  659. expect(reqHeaders['/'].foo).to.equal('bar');
  660. });
  661. });
  662. });