UNPKG

surgio

Version:

Generating rules for Surge, Clash, Quantumult like a PRO

206 lines 9.03 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = require("@surgio/logger"); const lodash_1 = __importDefault(require("lodash")); const constant_1 = require("../constant"); const cache_1 = require("../utils/cache"); const config_1 = require("../config"); const env_flag_1 = require("../utils/env-flag"); const http_client_1 = __importStar(require("../utils/http-client")); const utils_1 = require("../utils"); const validators_1 = require("../validators"); const logger = (0, logger_1.createLogger)({ service: 'surgio:Provider', }); class Provider { name; type; config; // Whether the provider supports getting subscription user info supportGetSubscriptionUserInfo = false; // Headers that will be passed to the upstream server passGatewayRequestHeaders; constructor(name, config) { this.name = name; const result = validators_1.ProviderValidator.safeParse(config); // istanbul ignore next if (!result.success) { throw new utils_1.SurgioError('Provider 配置校验失败', { cause: result.error, providerName: name, }); } this.config = result.data; this.type = result.data.type; this.passGatewayRequestHeaders = ((0, config_1.getConfig)()?.gateway?.passRequestHeaders ?? []).map((header) => header.toLowerCase()); if ((0, config_1.getConfig)()?.gateway?.passRequestUserAgent) { if (!this.passGatewayRequestHeaders.includes('user-agent')) { this.passGatewayRequestHeaders.push('user-agent'); } } for (const header of constant_1.PASS_GATEWAY_REQUEST_HEADERS_WHITELIST) { if (!this.passGatewayRequestHeaders.includes(header)) { this.passGatewayRequestHeaders.push(header); } } } /** * Generate a cache key for a provider resource based on an identifier. * * @param identifier - A unique identifier for the resource (typically user-agent + URL) * @returns MD5-hashed cache key */ static getResourceCacheKey(...identifiers) { const identifier = []; for (const identifierItem of identifiers) { if (typeof identifierItem === 'string') { identifier.push(identifierItem); } else { identifier.push(JSON.stringify(identifierItem)); } } return `${constant_1.CACHE_KEYS.Provider}:${(0, utils_1.toMD5)(identifier.join(''))}`; } /** * Fetch a cacheable resource from a URL with specified headers. * Returns cached response if available within the cache TTL. * * @param url - The subscription URL to fetch * @param headers - HTTP headers to include in the request * @param cacheKey - Cache key for storing/retrieving the response (auto-generated if not provided) * @returns Subscription data including body and optional user info */ static async requestCacheableResource(url, headers, cacheKey = this.getResourceCacheKey(headers, url)) { logger.debug('requestCacheableResource: %s %j %s', url, headers, cacheKey); const requestResource = async () => { const res = await http_client_1.default.get(url, { responseType: 'text', headers, }); const subsciptionCacheItem = { body: res.body, }; if (res.headers['subscription-userinfo']) { subsciptionCacheItem.subscriptionUserInfo = (0, utils_1.parseSubscriptionUserInfo)(res.headers['subscription-userinfo']); logger.debug('%s received subscription userinfo - raw: %s | parsed: %j', url, res.headers['subscription-userinfo'], subsciptionCacheItem.subscriptionUserInfo); } return subsciptionCacheItem; }; const cachedValue = await cache_1.unifiedCache.get(cacheKey); if (cachedValue) { logger.debug('requestCacheableResource: %s %j %s: cached', url, headers, cacheKey); } try { return cachedValue ? cachedValue : await (async () => { const subsciptionCacheItem = await requestResource(); await cache_1.unifiedCache.set(cacheKey, subsciptionCacheItem, (0, env_flag_1.getProviderCacheMaxage)()); logger.debug('requestCacheableResource: %s %j %s: not cached', url, headers, cacheKey); return subsciptionCacheItem; })(); } catch (error) { logger.error('requestCacheableResource: %s %j %s', url, headers, cacheKey, error); throw error; } } /** * Determine the HTTP headers to use for provider requests. * Filters headers based on the gateway's passRequestHeaders configuration. * * @param requestUserAgent - Optional User-Agent from the gateway request * @param requestHeaders - Optional custom headers from the gateway request * @returns Filtered headers object with required user-agent * * @remarks * - Always includes the user-agent header * - If user doesn't want to pass the user-agent header from the gateway request, a * default user-agent from the provider config will be used * - The requestUserAgent parameter takes priority over requestHeaders['user-agent'] * - Filters additional headers based on passGatewayRequestHeaders allowlist * - If passGatewayRequestHeaders is empty, only user-agent is returned * - The returned object always contains 'user-agent' regardless of configuration * * @example * ```typescript * // With passGatewayRequestHeaders: ['accept-language'] * const headers = provider.determineRequestHeaders( * 'custom-ua', * { 'accept-language': 'en-US', 'x-custom': 'value' } * ) * // Returns: { 'user-agent': 'custom-ua', 'accept-language': 'en-US' } * // Note: 'x-custom' is filtered out * ``` */ determineRequestHeaders(requestUserAgent, requestHeaders) { const passRequestUserAgent = this.passGatewayRequestHeaders.includes('user-agent'); const userAgent = (0, http_client_1.getUserAgent)(passRequestUserAgent ? requestUserAgent || requestHeaders?.['user-agent'] || this.config.requestUserAgent : this.config.requestUserAgent); // Normalize incoming headers to lowercase keys for case-insensitive matching // Always exclude user-agent from the normalized headers const normalizedHeaders = requestHeaders ? Object.fromEntries(Object.entries(requestHeaders) .map(([k, v]) => [k.toLowerCase(), v]) .filter(([k]) => k !== 'user-agent')) : {}; // Filter headers based on allowlist const filteredHeaders = lodash_1.default.pick(normalizedHeaders, this.passGatewayRequestHeaders); return { ...filteredHeaders, 'user-agent': userAgent, }; } get nextPort() { if (this.config.startPort) { return this.config.startPort++; } return 0; } // istanbul ignore next getSubscriptionUserInfo = async () => { throw new Error('此 Provider 不支持该功能'); }; } exports.default = Provider; //# sourceMappingURL=Provider.js.map