surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
206 lines • 9.03 kB
JavaScript
;
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