api-web-request-spec.ts 24 KB

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