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:
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user