f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
5.59 kB
import z from 'zod';
import { StreamProxyConfig } from '../db/schemas.js';
import {
Cache,
createLogger,
maskSensitiveInfo,
Env,
constants,
} from '../utils/index.js';
import { config as appConfig } from '../config/index.js';
const logger = createLogger('proxy');
const cache = Cache.getInstance<string, string>('publicIp');
export interface ProxyStream {
url: string;
filename?: string;
type?: 'nzb' | 'stream';
headers?: {
request?: Record<string, string>;
response?: Record<string, string>;
};
}
type ValidatedStreamProxyConfig = StreamProxyConfig & {
id: 'mediaflow' | 'stremthru' | 'builtin';
url: string;
credentials: string;
};
export abstract class BaseProxy {
protected readonly config: ValidatedStreamProxyConfig;
private readonly PRIVATE_CIDR =
/^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
constructor(config: StreamProxyConfig) {
if (config.id === constants.BUILTIN_SERVICE) {
config.url = appConfig.bootstrap.baseUrl;
config.publicUrl = undefined;
}
if (!config.id || !config.credentials || !config.url) {
throw new Error('Proxy configuration is missing');
}
this.config = {
enabled: config.enabled ?? false,
id: config.id,
url: config.url,
publicUrl: config.publicUrl,
credentials: config.credentials,
publicIp: config.publicIp,
proxiedAddons: config.proxiedAddons,
proxiedServices: config.proxiedServices,
};
}
public getConfig(): StreamProxyConfig {
return this.config;
}
protected abstract generateProxyUrl(endpoint: string): URL;
protected abstract getPublicIpEndpoint(): string;
protected abstract getPublicIpFromResponse(data: any): string | null;
protected abstract generateStreamUrls(
streams: ProxyStream[],
encrypt?: boolean
): Promise<string[] | null>;
public async getPublicIp(): Promise<string | null> {
if (!this.config.url) {
logger.error('proxy url is missing');
throw new Error('Proxy URL is missing');
}
if (this.config.publicIp) {
return this.config.publicIp;
}
const proxyUrl = new URL(this.config.url.replace(/\/$/, ''));
if (this.PRIVATE_CIDR.test(proxyUrl.hostname)) {
logger.warn('proxy url is a private ip, skipping public ip lookup');
return null;
}
const cacheKey = `${this.config.id}:${this.config.url}:${this.config.credentials}`;
const cachedPublicIp = cache ? await cache.get(cacheKey) : null;
if (cachedPublicIp) {
logger.debug(
{ proxy: this.config.id },
'returning cached proxy public ip'
);
return cachedPublicIp;
}
const ipUrl = this.generateProxyUrl(this.getPublicIpEndpoint());
logger.debug(
{
proxy: this.config.id,
endpoint: `${ipUrl.protocol}//${maskSensitiveInfo(ipUrl.hostname)}${ipUrl.pathname}`,
},
'fetching proxy public ip'
);
const response = await fetch(ipUrl.toString(), {
method: 'GET',
headers: this.getHeaders(),
signal: AbortSignal.timeout(30000), // 30 second timeout
});
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`);
}
const data = await response.json();
const publicIp = this.getPublicIpFromResponse(data);
const { error, success } = z
.union([z.ipv4(), z.ipv6()])
.safeParse(publicIp);
if (error || !success) {
logger.error(
{ proxy: this.config.id, ip: publicIp },
'proxy returned invalid ip'
);
throw new Error(`Proxy did not respond with a valid public IP`);
}
if (publicIp && cache) {
await cache.set(cacheKey, publicIp, appConfig.proxy.ip.cacheTtl);
} else {
logger.error(
{ proxy: this.config.id },
'proxy did not return a public ip'
);
throw new Error('Proxy did not respond with a public IP');
}
return publicIp;
}
protected abstract getHeaders(): Record<string, string>;
public async generateUrls(
streams: ProxyStream[],
encrypt?: boolean
): Promise<string[] | { error: string } | null> {
if (!streams.length) {
return [];
}
if (!this.config) {
throw new Error('Proxy configuration is missing');
}
try {
let urls = await this.generateStreamUrls(streams, encrypt);
const publicUrl = this.config.publicUrl;
if (publicUrl && urls) {
const publicUrlObj = new URL(publicUrl);
const publicBasePath = publicUrlObj.pathname.replace(/\/+$/, ''); // remove trailing slash
urls = urls.map((url) => {
const urlObj = new URL(url);
// Set protocol, hostname, and port from publicUrl
urlObj.protocol = publicUrlObj.protocol;
urlObj.hostname = publicUrlObj.hostname;
urlObj.port = publicUrlObj.port;
// Adjust pathname: join publicUrl's base path with the original path, avoiding duplicate slashes
const origPath = urlObj.pathname.replace(/^\/+/, ''); // remove leading slash
urlObj.pathname = publicBasePath
? `${publicBasePath}/${origPath}`.replace(/\/{2,}/g, '/')
: `/${origPath}`;
return urlObj.toString();
});
}
return urls;
} catch (error) {
logger.error(
{
proxy: this.config.id,
err: error instanceof Error ? error.message : String(error),
},
'failed to generate proxy urls'
);
return { error: error instanceof Error ? error.message : String(error) };
}
}
}