mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 21:44:59 -05:00
Handle unsupported attachment HTTP statuses without crashing mirror archives
Reject invalid statuses and malformed headers inside the HTTP response callback, close rejected streams and drain bodyless responses. Regression tests cover unsupported statuses, malformed headers and successful downloads. Thanks to xet7 !
This commit is contained in:
1 parent
ad3b67dfa9
commit
6dd41dd414
2 files changed
+68
-4
No files matched your search
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { Readable } = require('node:stream');
|
||||
const { EventEmitter } = require('node:events');
|
||||
|
||||
test('pinned attachment HTTP adapter rejects invalid responses without uncaught callback errors', async () => {
|
||||
const { pinnedFetch } = await import('../tools/mirror-archive.mjs');
|
||||
async function run(status, headers = {}) {
|
||||
const response = Readable.from([Buffer.from('attachment')]);
|
||||
response.statusCode = status;
|
||||
response.headers = headers;
|
||||
const transport = { request(url, options, callback) {
|
||||
const request = new EventEmitter();
|
||||
request.end = () => queueMicrotask(() => callback(response));
|
||||
return request;
|
||||
} };
|
||||
const promise = pinnedFetch(new URL('https://example.com/file'), {}, [{ address: '93.184.216.34' }], transport);
|
||||
return { promise, response };
|
||||
}
|
||||
for (const status of [0, 101, 199, 600, 999, undefined]) {
|
||||
const { promise, response } = await run(status);
|
||||
await assert.rejects(promise, /unsupported HTTP status/);
|
||||
assert.equal(response.destroyed, true);
|
||||
}
|
||||
const malformed = await run(200, { 'bad header': 'value' });
|
||||
await assert.rejects(malformed.promise, /invalid header name/i);
|
||||
assert.equal(malformed.response.destroyed, true);
|
||||
const valid = await run(200, { 'content-type': 'text/plain' });
|
||||
assert.equal(await (await valid.promise).text(), 'attachment');
|
||||
for (const status of [204, 205, 304]) {
|
||||
const { promise } = await run(status);
|
||||
assert.equal((await promise).body, null);
|
||||
}
|
||||
});
|
||||
@@ -135,10 +135,39 @@ export async function publicLink(url, resolveHost = lookup) {
|
||||
})) throw new Error('Linked URL does not resolve exclusively to public Internet addresses');
|
||||
return addresses;
|
||||
}
|
||||
async function pinnedFetch(url, options, addresses) {
|
||||
return new Promise((resolve,reject)=>{
|
||||
const request=(url.protocol==='https:'?https:http).request(url,{headers:options.headers,signal:options.signal,lookup:(host,lookupOptions,callback)=>{const values=addresses.map(a=>({address:a.address,family:isIP(a.address)}));if(lookupOptions.all) callback(null,values);else callback(null,values[0].address,values[0].family);}},response=>{resolve(new Response([204,205,304].includes(response.statusCode)?null:Readable.toWeb(response),{status:response.statusCode,headers:Object.fromEntries(Object.entries(response.headers).filter(([k,v])=>v!==undefined).map(([k,v])=>[k,Array.isArray(v)?v.join(', '):v]))}));});
|
||||
request.on('error',reject);request.end();
|
||||
export async function pinnedFetch(url, options, addresses, transport = url.protocol === 'https:' ? https : http) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = transport.request(url, {
|
||||
headers: options.headers,
|
||||
signal: options.signal,
|
||||
lookup: (host, lookupOptions, callback) => {
|
||||
const values = addresses.map(a => ({ address: a.address, family: isIP(a.address) }));
|
||||
if (lookupOptions.all) callback(null, values);
|
||||
else callback(null, values[0].address, values[0].family);
|
||||
},
|
||||
}, response => {
|
||||
// Node accepts status codes outside Fetch's 200..599 range. Errors in
|
||||
// this event callback must reject the download, not escape and crash
|
||||
// the archive process (including malformed response headers).
|
||||
try {
|
||||
const status = response.statusCode;
|
||||
if (!Number.isInteger(status) || status < 200 || status > 599) {
|
||||
throw new Error(`Linked URL returned unsupported HTTP status ${status}`);
|
||||
}
|
||||
const emptyBody = [204, 205, 304].includes(status);
|
||||
const headers = Object.fromEntries(Object.entries(response.headers)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.map(([key, value]) => [key, Array.isArray(value) ? value.join(', ') : value]));
|
||||
const result = new Response(emptyBody ? null : Readable.toWeb(response), { status, headers });
|
||||
if (emptyBody) response.resume();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
response.destroy();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
request.on('error', reject);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
export function commentIdentity(comment) {return staticCommentIdentity(comment);}
|
||||
|
||||
Reference in new issue
Block a user