Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 202 additions & 58 deletions proxy.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import process from 'node:process';
/* global RequestInit */
import { Buffer } from 'buffer';
import { FetchMode, ServerSetting } from './src/types/types';
import { Connect } from 'vite';
import httpProxy from 'http-proxy';
import { exec } from 'child_process';
import { spawn } from 'child_process';
import { brotliDecompressSync, gunzipSync, zstdDecompressSync } from 'zlib';

const proxy = httpProxy.createProxyServer({});

const settings: ServerSetting = {
CLIENT_HOST: 'http://localhost:3000',
fetchMode: FetchMode.PROXY,
cookies: '',
siteCookies: {},
usePerSiteCookies: false,
disAllowedRequestHeaders: [
'sec-ch-ua',
'sec-ch-ua-mobile',
'sec-ch-ua-platform',
'sec-fetch-site',
'origin',
'sec-fetch-site',
'sec-fetch-dest',
'pragma',
],
Expand Down Expand Up @@ -62,6 +64,19 @@ const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => {
});
};

function getCookiesForHost(hostname: string): string | undefined {
if (settings.usePerSiteCookies && settings.siteCookies) {
// exact match
if (settings.siteCookies[hostname]) return settings.siteCookies[hostname];
// parent domain match: key "source.com" matches "www.source.com", "api.source.com"
for (const [site, cookie] of Object.entries(settings.siteCookies)) {
if (hostname.endsWith('.' + site)) return cookie;
}
return undefined; // per-site mode: no cookie for unmatched hosts
}
return settings.cookies; // global mode: same cookie for all requests
}

const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => {
const rawUrl = 'https:' + req.url;
if (req.headers['access-control-request-method']) {
Expand Down Expand Up @@ -97,7 +112,6 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => {
}
}
req.headers['sec-fetch-mode'] = 'cors';
if (settings.cookies) req.headers['cookie'] = settings.cookies;
if (!settings.useUserAgent) delete req.headers['user-agent'];
req.headers.host = _url.host;
req.url = _url.toString();
Expand All @@ -114,82 +128,207 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => {
}
};

const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
const _url = new URL(req.url || '');
function curlRequest(
url: string,
method: string,
req: Connect.IncomingMessage,
bodyBuffer: Buffer,
redirectCount: number,
res: Connect.ServerResponse,
) {
if (redirectCount >= 5) {
res.statusCode = 508;
res.end('Too many redirects');
return;
}

const _url = new URL(url);
const args = ['-X', method, '-s', '-D', '-', _url.href];

if (settings.useUserAgent && req.headers['user-agent'])
args.push('-H', 'User-Agent: ' + req.headers['user-agent']);
const hostCookie = getCookiesForHost(_url.hostname);
if (hostCookie) args.push('-H', 'Cookie: ' + hostCookie);
if (req.headers.origin2) args.push('-H', 'Origin: ' + req.headers.origin2);
if (req.headers['content-type'] && bodyBuffer.length > 0)
args.push('-H', 'Content-Type: ' + req.headers['content-type']);

if (bodyBuffer.length > 0) args.push('--data-binary', '@-');

console.log('\x1b[36m', '----------------');
console.log(
`Making proxy request - at ${new Date().toLocaleTimeString()}
url: ${_url.href}
headers:`,
'Making CURL request - at ' +
new Date().toLocaleTimeString() +
'\n url: ' +
_url.href +
'\n headers:',
);
Object.entries(req.headers).forEach(([name, value]) => {
console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value);
args.forEach((a, i) => {
if (args[i - 1] === '-H') console.log('\t', '\x1b[32m', a, '\x1b[37m');
});
console.log('\x1b[36m', '----------------');

if (settings.fetchMode === FetchMode.CURL) {
let curl = `curl -L '${_url.href}'`;
if (settings.useUserAgent)
curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`;
if (settings.cookies) curl += ` -H 'Cookie: ${settings.cookies}'`;
if (req.headers.origin2) curl += ` -H 'Origin: ${req.headers.origin2}'`;

const isWindows = process.platform === 'win32';
const options = isWindows
? {
shell:
process.env.BASH_LOCATION ||
process.env.ProgramFiles + '\\git\\usr\\bin\\bash.exe',
}
: {};
const child = spawn('curl', args);

exec(curl, options, (error, stdout) => {
if (error) {
if (bodyBuffer.length > 0) {
child.stdin.write(bodyBuffer);
child.stdin.end();
}

const stdoutChunks: Buffer[] = [];
child.stdout.on('data', chunk => stdoutChunks.push(Buffer.from(chunk)));

let stderr = '';
child.stderr.on('data', chunk => (stderr += chunk));

child.on('close', code => {
if (code !== 0) {
console.error(stderr);
if (!res.headersSent) {
res.statusCode = 500;
res.write(`exec error: ${error}`);
res.write('curl error code: ' + code + '\n' + stderr);
res.end();
return;
}
return;
}

const output = Buffer.concat(stdoutChunks).toString('utf-8');
const headerEnd = output.indexOf('\r\n\r\n');
if (headerEnd === -1) {
res.statusCode = 200;
res.write(stdout);
res.write(output);
res.end();
});
} else if (settings.fetchMode === FetchMode.NODE_FETCH) {
const headers = new Headers();
if (settings.useUserAgent)
headers.append('user-agent', req.headers['user-agent'] as string);
if (settings.cookies) headers.append('cookie', settings.cookies);
if (req.headers.origin2)
headers.append('origin', req.headers.origin2 as string);

fetch(_url.href, { headers })
.then(async res2 => {
res.statusCode = res2.status;
res2.headers.forEach((val, key) => {
if (!settings.disAllowResponseHeaders.includes(key)) {
res.setHeader(key, val);
return;
}

const headerBlock = output.slice(0, headerEnd);
const body = output.slice(headerEnd + 4);

const statusMatch = headerBlock.match(/^HTTP\/[\d.]+\s+(\d+)/m);
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;

if ([301, 302, 303, 307, 308].includes(statusCode)) {
const locMatch = headerBlock.match(/^location:\s*(.+)$/im);
if (locMatch) {
try {
const redirectUrl = new URL(locMatch[1].trim(), _url.href);
let nextMethod = method;
let nextBody = bodyBuffer;
if ([301, 302, 303].includes(statusCode)) {
nextMethod = 'GET';
nextBody = Buffer.alloc(0);
}
});
res.write(await res2.text());
res.end();
})
.catch(err => {
console.error(err);
res.statusCode = 500;
res.end();
});
} else if (settings.fetchMode === FetchMode.PROXY) {
curlRequest(
redirectUrl.href,
nextMethod,
req,
nextBody,
redirectCount + 1,
res,
);
return;
} catch {
console.error('Redirect URL parse error:', locMatch[1]);
}
}
}

res.statusCode = statusCode;

const headerLines = headerBlock.split('\r\n').slice(1);
for (const line of headerLines) {
const ci = line.indexOf(':');
if (ci === -1) continue;
const key = line.slice(0, ci).trim().toLowerCase();
const val = line.slice(ci + 1).trim();
if (!settings.disAllowResponseHeaders.includes(key)) {
res.setHeader(key, val);
}
}

res.write(body);
res.end();
});

res.on('close', () => {
if (!child.killed) child.kill();
});
}

const proxyRequest: Connect.SimpleHandleFunction = (req, res) => {
const _url = new URL(req.url || '');
console.log('\x1b[36m', '----------------');
console.log(
`Making proxy request - at ${new Date().toLocaleTimeString()}
url: ${_url.href}
headers:`,
);
Object.entries(req.headers).forEach(([name, value]) => {
console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value);
});
console.log('\x1b[36m', '----------------');

if (settings.fetchMode === FetchMode.PROXY) {
const hostCookie = getCookiesForHost(_url.hostname);
if (hostCookie) req.headers['cookie'] = hostCookie;
else delete req.headers['cookie'];
proxy.web(
req,
res,
{ target: _url.origin, selfHandleResponse: true, followRedirects: true },
{ target: _url.origin, selfHandleResponse: true },
err => {
console.error('Proxy target error:', err);
res.statusCode = 500;
res.end();
},
);
return;
}

const method = req.method || 'GET';
const chunks: Buffer[] = [];

req.on('data', chunk => chunks.push(Buffer.from(chunk)));
req.on('end', () => {
const bodyBuffer = Buffer.concat(chunks);

if (settings.fetchMode === FetchMode.CURL) {
curlRequest(_url.href, method, req, bodyBuffer, 0, res);
} else if (settings.fetchMode === FetchMode.NODE_FETCH) {
const headers = new Headers();

if (settings.useUserAgent && req.headers['user-agent'])
headers.append('user-agent', req.headers['user-agent'] as string);
const hostCookie = getCookiesForHost(_url.hostname);
if (hostCookie) headers.append('cookie', hostCookie);
if (req.headers.origin2)
headers.append('origin', req.headers.origin2 as string);
if (req.headers['content-type'])
headers.append('content-type', req.headers['content-type'] as string);

const fetchOptions: RequestInit = { method, headers };
if (method !== 'GET' && method !== 'HEAD' && bodyBuffer.length > 0) {
fetchOptions.body = bodyBuffer;
}

fetch(_url.href, fetchOptions)
.then(async res2 => {
res.statusCode = res2.status;
res2.headers.forEach((val, key) => {
if (!settings.disAllowResponseHeaders.includes(key)) {
res.setHeader(key, val);
}
});
res.write(await res2.text());
res.end();
})
.catch(err => {
console.error(err);
res.statusCode = 500;
res.end();
});
}
});
};

proxy.on('proxyRes', function (proxyRes, req, res) {
Expand Down Expand Up @@ -224,6 +363,7 @@ proxy.on('proxyRes', function (proxyRes, req, res) {
}

req.removeAllListeners();
proxyRes.destroy();
proxyRequest(req, res);
return;
} catch (err) {
Expand Down Expand Up @@ -259,7 +399,11 @@ proxy.on('proxyRes', function (proxyRes, req, res) {
} else if (contentEncoding.includes('gzip')) {
decompressedBuffer = gunzipSync(compressedBuffer);
} else if (contentEncoding.includes('zstd')) {
decompressedBuffer = zstdDecompressSync(compressedBuffer);
try {
decompressedBuffer = zstdDecompressSync(compressedBuffer);
} catch {
decompressedBuffer = compressedBuffer;
}
} else {
decompressedBuffer = compressedBuffer;
}
Expand Down
Loading
Loading