protocol.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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.slice(0, rv));
  18. } else {
  19. controller.close();
  20. }
  21. } catch (e) {
  22. controller.error(e);
  23. }
  24. }
  25. });
  26. }
  27. function makeStreamFromFileInfo ({
  28. filePath,
  29. offset = 0,
  30. length = -1
  31. }: {
  32. filePath: string;
  33. offset?: number;
  34. length?: number;
  35. }): ReadableStream {
  36. return Readable.toWeb(createReadStream(filePath, {
  37. start: offset,
  38. end: length >= 0 ? offset + length : undefined
  39. }));
  40. }
  41. function convertToRequestBody (uploadData: ProtocolRequest['uploadData']): RequestInit['body'] {
  42. if (!uploadData) return null;
  43. // Optimization: skip creating a stream if the request is just a single buffer.
  44. if (uploadData.length === 1 && (uploadData[0] as any).type === 'rawData') return uploadData[0].bytes;
  45. const chunks = [...uploadData] as any[]; // TODO: types are wrong
  46. let current: ReadableStreamDefaultReader | null = null;
  47. return new ReadableStream({
  48. async pull (controller) {
  49. if (current) {
  50. const { done, value } = await current.read();
  51. // (done => value === undefined) as per WHATWG spec
  52. if (done) {
  53. current = null;
  54. return this.pull!(controller);
  55. } else {
  56. controller.enqueue(value);
  57. }
  58. } else {
  59. if (!chunks.length) { return controller.close(); }
  60. const chunk = chunks.shift()!;
  61. if (chunk.type === 'rawData') {
  62. controller.enqueue(chunk.bytes);
  63. } else if (chunk.type === 'file') {
  64. current = makeStreamFromFileInfo(chunk).getReader();
  65. return this.pull!(controller);
  66. } else if (chunk.type === 'stream') {
  67. current = makeStreamFromPipe(chunk.body).getReader();
  68. return this.pull!(controller);
  69. } else if (chunk.type === 'blob') {
  70. // Note that even though `getBlobData()` is a `Session` API, it doesn't
  71. // actually use the `Session` context. Its implementation solely relies
  72. // on global variables which allows us to implement this feature without
  73. // knowledge of the `Session` associated with the current request by
  74. // always pulling `Blob` data out of the default `Session`.
  75. controller.enqueue(await session.defaultSession.getBlobData(chunk.blobUUID));
  76. } else {
  77. throw new Error(`Unknown upload data chunk type: ${chunk.type}`);
  78. }
  79. }
  80. }
  81. }) as RequestInit['body'];
  82. }
  83. function validateResponse (res: Response) {
  84. if (!res || typeof res !== 'object') return false;
  85. if (res.type === 'error') return true;
  86. const exists = (key: string) => Object.hasOwn(res, key);
  87. if (exists('status') && typeof res.status !== 'number') return false;
  88. if (exists('statusText') && typeof res.statusText !== 'string') return false;
  89. if (exists('headers') && typeof res.headers !== 'object') return false;
  90. if (exists('body')) {
  91. if (typeof res.body !== 'object') return false;
  92. if (res.body !== null && !(res.body instanceof ReadableStream)) return false;
  93. }
  94. return true;
  95. }
  96. Protocol.prototype.handle = function (this: Electron.Protocol, scheme: string, handler: (req: Request) => Response | Promise<Response>) {
  97. const register = isBuiltInScheme(scheme) ? this.interceptProtocol : this.registerProtocol;
  98. const success = register.call(this, scheme, async (preq: ProtocolRequest, cb: any) => {
  99. try {
  100. const body = convertToRequestBody(preq.uploadData);
  101. const headers = new Headers(preq.headers);
  102. if (headers.get('origin') === 'null') {
  103. headers.delete('origin');
  104. }
  105. const req = new Request(preq.url, {
  106. headers,
  107. method: preq.method,
  108. referrer: preq.referrer,
  109. body,
  110. duplex: body instanceof ReadableStream ? 'half' : undefined
  111. } as any);
  112. const res = await handler(req);
  113. if (!validateResponse(res)) {
  114. return cb({ error: ERR_UNEXPECTED });
  115. } else if (res.type === 'error') {
  116. cb({ error: ERR_FAILED });
  117. } else {
  118. cb({
  119. data: res.body ? Readable.fromWeb(res.body as ReadableStream<ArrayBufferView>) : null,
  120. headers: res.headers ? Object.fromEntries(res.headers) : {},
  121. statusCode: res.status,
  122. statusText: res.statusText,
  123. mimeType: (res as any).__original_resp?._responseHead?.mimeType
  124. });
  125. }
  126. } catch (e) {
  127. console.error(e);
  128. cb({ error: ERR_UNEXPECTED });
  129. }
  130. });
  131. if (!success) throw new Error(`Failed to register protocol: ${scheme}`);
  132. };
  133. Protocol.prototype.unhandle = function (this: Electron.Protocol, scheme: string) {
  134. const unregister = isBuiltInScheme(scheme) ? this.uninterceptProtocol : this.unregisterProtocol;
  135. if (!unregister.call(this, scheme)) { throw new Error(`Failed to unhandle protocol: ${scheme}`); }
  136. };
  137. Protocol.prototype.isProtocolHandled = function (this: Electron.Protocol, scheme: string) {
  138. const isRegistered = isBuiltInScheme(scheme) ? this.isProtocolIntercepted : this.isProtocolRegistered;
  139. return isRegistered.call(this, scheme);
  140. };
  141. const protocol = {
  142. registerSchemesAsPrivileged,
  143. getStandardSchemes,
  144. registerStringProtocol: (...args) => session.defaultSession.protocol.registerStringProtocol(...args),
  145. registerBufferProtocol: (...args) => session.defaultSession.protocol.registerBufferProtocol(...args),
  146. registerStreamProtocol: (...args) => session.defaultSession.protocol.registerStreamProtocol(...args),
  147. registerFileProtocol: (...args) => session.defaultSession.protocol.registerFileProtocol(...args),
  148. registerHttpProtocol: (...args) => session.defaultSession.protocol.registerHttpProtocol(...args),
  149. registerProtocol: (...args) => session.defaultSession.protocol.registerProtocol(...args),
  150. unregisterProtocol: (...args) => session.defaultSession.protocol.unregisterProtocol(...args),
  151. isProtocolRegistered: (...args) => session.defaultSession.protocol.isProtocolRegistered(...args),
  152. interceptStringProtocol: (...args) => session.defaultSession.protocol.interceptStringProtocol(...args),
  153. interceptBufferProtocol: (...args) => session.defaultSession.protocol.interceptBufferProtocol(...args),
  154. interceptStreamProtocol: (...args) => session.defaultSession.protocol.interceptStreamProtocol(...args),
  155. interceptFileProtocol: (...args) => session.defaultSession.protocol.interceptFileProtocol(...args),
  156. interceptHttpProtocol: (...args) => session.defaultSession.protocol.interceptHttpProtocol(...args),
  157. interceptProtocol: (...args) => session.defaultSession.protocol.interceptProtocol(...args),
  158. uninterceptProtocol: (...args) => session.defaultSession.protocol.uninterceptProtocol(...args),
  159. isProtocolIntercepted: (...args) => session.defaultSession.protocol.isProtocolIntercepted(...args),
  160. handle: (...args) => session.defaultSession.protocol.handle(...args),
  161. unhandle: (...args) => session.defaultSession.protocol.unhandle(...args),
  162. isProtocolHandled: (...args) => session.defaultSession.protocol.isProtocolHandled(...args)
  163. } as typeof Electron.protocol;
  164. export default protocol;