fix(proxy): propagate origin status code and handle 304 correctly (#2197)

* fix(novelbuddy): fix watermark regex and bump version to 2.1.2

* fix(novelbuddy): fix watermark regex, remove chapter api fetch and bump version to 2.1.2

* fix(proxy): propagate origin status code and handle 304 Not Modified correctly

* Add API call for fetching manga chapters

Fetch chapters from API if available, otherwise use initial data.

* revert(novelbuddy): revert novelbuddy to version 2.1.1 and simple watermark removal

* fix(proxy): improve redirect handling, status propagation, and 304 handling

Restored verbose logging and implemented robust internal redirect following with infinite loop protection. Verified via web debugger.

* fix: type errors for buffer and redact sensitive logs

* fix: replace any with strictly typed separate variables

* chore(proxy): revert log redaction and run prettier
This commit is contained in:
7ui77
2026-05-19 16:24:14 +07:00
committed by GitHub
parent 05a804621c
commit 86de999a00
+91 -116
View File
@@ -75,6 +75,7 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => {
res.setHeader('Access-Control-Allow-Origin', settings.CLIENT_HOST); res.setHeader('Access-Control-Allow-Origin', settings.CLIENT_HOST);
res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Credentials', 'true');
req.headers.referer = rawUrl; req.headers.referer = rawUrl;
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
res.statusCode = 200; res.statusCode = 200;
res.end(); res.end();
@@ -90,21 +91,17 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => {
} }
} }
req.headers['sec-fetch-mode'] = 'cors'; req.headers['sec-fetch-mode'] = 'cors';
if (settings.cookies) { if (settings.cookies) req.headers['cookie'] = settings.cookies;
req.headers['cookie'] = settings.cookies; if (!settings.useUserAgent) delete req.headers['user-agent'];
}
if (!settings.useUserAgent) {
delete req.headers['user-agent'];
}
req.headers.host = _url.host; req.headers.host = _url.host;
req.url = _url.toString(); req.url = _url.toString();
res.statusCode = 200;
proxyRequest(req, res); proxyRequest(req, res);
} catch (err) { } catch (err) {
console.log('\x1b[31m', '----------ERRROR----------'); console.log('\x1b[31m', '----------ERRROR----------');
console.error(err); console.error(err);
console.log('\x1b[31m', '----------ERRROR----------'); console.log('\x1b[31m', '----------ERRROR----------');
if (!res.closed) { if (!res.closed) {
res.statusCode = 500;
res.end(); res.end();
} }
} }
@@ -123,17 +120,14 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value); console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value);
}); });
console.log('\x1b[36m', '----------------'); console.log('\x1b[36m', '----------------');
if (settings.fetchMode === FetchMode.CURL) { if (settings.fetchMode === FetchMode.CURL) {
//i mean if it works it works i guess, better than nothing let curl = `curl -L '${_url.href}'`;
let curl = `curl '${_url.href}'`; if (settings.useUserAgent)
if (settings.useUserAgent) {
curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`; curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`;
}
if (settings.cookies) curl += ` -H 'Cookie: ${settings.cookies}'`; if (settings.cookies) curl += ` -H 'Cookie: ${settings.cookies}'`;
if (req.headers.origin2) curl += ` -H 'Origin: ${req.headers.origin2}'`; if (req.headers.origin2) curl += ` -H 'Origin: ${req.headers.origin2}'`;
console.log('Running curl command:', curl);
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
const options = isWindows const options = isWindows
? { ? {
@@ -143,37 +137,27 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
} }
: {}; : {};
exec(curl, options, (error, stdout, stderr) => { exec(curl, options, (error, stdout) => {
if (error) { if (error) {
console.error(`exec error: ${error}`);
res.statusCode = 500; res.statusCode = 500;
res.write(`exec error: ${error}`); res.write(`exec error: ${error}`);
res.end(); res.end();
return; return;
} }
if (stderr) {
console.error(`stderr: ${stderr}`);
}
res.statusCode = 200; res.statusCode = 200;
res.write(stdout); res.write(stdout);
res.end(); res.end();
}); });
} else if (settings.fetchMode === FetchMode.NODE_FETCH) { } else if (settings.fetchMode === FetchMode.NODE_FETCH) {
const headers = new Headers(); const headers = new Headers();
if (settings.useUserAgent) { if (settings.useUserAgent)
headers.append('user-agent', req.headers['user-agent'] as string); headers.append('user-agent', req.headers['user-agent'] as string);
} if (settings.cookies) headers.append('cookie', settings.cookies);
if (settings.cookies) { if (req.headers.origin2)
headers.append('cookie', settings.cookies);
}
if (req.headers.origin2) {
headers.append('origin', req.headers.origin2 as string); headers.append('origin', req.headers.origin2 as string);
}
fetch(_url.href, { fetch(_url.href, { headers })
headers: headers, .then(async res2 => {
})
.then(async res2 => [res2, await res2.text()] as const)
.then(([res2, text]) => {
res.statusCode = res2.status; res.statusCode = res2.status;
res2.headers.forEach((val, key) => { res2.headers.forEach((val, key) => {
if ( if (
@@ -184,7 +168,7 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
res.setHeader(key, val); res.setHeader(key, val);
} }
}); });
res.write(text); res.write(await res2.text());
res.end(); res.end();
}) })
.catch(err => { .catch(err => {
@@ -196,13 +180,9 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
proxy.web( proxy.web(
req, req,
res, res,
{ { target: _url.origin, selfHandleResponse: true, followRedirects: true },
target: _url.origin,
selfHandleResponse: true,
followRedirects: true,
},
err => { err => {
console.error(err); console.error('Proxy target error:', err);
res.statusCode = 500; res.statusCode = 500;
res.end(); res.end();
}, },
@@ -211,94 +191,89 @@ const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
}; };
proxy.on('proxyRes', function (proxyRes, req, res) { proxy.on('proxyRes', function (proxyRes, req, res) {
const statusCode = proxyRes.statusCode; const statusCode = proxyRes.statusCode || 200;
// Redirect
if (
statusCode === 301 ||
statusCode === 302 ||
statusCode === 303 ||
statusCode === 307 ||
statusCode === 308
) {
req.method = 'GET';
req.headers['content-length'] = '0';
delete req.headers['content-type'];
// Remove all listeners (=reset events to initial state)
req.removeAllListeners();
// Initiate a new proxy request. // Redirect handling
proxyRequest(req, res); if ([301, 302, 303, 307, 308].includes(statusCode)) {
return false; const location = proxyRes.headers['location'];
} if (location) {
const contentEncoding = proxyRes.headers['content-encoding'];
const isBrotli =
contentEncoding &&
(Array.isArray(contentEncoding)
? contentEncoding.some(enc => enc.includes('br'))
: contentEncoding.includes('br'));
const isGzip =
contentEncoding &&
(Array.isArray(contentEncoding)
? contentEncoding.some(enc => enc.includes('gzip'))
: contentEncoding.includes('gzip'));
const isZstd =
contentEncoding &&
(Array.isArray(contentEncoding)
? contentEncoding.some(enc => enc.includes('zstd'))
: contentEncoding.includes('zstd'));
if (isBrotli || isGzip || isZstd) {
delete proxyRes.headers['content-encoding'];
delete proxyRes.headers['content-length'];
for (const _header in proxyRes.headers) {
if (!settings.disAllowResponseHeaders.includes(_header)) {
res.setHeader(_header, proxyRes.headers[_header] as string);
}
}
const chunks: Buffer[] = [];
proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk)));
proxyRes.on('end', async function () {
try { try {
const buffer = Buffer.concat(chunks); const _url = new URL(req.url || '');
let decompressed; const redirectUrl = new URL(location, _url.href);
req.url = redirectUrl.toString();
if (isBrotli) { // Prevent infinite loops
decompressed = brotliDecompressSync(buffer); const reqWithRedirect = req as Connect.IncomingMessage & {
} else if (isZstd) { _redirectCount?: number;
decompressed = zstdDecompressSync(buffer); };
} else { const redirectCount = reqWithRedirect._redirectCount || 0;
decompressed = gunzipSync(buffer); if (redirectCount >= 5) {
res.statusCode = 508;
res.end('Too many redirects');
return;
}
reqWithRedirect._redirectCount = redirectCount + 1;
// Update method for 301/302/303 to GET as per spec
if ([301, 302, 303].includes(statusCode)) {
req.method = 'GET';
req.headers['content-length'] = '0';
delete req.headers['content-type'];
} }
res.write(Buffer.from(decompressed)); req.removeAllListeners();
res.end(); proxyRequest(req, res);
return;
} catch (err) { } catch (err) {
console.error(err); console.error('Redirect parsing error:', err);
res.statusCode = 500;
res.end(`Error decompressing ${isBrotli ? 'Brotli' : 'GZIP'} content`);
}
});
} else {
for (const _header in proxyRes.headers) {
if (!settings.disAllowResponseHeaders.includes(_header)) {
res.setHeader(_header, proxyRes.headers[_header] as string);
} }
} }
for (const _header in settings.disAllowedRequestHeaders) {
delete proxyRes.headers[_header];
}
proxyRes.on('data', function (chunk) {
res.write(chunk);
});
proxyRes.on('end', function () {
res.end();
});
} }
res.statusCode = statusCode;
// Propagate headers but filter restricted ones
Object.keys(proxyRes.headers).forEach(key => {
if (
!settings.disAllowResponseHeaders.includes(key) &&
key !== 'content-encoding' &&
key !== 'content-length'
) {
res.setHeader(key, proxyRes.headers[key] as string);
}
});
if (statusCode === 304) {
res.end();
return;
}
const contentEncoding = proxyRes.headers['content-encoding'] || '';
const chunks: Buffer[] = [];
proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk)));
proxyRes.on('end', () => {
try {
const compressedBuffer = Buffer.concat(chunks);
if (compressedBuffer.length > 0) {
let decompressedBuffer: Buffer;
if (contentEncoding.includes('br')) {
decompressedBuffer = brotliDecompressSync(compressedBuffer);
} else if (contentEncoding.includes('gzip')) {
decompressedBuffer = gunzipSync(compressedBuffer);
} else if (contentEncoding.includes('zstd')) {
decompressedBuffer = zstdDecompressSync(compressedBuffer);
} else {
decompressedBuffer = compressedBuffer;
}
res.write(decompressedBuffer);
}
res.end();
} catch (err) {
console.error('Decompression error:', err);
res.statusCode = 500;
res.end('Decompression error');
}
});
}); });
export { proxyHandlerMiddle, proxySettingMiddleware }; export { proxyHandlerMiddle, proxySettingMiddleware };