-
Notifications
You must be signed in to change notification settings - Fork 20
refactor: adds authentication to media features #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6380a46
6d83f68
3f154b5
ef3a08b
972c9fb
758dd9e
91b8eea
727d611
3029b75
c2a08f1
204ef34
2c779b7
cd215b0
695476e
e9c776e
294fe6e
981a1c7
ec3c9df
2f05530
6ed5e75
5e17654
7640ae5
87f4973
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,62 +1,271 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { type OutgoingHttpHeaders } from 'node:http'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { type IncomingHttpHeaders } from 'node:http'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import https from 'node:https'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type RequestOptions = Parameters<typeof https.request>[1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type MultipartResult = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content: Buffer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers?: Record<string, string>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirect?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * parses Matrix federation multipart/mixed media responses according to spec. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * https://spec.matrix.org/v1.15/server-server-api/#get_matrixfederationv1mediadownloadmediaid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function parseMultipart(buffer: Buffer, boundary: string): MultipartResult { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bufferStr = buffer.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // check if the second part contains a Location header (CDN redirect) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // pattern: after first boundary and JSON part, look for Location header | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parts = bufferStr.split(`--${boundary}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parts.length >= 3) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const secondPart = parts[2]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const locationMatch = secondPart.match(/\r?\nLocation:\s*(.+)\r?\n/i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (locationMatch) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content: Buffer.from(''), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirect: locationMatch[1].trim(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // find where the last part's content starts (after the last \r\n\r\n) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lastHeaderEnd = buffer.lastIndexOf('\r\n\r\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lastHeaderEnd === -1) return { content: buffer }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const binaryStart = lastHeaderEnd + 4; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const closingBoundary = buffer.lastIndexOf(`\r\n--${boundary}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const content = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| closingBoundary > binaryStart | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? buffer.subarray(binaryStart, closingBoundary) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : buffer.subarray(binaryStart); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { content }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ricardogarim marked this conversation as resolved.
Show resolved
Hide resolved
ricardogarim marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleJson<T>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentType: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: () => Promise<Buffer>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!contentType.includes('application/json')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Content-Type is not application/json'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse((await body()).toString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Failed to parse JSON response'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleText( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentType: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: () => Promise<Buffer>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!contentType.includes('text/')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (await body()).toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the redirect URL should be fetched without Matrix auth | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and will only occur for media downloads as per Matrix spec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleMultipartRedirect<T>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirect: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<FetchResponse<T>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const redirectResponse = await fetch<T>(new URL(redirect), { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'GET', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!redirectResponse.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Failed to fetch media from redirect: ${redirect}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+80
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden redirect fetch: enforce https and set Host for consistent SNI. Prevents accidental plain HTTP and ensures correct SNI without relying on caller headers. - const redirectResponse = await fetch<T>(new URL(redirect), {
- method: 'GET',
- headers: {},
- });
+ const u = new URL(redirect);
+ if (u.protocol !== 'https:') {
+ throw new Error(`Rejected non-HTTPS redirect: ${redirect}`);
+ }
+ const redirectResponse = await fetch<T>(u, {
+ method: 'GET',
+ headers: { Host: u.host },
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return redirectResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleMultipart<T>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentType: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: () => Promise<Buffer>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| depth = 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<MultipartResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!/\bmultipart\b/i.test(contentType)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Content-Type is not multipart'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // extract boundary from content-type header | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const boundaryMatch = contentType.match(/boundary=([^;,\s]+)/i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!boundaryMatch) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Boundary not found in Content-Type header'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // remove quotes if present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const boundary = boundaryMatch[1].replace(/^["']|["']$/g, ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const multipart = parseMultipart(await body(), boundary); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (multipart.redirect) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (depth >= 5) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Too many redirects in multipart response'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const redirectResponse = await handleMultipartRedirect<T>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multipart.redirect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return handleMultipart( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirectResponse.headers['content-type'] || '', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirectResponse.body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| depth + 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return multipart; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type FetchResponse<T> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ok: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: number | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: IncomingHttpHeaders; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buffer: () => Promise<Buffer>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json: () => Promise<T>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: () => Promise<string>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multipart: () => Promise<MultipartResult>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: () => Promise<Buffer>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // this fetch is used when connecting to a multihome server, same server hosting multiple homeservers, and we need to verify the cert with the right SNI (hostname), or else, cert check will fail due to connecting through ip and not hostname (due to matrix spec). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function fetch(url: URL, options: RequestInit) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function fetch<T>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options: RequestInit, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<FetchResponse<T>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const serverName = new URL( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `http://${(options.headers as OutgoingHttpHeaders).Host}` as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `http://${(options.headers as IncomingHttpHeaders).Host}` as string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).hostname; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestParams: RequestOptions = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host: url.hostname, // IP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: url.port, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: options.method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: url.pathname + url.search, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(options.headers as OutgoingHttpHeaders), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'content-type': 'application/json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: options.headers as IncomingHttpHeaders, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| servername: serverName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
145
to
156
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SNI: fall back to URL hostname when Host header is absent. Current code computes servername from a possibly missing Host header and can become "undefined", breaking TLS SNI. Fallback to url.hostname. Apply: -const serverName = new URL(
- `http://${(options.headers as IncomingHttpHeaders).Host}` as string,
-).hostname;
+const hostHeader = (options.headers as IncomingHttpHeaders)?.Host;
+const serverName = hostHeader
+ ? new URL(`http://${hostHeader}`).hostname
+ : url.hostname;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response: { statusCode: number | undefined; body: string } = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const request = https.request(requestParams, (res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let data = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on('data', (chunk) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data += chunk; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on('end', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: res.statusCode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: data, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.on('error', (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: number | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: () => Promise<Buffer>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: IncomingHttpHeaders; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } = await new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const request = https.request(requestParams, (res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chunks: Buffer[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.once('error', reject); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.pause(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let body: Promise<Buffer>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: res.statusCode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: res.headers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!body) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body = new Promise<Buffer>((resBody, rejBody) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Make @hs/core fetch size limit configurable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let total = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_RESPONSE_BYTES = 50 * 1024 * 1024; // 50 MB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onData = (chunk: Buffer) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total += chunk.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (total > MAX_RESPONSE_BYTES) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const err = new Error('Response exceeds size limit'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.destroy(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rejBody(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chunks.push(chunk); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onEnd = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resBody(Buffer.concat(chunks)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onErr = (err: Error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rejBody(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onAborted = () => onErr(new Error('Response aborted')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cleanup = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.off('data', onData); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.off('end', onEnd); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.off('error', onErr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.off('aborted', onAborted); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on('data', onData); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.once('end', onEnd); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.once('error', onErr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.once('aborted', onAborted); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.resume(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return body; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.end(options.body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const signal = options.signal; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (signal) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onAbort = () => request.destroy(new Error('Aborted')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signal.addEventListener('abort', onAbort, { once: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.once('close', () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signal.removeEventListener('abort', onAbort), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.on('error', (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Make @hs/core fetch timeout configurable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.setTimeout(20_000, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.destroy(new Error('Request timed out after 20s')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request.end(options.body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const contentType = response.headers['content-type'] || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ok: response.statusCode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? response.statusCode >= 200 && response.statusCode < 300 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json: () => JSON.parse(response.body), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: () => response.body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buffer: () => response.body(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json: () => handleJson<T>(contentType, response.body), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ricardogarim marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: () => handleText(contentType, response.body), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multipart: () => handleMultipart(contentType, response.body), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: response.body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: response.statusCode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: response.headers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const reason = err instanceof Error ? err.message : String(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ok: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json: () => undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: () => (err instanceof Error ? err.message : String(err)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buffer: () => Promise.reject(reason), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json: () => Promise.reject(reason), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: () => Promise.reject(reason), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multipart: () => Promise.reject(reason), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: () => Promise.reject(reason), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.