protocol.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { ProtocolRequest, session } from 'electron/main';
  2. import { createReadStream } from 'fs';
  3. import { Readable } from 'stream';
  4. import { ReadableStream } from 'stream/web';
  5. // Global protocol APIs.
  6. const { registerSchemesAsPrivileged, getStandardSchemes, Protocol } = process._linkedBinding('electron_browser_protocol');
  7. const ERR_FAILED = -2;
  8. const ERR_UNEXPECTED = -9;
  9. const isBuiltInScheme = (scheme: string) => ['http', 'https', 'file'].includes(scheme);
  10. function makeStreamFromPipe (pipe: any): ReadableStream {
  11. const buf = new Uint8Array(1024 * 1024 /* 1 MB */);
  12. return new ReadableStream({
  13. async pull (controller) {
  14. try {
  15. const rv = await pipe.read(buf);
  16. if (rv > 0) {
  17. controller.enqueue(buf.subarray(0, rv));
  18. } else {
  19. controller.close();
  20. }
  21. } catch (e) {
  22. controller.error(e);
  23. }
  24. }
  25. });
  26. }
  27. function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): RequestInit['body'] {
  28. if (!uploadData) return null;
  29. // Optimization: skip creating a stream if the request is just a single buffer.
  30. if (uploadData.length === 1 && (uploadData[0] as any).type === 'rawData') return uploadData[0].bytes;
  31. const chunks = [...uploadData] as any[]; // TODO: types are wrong
  32. let current: ReadableStreamDefaultReader | null = null;
  33. return new ReadableStream({
  34. pull (controller) {
  35. if (current) {
  36. current.read().then(({ done, value }) => {
  37. controller.enqueue(value);
  38. if (done) current = null;
  39. }, (err) => {
  40. controller.error(err);
  41. });
  42. } else {
  43. if (!chunks.length) { return controller.close(); }
  44. const chunk = chunks.shift()!;
  45. if (chunk.type === 'rawData') { controller.enqueue(chunk.bytes); } else if (chunk.type === 'file') {
  46. current = Readable.toWeb(createReadStream(chunk.filePath, { start: chunk.offset ?? 0, end: chunk.length >= 0 ? chunk.offset + chunk.length : undefined })).getReader();
  47. this.pull!(controller);
  48. } else if (chunk.type === 'stream') {
  49. current = makeStreamFromPipe(chunk.body).getReader();
  50. this.pull!(controller);
  51. }
  52. }
  53. }
  54. }) as RequestInit['body'];
  55. }
  56. // TODO(codebytere): Use Object.hasOwn() once we update to ECMAScript 2022.
  57. function validateResponse (res: Response) {
  58. if (!res || typeof res !== 'object') return false;
  59. if (res.type === 'error') return true;
  60. const exists = (key: string) => Object.hasOwn(res, key);
  61. if (exists('status') && typeof res.status !== 'number') return false;
  62. if (exists('statusText') && typeof res.statusText !== 'string') return false;
  63. if (exists('headers') && typeof res.headers !== 'object') return false;
  64. if (exists('body')) {
  65. if (typeof res.body !== 'object') return false;
  66. if (res.body !== null && !(res.body instanceof ReadableStream)) return false;
  67. }
  68. return true;
  69. }
  70. Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, handler: (req: Request) => Response | Promise<Response>) {
  71. const register = isBuiltInScheme(scheme) ? this.interceptProtocol : this.registerProtocol;
  72. const success = register.call(this, scheme, async (preq: ProtocolRequest, cb: any) => {
  73. try {
  74. const body = convertToRequestBody(preq.uploadData);
  75. const req = new Request(preq.url, {
  76. headers: preq.headers,
  77. method: preq.method,
  78. referrer: preq.referrer,
  79. body,
  80. duplex: body instanceof ReadableStream ? 'half' : undefined
  81. } as any);
  82. const res = await handler(req);
  83. if (!validateResponse(res)) {
  84. return cb({ error: ERR_UNEXPECTED });
  85. } else if (res.type === 'error') {
  86. cb({ error: ERR_FAILED });
  87. } else {
  88. cb({
  89. data: res.body ? Readable.fromWeb(res.body as ReadableStream<ArrayBufferView>) : null,
  90. headers: res.headers ? Object.fromEntries(res.headers) : {},
  91. statusCode: res.status,
  92. statusText: res.statusText,
  93. mimeType: (res as any).__original_resp?._responseHead?.mimeType
  94. });
  95. }
  96. } catch (e) {
  97. console.error(e);
  98. cb({ error: ERR_UNEXPECTED });
  99. }
  100. });
  101. if (!success) throw new Error(`Failed to register protocol: ${scheme}`);
  102. };
  103. Protocol.prototype.unhandle = function (this: Electron.Protocol, scheme: string) {
  104. const unregister = isBuiltInScheme(scheme) ? this.uninterceptProtocol : this.unregisterProtocol;
  105. if (!unregister.call(this, scheme)) { throw new Error(`Failed to unhandle protocol: ${scheme}`); }
  106. };
  107. Protocol.prototype.isProtocolHandled = function (this: Electron.Protocol, scheme: string) {
  108. const isRegistered = isBuiltInScheme(scheme) ? this.isProtocolIntercepted : this.isProtocolRegistered;
  109. return isRegistered.call(this, scheme);
  110. };
  111. const protocol = {
  112. registerSchemesAsPrivileged,
  113. getStandardSchemes,
  114. registerStringProtocol: (...args) => session.defaultSession.protocol.registerStringProtocol(...args),
  115. registerBufferProtocol: (...args) => session.defaultSession.protocol.registerBufferProtocol(...args),
  116. registerStreamProtocol: (...args) => session.defaultSession.protocol.registerStreamProtocol(...args),
  117. registerFileProtocol: (...args) => session.defaultSession.protocol.registerFileProtocol(...args),
  118. registerHttpProtocol: (...args) => session.defaultSession.protocol.registerHttpProtocol(...args),
  119. registerProtocol: (...args) => session.defaultSession.protocol.registerProtocol(...args),
  120. unregisterProtocol: (...args) => session.defaultSession.protocol.unregisterProtocol(...args),
  121. isProtocolRegistered: (...args) => session.defaultSession.protocol.isProtocolRegistered(...args),
  122. interceptStringProtocol: (...args) => session.defaultSession.protocol.interceptStringProtocol(...args),
  123. interceptBufferProtocol: (...args) => session.defaultSession.protocol.interceptBufferProtocol(...args),
  124. interceptStreamProtocol: (...args) => session.defaultSession.protocol.interceptStreamProtocol(...args),
  125. interceptFileProtocol: (...args) => session.defaultSession.protocol.interceptFileProtocol(...args),
  126. interceptHttpProtocol: (...args) => session.defaultSession.protocol.interceptHttpProtocol(...args),
  127. interceptProtocol: (...args) => session.defaultSession.protocol.interceptProtocol(...args),
  128. uninterceptProtocol: (...args) => session.defaultSession.protocol.uninterceptProtocol(...args),
  129. isProtocolIntercepted: (...args) => session.defaultSession.protocol.isProtocolIntercepted(...args),
  130. handle: (...args) => session.defaultSession.protocol.handle(...args),
  131. unhandle: (...args) => session.defaultSession.protocol.unhandle(...args),
  132. isProtocolHandled: (...args) => session.defaultSession.protocol.isProtocolHandled(...args)
  133. } as typeof Electron.protocol;
  134. export default protocol;