f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
8.35 kB
import {
Cache,
HEADERS_FOR_IP_FORWARDING,
INTERNAL_SECRET_HEADER,
Env,
maskSensitiveInfo,
} from './index.js';
import { config as appConfig } from '../config/index.js';
import {
BodyInit,
Dispatcher,
fetch,
Headers,
HeadersInit,
ProxyAgent,
RequestInit,
} from 'undici';
import { socksDispatcher } from 'fetch-socks';
import { createLogger } from '../logging/logger.js';
const logger = createLogger('http');
const urlCount = Cache.getInstance<string, number>(
'url-count',
undefined,
'memory'
);
export class PossibleRecursiveRequestError extends Error {
constructor(message: string) {
super(message);
this.name = 'PossibleRecursiveRequestError';
}
}
export function makeUrlLogSafe(url: string) {
// for each component of the path, if it is longer than 10 characters, mask it
// and replace the query params of key 'password' with '****'
return url
.split('/')
.map((component) => {
if (component.length > 10 && !component.includes('.')) {
return maskSensitiveInfo(component);
}
return component;
})
.join('/')
.replace(/(?<![^?&])(password=[^&]+)/g, 'password=****')
.replace(/(?<![^?&])(apikey=[^&]+)/gi, 'apikey=****');
}
export interface RequestOptions {
timeout: number;
method?: string;
forwardIp?: string;
ignoreRecursion?: boolean;
headers?: HeadersInit;
body?: BodyInit;
forceProxy?: string;
rawOptions?: RequestInit;
}
export async function makeRequest(url: string, options: RequestOptions) {
const urlObj = new URL(url);
if (
appConfig.bootstrap.baseUrl &&
urlObj.origin === appConfig.bootstrap.baseUrl
) {
const internalUrl = new URL(appConfig.bootstrap.internalUrl);
urlObj.protocol = internalUrl.protocol;
urlObj.host = internalUrl.host;
urlObj.port = internalUrl.port;
}
if (appConfig.http.requestUrlMappings) {
for (const [key, value] of Object.entries(
appConfig.http.requestUrlMappings
)) {
if (urlObj.origin === key) {
const mappedUrl = new URL(value);
urlObj.protocol = mappedUrl.protocol;
urlObj.host = mappedUrl.host;
urlObj.port = mappedUrl.port;
break;
}
}
}
const { useProxy, proxyIndex } = shouldProxy(urlObj);
const headers = new Headers(options.headers);
if (options.forwardIp) {
for (const header of HEADERS_FOR_IP_FORWARDING) {
headers.set(header, options.forwardIp);
}
}
if (urlObj.toString().startsWith(appConfig.bootstrap.internalUrl)) {
headers.set(INTERNAL_SECRET_HEADER, appConfig.bootstrap.internalSecret);
}
let domainUserAgent = domainHasUserAgent(urlObj);
if (domainUserAgent) {
headers.set('User-Agent', domainUserAgent);
}
if (
['none', 'false', '', 'undefined'].includes(
(headers.get('User-Agent') ?? '').toLowerCase().trim()
)
) {
headers.delete('User-Agent');
}
// block recursive requests
const key = `${urlObj.toString()}-${options.forwardIp}`;
const currentCount = (await urlCount.get(key)) ?? 0;
if (
currentCount > appConfig.recursion.thresholdLimit &&
!options.ignoreRecursion
) {
logger.warn(
{ url: makeUrlLogSafe(urlObj.toString()), count: currentCount },
'detected possible recursive requests, blocking'
);
throw new PossibleRecursiveRequestError(
`Possible recursive request to ${urlObj.toString()}`
);
}
if (currentCount > 0) {
await urlCount.update(key, currentCount + 1);
} else {
await urlCount.set(key, 1, appConfig.recursion.thresholdWindow);
}
let dispatcher: Dispatcher | undefined;
if (options.forceProxy) {
dispatcher = getProxyAgent(options.forceProxy);
} else if (useProxy) {
dispatcher = getProxyAgent(appConfig.http.addonProxy[proxyIndex]);
}
logger.trace(
{
url: makeUrlLogSafe(urlObj.toString()),
method: options.method ?? 'GET',
proxy: useProxy
? `proxy-${proxyIndex + 1}`
: options.forceProxy
? 'forced'
: 'direct',
},
'http request'
);
let response;
try {
response = await fetch(urlObj.toString(), {
...options.rawOptions,
method: options.method,
body: options.body,
headers: headers,
dispatcher: dispatcher,
signal: AbortSignal.timeout(options.timeout),
});
} catch (err) {
if (
err instanceof Error &&
err.name === 'TypeError' &&
err.message === 'fetch failed' &&
err.cause
) {
const cause = { ...(err.cause as Record<string, any>) };
delete cause.stack;
logger.error({ cause }, 'fetch failed due to network error');
}
throw err;
}
return response;
}
const proxyAgents = new Map<string, Dispatcher>();
export function getProxyAgent(proxyUrl: string): Dispatcher | undefined {
if (!proxyUrl) {
return undefined;
}
let proxyAgent = proxyAgents.get(proxyUrl);
if (!proxyAgent) {
const proxyUrlObj = new URL(proxyUrl);
if (
proxyUrlObj.protocol === 'socks5:' ||
proxyUrlObj.protocol === 'socks5h:'
) {
proxyAgent = socksDispatcher({
type: 5,
port: parseInt(proxyUrlObj.port),
host: proxyUrlObj.hostname,
userId: proxyUrlObj.username || undefined,
password: proxyUrlObj.password || undefined,
});
} else {
proxyAgent = new ProxyAgent(proxyUrl);
}
}
return proxyAgent;
}
export function shouldProxy(url: URL): {
useProxy: boolean;
proxyIndex: number;
} {
let useProxy = false;
let hostname = url.hostname;
let proxyIndex = -1;
if (!appConfig.http.addonProxy || appConfig.http.addonProxy.length === 0) {
return { useProxy: false, proxyIndex };
}
if (hostname === 'localhost') {
return { useProxy: false, proxyIndex };
}
useProxy = true;
if (
appConfig.http.addonProxyConfig &&
Object.keys(appConfig.http.addonProxyConfig).length > 0
) {
for (const [ruleHostname, ruleValue] of Object.entries(
appConfig.http.addonProxyConfig
)) {
const ruleProxyIndexOrBool = String(ruleValue);
if (
['true', 'false'].includes(ruleProxyIndexOrBool) === false &&
isNaN(parseInt(ruleProxyIndexOrBool))
) {
logger.error(
{ hostname: ruleHostname, value: ruleProxyIndexOrBool },
'invalid proxy config value'
);
continue;
}
if (ruleHostname === '*') {
useProxy = !(ruleProxyIndexOrBool === 'false');
proxyIndex = Number.isInteger(parseInt(ruleProxyIndexOrBool))
? parseInt(ruleProxyIndexOrBool)
: ruleProxyIndexOrBool === 'true'
? 0
: -1;
} else if (ruleHostname.startsWith('*')) {
if (hostname.endsWith(ruleHostname.slice(1))) {
useProxy = !(ruleProxyIndexOrBool === 'false');
proxyIndex = Number.isInteger(parseInt(ruleProxyIndexOrBool))
? parseInt(ruleProxyIndexOrBool)
: ruleProxyIndexOrBool === 'true'
? 0
: -1;
}
}
if (hostname === ruleHostname) {
useProxy = !(ruleProxyIndexOrBool === 'false');
proxyIndex = Number.isInteger(parseInt(ruleProxyIndexOrBool))
? parseInt(ruleProxyIndexOrBool)
: ruleProxyIndexOrBool === 'true'
? 0
: -1;
}
}
} else {
proxyIndex = 0;
}
if (useProxy && appConfig.http.addonProxy[proxyIndex] === undefined) {
logger.error({ proxyIndex }, 'proxy index out of range');
return { useProxy: false, proxyIndex: -1 };
}
return { useProxy, proxyIndex };
}
export function domainHasUserAgent(url: URL) {
let userAgent: string | undefined;
let hostname = url.hostname;
if (
!appConfig.http.hostnameUserAgentOverrides ||
Object.keys(appConfig.http.hostnameUserAgentOverrides).length === 0
) {
return undefined;
}
const mappings = Object.entries(appConfig.http.hostnameUserAgentOverrides);
for (const [ruleHostname, ruleUserAgent] of mappings) {
if (ruleHostname === '*') {
userAgent = ruleUserAgent;
} else if (ruleHostname.startsWith('*')) {
if (hostname.endsWith(ruleHostname.slice(1))) {
userAgent = ruleUserAgent;
}
} else if (hostname === ruleHostname) {
userAgent = ruleUserAgent;
}
}
return userAgent;
}