feat: request rewriter

This commit is contained in:
DIYgod 2024-03-30 18:21:14 +08:00
parent 176e773013
commit a828b3dd1c
No known key found for this signature in database
7 changed files with 99 additions and 131 deletions

View File

@ -1,4 +1,4 @@
import '@/utils/request-interceptor';
import '@/utils/request-rewriter';
import { Hono } from 'hono';

View File

@ -34,6 +34,9 @@ if (proxyIsPAC) {
dispatcher = new ProxyAgent({
uri: proxyUri,
token: proxyObj?.auth ? `Basic ${proxyObj.auth}` : undefined,
requestTls: {
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0',
},
});
} else if (proxyUri.startsWith('socks')) {
agent = new SocksProxyAgent(proxyUri);

View File

@ -1,19 +1,14 @@
import { setupServer } from 'msw/node';
import { http } from 'msw';
import logger from '@/utils/logger';
import { config } from '@/config';
import { fetch, Headers, FormData, Request, Response } from 'undici';
import { fetch, Request, RequestInfo, RequestInit } from 'undici';
import proxy from '@/utils/proxy';
Object.defineProperties(globalThis, {
fetch: { value: fetch, writable: true },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
});
const wrappedFetch: typeof fetch = (input: RequestInfo, init?: RequestInit) => {
const request = new Request(input, init);
const options: RequestInit = {};
logger.debug(`Outgoing request: ${request.method} ${request.url}`);
const handler = (request: globalThis.Request, options: NonNullable<Parameters<typeof fetch>[1]>) => {
// ua
if (!request.headers.get('user-agent')) {
request.headers.set('user-agent', config.ua);
@ -45,21 +40,11 @@ const handler = (request: globalThis.Request, options: NonNullable<Parameters<ty
}
if (proxyRegex.test(request.url) && request.url.startsWith('http') && !(urlHandler && urlHandler.host === proxy.proxyUrlHandler?.host)) {
// fetch
options.dispatcher = proxy.dispatcher;
}
}
return fetch(request, options);
};
const server = setupServer(
// @ts-expect-error
http.all('*', ({ request }) => {
logger.debug(`Outgoing request: ${request.method} ${request.url}`);
const requestClone = request.clone();
const options = {};
handler(requestClone, options);
// @ts-expect-error
return fetch(requestClone, options);
})
);
server.listen();
export default wrappedFetch;

View File

@ -0,0 +1,66 @@
import http from 'node:http';
import https from 'node:https';
import logger from '@/utils/logger';
import { config } from '@/config';
import proxy from '@/utils/proxy';
type Get = typeof http.get | typeof https.get | typeof http.request | typeof https.request;
const getWrappedGet: <T extends Get>(origin: T) => T = (origin) =>
function (this: any, ...args: Parameters<typeof origin>) {
let url: URL;
let options: http.RequestOptions = {};
let callback: ((res: http.IncomingMessage) => void) | undefined;
if (typeof args[0] === 'string' || args[0] instanceof URL) {
url = new URL(args[0]);
if (typeof args[1] === 'object') {
options = args[1];
callback = args[2];
} else if (typeof args[1] === 'function') {
options = {};
callback = args[1];
}
} else {
options = args[0];
url = new URL(options.href || `${options.protocol}//${options.hostname || options.host}${options.path}${options.search || (options.query ? `?${options.query}` : '')}`);
if (typeof args[1] === 'function') {
callback = args[1];
}
}
logger.debug(`Outgoing request: ${options.method || 'GET'} ${url}`);
options.headers = options.headers || {};
const headersLowerCaseKeys = new Set(Object.keys(options.headers).map((key) => key.toLowerCase()));
// ua
if (!headersLowerCaseKeys.has('user-agent')) {
options.headers['user-agent'] = config.ua;
}
// Accept
if (!headersLowerCaseKeys.has('accept')) {
options.headers.accept = '*/*';
}
// referer
if (!headersLowerCaseKeys.has('referer')) {
options.headers.referer = url.origin;
}
// proxy
if (!options.agent && proxy.agent) {
const proxyRegex = new RegExp(proxy.proxyObj.url_regex);
if (proxyRegex.test(url.toString()) && url.protocol.startsWith('http') && url.host !== proxy.proxyUrlHandler?.host) {
options.agent = proxy.agent;
if (proxy.proxyObj.auth) {
options.headers['Proxy-Authorization'] = `Basic ${proxy.proxyObj.auth}`;
}
}
}
return Reflect.apply(origin, this, [url, options, callback]) as ReturnType<typeof origin>;
};
export default getWrappedGet;

View File

@ -0,0 +1,19 @@
import { Headers, FormData, Request, Response } from 'undici';
import http from 'node:http';
import https from 'node:https';
import fetch from '@/utils/request-rewriter/fetch';
import getWrappedGet from '@/utils/request-rewriter/get';
Object.defineProperties(globalThis, {
fetch: { value: fetch },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
});
http.get = getWrappedGet(http.get);
http.request = getWrappedGet(http.request);
https.get = getWrappedGet(https.get);
https.request = getWrappedGet(https.request);

View File

@ -1,105 +0,0 @@
import { config } from '@/config';
import logger from '@/utils/logger';
import http, { type RequestOptions } from 'node:http';
import https from 'node:https';
import proxy from '@/utils/proxy';
let proxyWrapper: (
url: string,
options: RequestOptions & {
headers: Record<string, string>;
}
) => boolean = () => false;
if (proxy.agent) {
const proxyRegex = new RegExp(proxy.proxyObj.url_regex);
const protocolMatch = (protocolLike?: string | null) => protocolLike?.toLowerCase().startsWith('http');
proxyWrapper = (url, options) => {
let urlHandler;
try {
urlHandler = new URL(url);
} catch {
// ignore
}
if (proxyRegex.test(url) && (protocolMatch(options.protocol) || protocolMatch(url)) && (!urlHandler || urlHandler.host !== proxy.proxyUrlHandler?.host)) {
options.agent = proxy.agent || false;
if (proxy.proxyObj.auth) {
options.headers['Proxy-Authorization'] = `Basic ${proxy.proxyObj.auth}`;
}
return true;
}
return false;
};
}
const requestWrapper = (url: string, options: http.RequestOptions = {}) => {
options.headers = options.headers || {};
const optionsWithHeaders = options as http.RequestOptions & {
headers: Record<string, string>;
};
const headersLowerCaseKeys = new Set(Object.keys(optionsWithHeaders.headers).map((key) => key.toLowerCase()));
let prxied = false;
if (config.proxyStrategy === 'all') {
prxied = proxyWrapper(url, optionsWithHeaders);
} else if (config.proxyStrategy === 'on_retry' && (optionsWithHeaders as any).retryCount) {
// TODO
prxied = proxyWrapper(url, optionsWithHeaders);
}
if (prxied) {
logger.debug(`Proxy for ${url}`);
} else {
logger.debug(`Requesting ${url}`);
}
// ua
if (!headersLowerCaseKeys.has('user-agent')) {
options.headers['user-agent'] = config.ua;
}
// Accept
if (!headersLowerCaseKeys.has('accept')) {
options.headers.Accept = '*/*';
}
let urlHandler;
try {
urlHandler = new URL(url);
} catch {
// ignore
}
if (
urlHandler && // referer
!headersLowerCaseKeys.has('referer')
) {
options.headers.referer = urlHandler.origin;
}
};
const httpWrap = (func: typeof http.request) => {
const origin = func;
const warpped: typeof http.request = function (...args) {
let url: string;
let options: http.RequestOptions;
if (args[0] instanceof URL || typeof args[0] === 'string') {
url = args[0].toString();
options = args[1] as http.RequestOptions;
} else {
options = args[0] as http.RequestOptions;
url = `${options.protocol}//${options.hostname || options.host}${options.path}`;
}
requestWrapper(url, options);
// @ts-expect-error apply
return origin.apply(this, args);
};
return warpped;
};
http.get = httpWrap(http.get);
https.get = httpWrap(https.get);
http.request = httpWrap(http.request);
https.request = httpWrap(https.request);

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",