diff --git a/lib/provider/ClashProvider.ts b/lib/provider/ClashProvider.ts index f06179797..e915a2696 100644 --- a/lib/provider/ClashProvider.ts +++ b/lib/provider/ClashProvider.ts @@ -12,164 +12,17 @@ import { NodeTypeEnum, ShadowsocksNodeConfig, ShadowsocksrNodeConfig, - SnellNodeConfig, + SnellNodeConfig, SubscriptionUserinfo, VmessNodeConfig, } from '../types'; -import { ConfigCache } from '../utils'; +import { parseSubscriptionUserInfo } from '../utils'; +import { ConfigCache, SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; import { NETWORK_TIMEOUT } from '../utils/constant'; import Provider from './Provider'; type SupportConfigTypes = ShadowsocksNodeConfig|VmessNodeConfig|HttpsNodeConfig|HttpNodeConfig|ShadowsocksrNodeConfig|SnellNodeConfig; export default class ClashProvider extends Provider { - public static async getClashSubscription(url: string, udpRelay?: boolean): Promise> { - assert(url, '未指定订阅地址 url'); - - const response = ConfigCache.has(url) ? ConfigCache.get(url) : await (async () => { - const res = await got.get(url, { - timeout: NETWORK_TIMEOUT, - }); - - ConfigCache.set(url, res.body); - - return res.body; - })(); - let clashConfig; - - try { - clashConfig = yaml.parse(response); - - if (typeof clashConfig === 'string') { - throw new Error(); - } - } catch (err) { - throw new Error(`${url} 不是一个合法的 YAML 文件`); - } - - const proxyList: any[] = clashConfig.Proxy; - - return proxyList.map(item => { - switch (item.type) { - case 'ss': - // istanbul ignore next - if (item.plugin && !['obfs', 'v2ray-plugin'].includes(item.plugin)) { - logger.warn(`不支持从 Clash 订阅中读取 ${item.plugin} 类型的 Shadowsocks 节点,节点 ${item.name} 会被省略`); - return null; - } - // istanbul ignore next - if (item.plugin === 'v2ray-plugin' && item['plugin-opts'].mode.toLowerCase() === 'quic') { - logger.warn(`不支持从 Clash 订阅中读取 QUIC 模式的 Shadowsocks 节点,节点 ${item.name} 会被省略`); - return null; - } - - return { - type: NodeTypeEnum.Shadowsocks, - nodeName: item.name, - hostname: item.server, - port: item.port, - method: item.cipher, - password: item.password, - 'udp-relay': resolveUdpRelay(item.udp, udpRelay), - ...(item.plugin && item.plugin === 'obfs' ? { - obfs: item['plugin-opts'].mode, - 'obfs-host': item['plugin-opts'].host || 'www.bing.com', - } : null), - ...(item.obfs ? { - obfs: item.obfs, - 'obfs-host': item['obfs-host'] || 'www.bing.com', - } : null), - ...(item.plugin && item.plugin === 'v2ray-plugin' && item['plugin-opts'].mode === 'websocket' ? { - obfs: item['plugin-opts'].tls === true ? 'wss' : 'ws', - 'obfs-host': item['plugin-opts'].host || item.server, - 'obfs-uri': item['plugin-opts'].path || '/', - ...(item['plugin-opts'].tls === true ? { - skipCertVerify: item['plugin-opts']['skip-cert-verify'] === true, - } : null), - } : null), - }; - - case 'vmess': - // istanbul ignore next - if (['kcp', 'http'].indexOf(item.network) > -1) { - logger.warn(`不支持从 Clash 订阅中读取 network 类型为 ${item.network} 的 Vmess 节点,节点 ${item.name} 会被省略`); - return null; - } - - return { - type: NodeTypeEnum.Vmess, - nodeName: item.name, - hostname: item.server, - port: item.port, - uuid: item.uuid, - alterId: item.alterId ? `${item.alterId}` : '0', - method: item.cipher || 'auto', - udp: resolveUdpRelay(item.udp, udpRelay), - tls: item.tls ?? false, - network: item.network || 'tcp', - ...(item.network === 'ws' ? { - path: _.get(item, 'ws-path', '/'), - host: _.get(item, 'ws-headers.Host', ''), - } : null), - ...(item.tls ? { - skipCertVerify: item['skip-cert-verify'] === true, - } : null), - }; - - case 'http': - if (!item.tls) { - return { - type: NodeTypeEnum.HTTP, - nodeName: item.name, - hostname: item.server, - port: item.port, - username: item.username /* istanbul ignore next */ || '', - password: item.password /* istanbul ignore next */ || '', - }; - } - - return { - type: NodeTypeEnum.HTTPS, - nodeName: item.name, - hostname: item.server, - port: item.port, - username: item.username || '', - password: item.password || '', - skipCertVerify: item['skip-cert-verify'] === true, - }; - - case 'snell': - return { - type: NodeTypeEnum.Snell, - nodeName: item.name, - hostname: item.server, - port: item.port, - psk: item.psk, - obfs: _.get(item, 'obfs-opts.mode', 'http'), - }; - - // istanbul ignore next - case 'ssr': - return { - type: NodeTypeEnum.Shadowsocksr, - nodeName: item.name, - hostname: item.server, - port: item.port, - password: item.password, - obfs: item.obfs, - obfsparam: item.obfsparam, - protocol: item.protocol, - protoparam: item.protocolparam, - method: item.cipher, - }; - - default: - logger.warn(`不支持从 Clash 订阅中读取 ${item.type} 的节点,节点 ${item.name} 会被省略`); - return null; - } - }) - .filter(item => !!item); - } - public readonly url: string; public readonly udpRelay?: boolean; @@ -198,11 +51,193 @@ export default class ClashProvider extends Provider { this.udpRelay = config.udpRelay; } - public getNodeList(): ReturnType { - return ClashProvider.getClashSubscription(this.url, this.udpRelay); + public async getSubscriptionUserInfo(): Promise { + const { subscriptionUserinfo } = await getClashSubscription(this.url, this.udpRelay); + + if (subscriptionUserinfo) { + return subscriptionUserinfo; + } + return null; + } + + public async getNodeList(): Promise> { + const { nodeList } = await getClashSubscription(this.url, this.udpRelay); + + return nodeList; } } +export const getClashSubscription = async ( + url: string, + udpRelay?: boolean, +): Promise<{ + readonly nodeList: ReadonlyArray; + readonly subscriptionUserinfo?: SubscriptionUserinfo; +}> => { + assert(url, '未指定订阅地址 url'); + + const response: SubsciptionCacheItem = SubscriptionCache.has(url) + ? SubscriptionCache.get(url) + : await ( + async () => { + const res = await got.get(url, { + timeout: NETWORK_TIMEOUT, + responseType: 'text', + }); + const subsciptionCacheItem: SubsciptionCacheItem = { + body: res.body, + }; + + if (res.headers['subscription-userinfo']) { + subsciptionCacheItem.subscriptionUserinfo = parseSubscriptionUserInfo(res.headers['subscription-userinfo'] as string); + } + + SubscriptionCache.set(url, subsciptionCacheItem); + + return subsciptionCacheItem; + } + )(); + let clashConfig; + + try { + clashConfig = yaml.parse(response.body); + + if (typeof clashConfig === 'string') { + throw new Error(); + } + } catch (err) { + throw new Error(`${url} 不是一个合法的 YAML 文件`); + } + + const proxyList: any[] = clashConfig.Proxy; + + const nodeList = proxyList.map(item => { + switch (item.type) { + case 'ss': + // istanbul ignore next + if (item.plugin && !['obfs', 'v2ray-plugin'].includes(item.plugin)) { + logger.warn(`不支持从 Clash 订阅中读取 ${item.plugin} 类型的 Shadowsocks 节点,节点 ${item.name} 会被省略`); + return null; + } + // istanbul ignore next + if (item.plugin === 'v2ray-plugin' && item['plugin-opts'].mode.toLowerCase() === 'quic') { + logger.warn(`不支持从 Clash 订阅中读取 QUIC 模式的 Shadowsocks 节点,节点 ${item.name} 会被省略`); + return null; + } + + return { + type: NodeTypeEnum.Shadowsocks, + nodeName: item.name, + hostname: item.server, + port: item.port, + method: item.cipher, + password: item.password, + 'udp-relay': resolveUdpRelay(item.udp, udpRelay), + ...(item.plugin && item.plugin === 'obfs' ? { + obfs: item['plugin-opts'].mode, + 'obfs-host': item['plugin-opts'].host || 'www.bing.com', + } : null), + ...(item.obfs ? { + obfs: item.obfs, + 'obfs-host': item['obfs-host'] || 'www.bing.com', + } : null), + ...(item.plugin && item.plugin === 'v2ray-plugin' && item['plugin-opts'].mode === 'websocket' ? { + obfs: item['plugin-opts'].tls === true ? 'wss' : 'ws', + 'obfs-host': item['plugin-opts'].host || item.server, + 'obfs-uri': item['plugin-opts'].path || '/', + ...(item['plugin-opts'].tls === true ? { + skipCertVerify: item['plugin-opts']['skip-cert-verify'] === true, + } : null), + } : null), + }; + + case 'vmess': + // istanbul ignore next + if (['kcp', 'http'].indexOf(item.network) > -1) { + logger.warn(`不支持从 Clash 订阅中读取 network 类型为 ${item.network} 的 Vmess 节点,节点 ${item.name} 会被省略`); + return null; + } + + return { + type: NodeTypeEnum.Vmess, + nodeName: item.name, + hostname: item.server, + port: item.port, + uuid: item.uuid, + alterId: item.alterId ? `${item.alterId}` : '0', + method: item.cipher || 'auto', + udp: resolveUdpRelay(item.udp, udpRelay), + tls: item.tls ?? false, + network: item.network || 'tcp', + ...(item.network === 'ws' ? { + path: _.get(item, 'ws-path', '/'), + host: _.get(item, 'ws-headers.Host', ''), + } : null), + ...(item.tls ? { + skipCertVerify: item['skip-cert-verify'] === true, + } : null), + }; + + case 'http': + if (!item.tls) { + return { + type: NodeTypeEnum.HTTP, + nodeName: item.name, + hostname: item.server, + port: item.port, + username: item.username /* istanbul ignore next */ || '', + password: item.password /* istanbul ignore next */ || '', + }; + } + + return { + type: NodeTypeEnum.HTTPS, + nodeName: item.name, + hostname: item.server, + port: item.port, + username: item.username || '', + password: item.password || '', + skipCertVerify: item['skip-cert-verify'] === true, + }; + + case 'snell': + return { + type: NodeTypeEnum.Snell, + nodeName: item.name, + hostname: item.server, + port: item.port, + psk: item.psk, + obfs: _.get(item, 'obfs-opts.mode', 'http'), + }; + + // istanbul ignore next + case 'ssr': + return { + type: NodeTypeEnum.Shadowsocksr, + nodeName: item.name, + hostname: item.server, + port: item.port, + password: item.password, + obfs: item.obfs, + obfsparam: item.obfsparam, + protocol: item.protocol, + protoparam: item.protocolparam, + method: item.cipher, + }; + + default: + logger.warn(`不支持从 Clash 订阅中读取 ${item.type} 的节点,节点 ${item.name} 会被省略`); + return null; + } + }) + .filter(item => !!item); + + return { + nodeList, + subscriptionUserinfo: response.subscriptionUserinfo, + }; +}; + function resolveUdpRelay(val?: boolean, defaultVal = false): boolean { if (val !== void 0) { return val; diff --git a/lib/provider/Provider.ts b/lib/provider/Provider.ts index 96614c9e6..d0e5045af 100644 --- a/lib/provider/Provider.ts +++ b/lib/provider/Provider.ts @@ -18,6 +18,7 @@ export default class Provider { public readonly tfo?: boolean; public readonly mptcp?: boolean; public readonly renameNode?: ProviderConfig['renameNode']; + private startPort?: number; constructor(public name: string, config: ProviderConfig) { diff --git a/lib/provider/ShadowsocksSubscribeProvider.ts b/lib/provider/ShadowsocksSubscribeProvider.ts index bd56f67c2..a788fab6c 100644 --- a/lib/provider/ShadowsocksSubscribeProvider.ts +++ b/lib/provider/ShadowsocksSubscribeProvider.ts @@ -1,8 +1,24 @@ import Joi from '@hapi/joi'; -import { ShadowsocksSubscribeProviderConfig } from '../types'; -import { getShadowsocksSubscription } from '../utils'; +import assert from 'assert'; +import got from 'got'; +import { default as legacyUrl } from 'url'; +import { createLogger } from '@surgio/logger'; + +import { + NodeTypeEnum, + ShadowsocksNodeConfig, + ShadowsocksSubscribeProviderConfig, + SubscriptionUserinfo, +} from '../types'; +import { decodeStringList, fromBase64, fromUrlSafeBase64, parseSubscriptionUserInfo } from '../utils'; +import { SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; +import { NETWORK_TIMEOUT } from '../utils/constant'; import Provider from './Provider'; +const logger = createLogger({ + service: 'surgio:ShadowsocksSubscribeProvider', +}); + export default class ShadowsocksSubscribeProvider extends Provider { public readonly url: string; public readonly udpRelay?: boolean; @@ -34,7 +50,92 @@ export default class ShadowsocksSubscribeProvider extends Provider { this.udpRelay = config.udpRelay; } - public getNodeList(): ReturnType { - return getShadowsocksSubscription(this.url, this.udpRelay); + public async getSubscriptionUserInfo(): Promise { + const { subscriptionUserinfo } = await getShadowsocksSubscription(this.url, this.udpRelay); + + if (subscriptionUserinfo) { + return subscriptionUserinfo; + } + return null; + } + + public async getNodeList(): Promise> { + const { nodeList } = await getShadowsocksSubscription(this.url, this.udpRelay); + + return nodeList; } } + +/** + * @see https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html + */ +export const getShadowsocksSubscription = async ( + url: string, + udpRelay?: boolean, +): Promise<{ + readonly nodeList: ReadonlyArray; + readonly subscriptionUserinfo?: SubscriptionUserinfo; +}> => { + assert(url, '未指定订阅地址 url'); + + async function requestConfigFromRemote(): ReturnType { + const response: SubsciptionCacheItem = SubscriptionCache.has(url) + ? SubscriptionCache.get(url) + : await ( + async () => { + const res = await got.get(url, { + timeout: NETWORK_TIMEOUT, + responseType: 'text', + }); + const subsciptionCacheItem: SubsciptionCacheItem = { + body: res.body, + }; + + if (res.headers['subscription-userinfo']) { + subsciptionCacheItem.subscriptionUserinfo = parseSubscriptionUserInfo(res.headers['subscription-userinfo'] as string); + } + + SubscriptionCache.set(url, subsciptionCacheItem); + + return subsciptionCacheItem; + } + )(); + + const nodeList = fromBase64(response.body) + .split('\n') + .filter(item => !!item && item.startsWith('ss://')) + .map(item => { + logger.debug('Parsing Shadowsocks URI', item); + const scheme = legacyUrl.parse(item, true); + const userInfo = fromUrlSafeBase64(scheme.auth).split(':'); + const pluginInfo = typeof scheme.query.plugin === 'string' ? decodeStringList(scheme.query.plugin.split(';')) : {}; + + return { + type: NodeTypeEnum.Shadowsocks, + nodeName: decodeURIComponent(scheme.hash.replace('#', '')), + hostname: scheme.hostname, + port: scheme.port, + method: userInfo[0], + password: userInfo[1], + ...(typeof udpRelay === 'boolean' ? { + 'udp-relay': udpRelay, + } : null), + ...(pluginInfo['obfs-local'] ? { + obfs: pluginInfo.obfs, + 'obfs-host': pluginInfo['obfs-host'], + } : null), + ...(pluginInfo['v2ray-plugin'] ? { + obfs: pluginInfo.tls ? 'wss' : 'ws', + 'obfs-host': pluginInfo.host, + } : null), + }; + }); + + return { + nodeList, + subscriptionUserinfo: response.subscriptionUserinfo, + }; + } + + return await requestConfigFromRemote(); +}; diff --git a/lib/provider/ShadowsocksrSubscribeProvider.ts b/lib/provider/ShadowsocksrSubscribeProvider.ts index 4bb3f14cd..675be4bd5 100644 --- a/lib/provider/ShadowsocksrSubscribeProvider.ts +++ b/lib/provider/ShadowsocksrSubscribeProvider.ts @@ -1,6 +1,12 @@ import Joi from '@hapi/joi'; -import { ShadowsocksrSubscribeProviderConfig } from '../types'; -import { getShadowsocksrSubscription } from '../utils'; +import assert from 'assert'; +import got from 'got'; + +import { ShadowsocksrNodeConfig, ShadowsocksrSubscribeProviderConfig, SubscriptionUserinfo } from '../types'; +import { fromBase64, parseSubscriptionUserInfo } from '../utils'; +import { SubsciptionCacheItem, SubscriptionCache } from '../utils/cache'; +import { NETWORK_TIMEOUT } from '../utils/constant'; +import { parseSSRUri } from '../utils/ssr'; import Provider from './Provider'; export default class ShadowsocksrSubscribeProvider extends Provider { @@ -34,7 +40,64 @@ export default class ShadowsocksrSubscribeProvider extends Provider { this.udpRelay = config.udpRelay; } - public getNodeList(): ReturnType { - return getShadowsocksrSubscription(this.url, this.udpRelay); + public async getNodeList(): Promise> { + const { nodeList } = await getShadowsocksrSubscription(this.url, this.udpRelay); + + return nodeList; } } + +export const getShadowsocksrSubscription = async ( + url: string, + udpRelay?: boolean, +): Promise<{ + readonly nodeList: ReadonlyArray; + readonly subscriptionUserinfo?: SubscriptionUserinfo; +}> => { + assert(url, '未指定订阅地址 url'); + + async function requestConfigFromRemote(): ReturnType { + const response: SubsciptionCacheItem = SubscriptionCache.has(url) + ? SubscriptionCache.get(url) + : await ( + async () => { + const res = await got.get(url, { + timeout: NETWORK_TIMEOUT, + responseType: 'text', + }); + const subsciptionCacheItem: SubsciptionCacheItem = { + body: res.body, + }; + + if (res.headers['subscription-userinfo']) { + subsciptionCacheItem.subscriptionUserinfo = parseSubscriptionUserInfo(res.headers['subscription-userinfo'] as string); + } + + SubscriptionCache.set(url, subsciptionCacheItem); + + return subsciptionCacheItem; + } + )(); + + const nodeList = fromBase64(response.body) + .split('\n') + .filter(item => !!item && item.startsWith('ssr://')) + .map(str => { + const nodeConfig = parseSSRUri(str); + + if (udpRelay !== void 0) { + (nodeConfig['udp-relay'] as boolean) = udpRelay; + } + + return nodeConfig; + }); + + return { + nodeList, + subscriptionUserinfo: response.subscriptionUserinfo, + }; + } + + return await requestConfigFromRemote(); +}; + diff --git a/lib/types.ts b/lib/types.ts index d732dab37..15c091516 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -220,6 +220,13 @@ export interface CreateServerOptions { readonly cwd?: string; } +export interface SubscriptionUserinfo { + readonly upload: number; + readonly download: number; + readonly total: number; + readonly expire: number; +} + export type NodeFilterType = (nodeConfig: PossibleNodeConfigType) => boolean; export type NodeNameFilterType = (simpleNodeConfig: SimpleNodeConfig) => boolean; diff --git a/lib/utils/cache.ts b/lib/utils/cache.ts new file mode 100644 index 000000000..c058f6b69 --- /dev/null +++ b/lib/utils/cache.ts @@ -0,0 +1,17 @@ +import LRU from 'lru-cache'; + +import { SubscriptionUserinfo } from '../types'; +import { PROVIDER_CACHE_MAXAGE } from './constant'; + +export interface SubsciptionCacheItem { + readonly body: string; + subscriptionUserinfo?: SubscriptionUserinfo, // tslint:disable-line:readonly-keyword +} + +export const ConfigCache = new LRU({ + maxAge: PROVIDER_CACHE_MAXAGE, +}); + +export const SubscriptionCache = new LRU({ + maxAge: PROVIDER_CACHE_MAXAGE, +}); diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 9f5ea729d..fb8cbc96e 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -3,7 +3,6 @@ import Debug from 'debug'; import fs from 'fs-extra'; import got from 'got'; import _ from 'lodash'; -import LRU from 'lru-cache'; import os from 'os'; import path from 'path'; import queryString from 'query-string'; @@ -26,21 +25,18 @@ import { ShadowsocksrNodeConfig, SimpleNodeConfig, SnellNodeConfig, - SortedNodeNameFilterType, + SortedNodeNameFilterType, SubscriptionUserinfo, VmessNodeConfig, } from '../types'; -import { NETWORK_TIMEOUT, OBFS_UA, PROVIDER_CACHE_MAXAGE, PROXY_TEST_INTERVAL, PROXY_TEST_URL } from './constant'; +import { NETWORK_TIMEOUT, OBFS_UA, PROXY_TEST_INTERVAL, PROXY_TEST_URL } from './constant'; import { isIp } from './dns'; import { validateFilter } from './filter'; import { parseSSRUri } from './ssr'; import { formatVmessUri } from './v2ray'; +import { ConfigCache, SubsciptionCacheItem, SubscriptionCache } from './cache'; const debug = Debug('surgio:utils'); -export const ConfigCache = new LRU({ - maxAge: PROVIDER_CACHE_MAXAGE, -}); - // istanbul ignore next export const resolveRoot = (...args: readonly string[]): string => path.join(__dirname, '../../', ...args); @@ -149,95 +145,6 @@ export const getShadowsocksJSONConfig = async ( return await requestConfigFromRemote(); }; -/** - * @see https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html - */ -export const getShadowsocksSubscription = async ( - url: string, - udpRelay?: boolean, -): Promise> => { - assert(url, '未指定订阅地址 url'); - - async function requestConfigFromRemote(): Promise> { - const response = ConfigCache.has(url) ? ConfigCache.get(url) : await (async () => { - const res = await got.get(url, { - timeout: NETWORK_TIMEOUT, - }); - - ConfigCache.set(url, res.body); - - return res.body; - })(); - - const configList = fromBase64(response).split('\n') - .filter(item => !!item && item.startsWith("ss://")); - - return configList.map(item => { - debug('SS URI', item); - const scheme = legacyUrl.parse(item, true); - const userInfo = fromUrlSafeBase64(scheme.auth).split(':'); - const pluginInfo = typeof scheme.query.plugin === 'string' ? decodeStringList(scheme.query.plugin.split(';')) : {}; - - return { - type: NodeTypeEnum.Shadowsocks, - nodeName: decodeURIComponent(scheme.hash.replace('#', '')), - hostname: scheme.hostname, - port: scheme.port, - method: userInfo[0], - password: userInfo[1], - ...(typeof udpRelay === 'boolean' ? { - 'udp-relay': udpRelay, - } : null), - ...(pluginInfo['obfs-local'] ? { - obfs: pluginInfo.obfs, - 'obfs-host': pluginInfo['obfs-host'], - } : null), - ...(pluginInfo['v2ray-plugin'] ? { - obfs: pluginInfo.tls ? 'wss' : 'ws', - 'obfs-host': pluginInfo.host, - } : null), - }; - }); - } - - return await requestConfigFromRemote(); -}; - -export const getShadowsocksrSubscription = async ( - url: string, - udpRelay?: boolean, -): Promise> => { - assert(url, '未指定订阅地址 url'); - - async function requestConfigFromRemote(): Promise> { - const response = ConfigCache.has(url) ? ConfigCache.get(url) : await (async () => { - const res = await got.get(url, { - timeout: NETWORK_TIMEOUT, - }); - - ConfigCache.set(url, res.body); - - return res.body; - })(); - - const configList = fromBase64(response) - .split('\n') - .filter(item => !!item && item.startsWith("ssr://")); - - return configList.map(str => { - const nodeConfig = parseSSRUri(str); - - if (udpRelay !== void 0) { - (nodeConfig['udp-relay'] as boolean) = udpRelay; - } - - return nodeConfig; - }); - } - - return await requestConfigFromRemote(); -}; - export const getV2rayNSubscription = async ( url: string, ): Promise> => { @@ -1325,3 +1232,23 @@ export const applyFilter = ( return nodes; }; + +export const parseSubscriptionUserInfo = (str: string): SubscriptionUserinfo => { + const res = { + upload: 0, + download: 0, + total: 0, + expire: 0, + }; + + str.split(';').forEach(item => { + const pair = item.split('='); + const value = Number(pair[1].trim()); + + if (!Number.isNaN(value)) { + res[pair[0].trim()] = Number(pair[1].trim()) + } + }); + + return res; +}; diff --git a/lib/utils/remote-snippet.ts b/lib/utils/remote-snippet.ts index 053977563..6ecfaba43 100644 --- a/lib/utils/remote-snippet.ts +++ b/lib/utils/remote-snippet.ts @@ -5,7 +5,7 @@ import { logger } from '@surgio/logger'; import { RemoteSnippet, RemoteSnippetConfig } from '../types'; import { NETWORK_CONCURRENCY, NETWORK_TIMEOUT, REMOTE_SNIPPET_CACHE_MAXAGE } from './constant'; -import { ConfigCache } from './index'; +import { ConfigCache } from './cache'; import { createTmpFactory } from './tmp-helper'; export const addProxyToSurgeRuleSet = (str: string, proxyName: string): string => { diff --git a/test/provider/ClashProvider.test.ts b/test/provider/ClashProvider.test.ts index c50745cdd..ca01f5ccd 100644 --- a/test/provider/ClashProvider.test.ts +++ b/test/provider/ClashProvider.test.ts @@ -1,9 +1,10 @@ import test from 'ava'; -import ClashProvider from '../../lib/provider/ClashProvider'; +import { getClashSubscription } from '../../lib/provider/ClashProvider'; import { NodeTypeEnum } from '../../lib/types'; test('getClashSubscription', async t => { - const config = [...await ClashProvider.getClashSubscription('http://example.com/clash-sample.yaml')]; + const { nodeList } = await getClashSubscription('http://example.com/clash-sample.yaml'); + const config = [...nodeList]; t.deepEqual(config.map(item => item.nodeName), ['ss1', 'ss2', 'ss3', 'vmess', 'http 1', 'http 2','snell', 'ss4', 'ss-wss']); t.deepEqual(config.shift(), { @@ -102,7 +103,7 @@ test('getClashSubscription', async t => { }); test('getClashSubscription udpRelay', async t => { - const config = await ClashProvider.getClashSubscription('http://example.com/clash-sample.yaml', true); + const { nodeList: config } = await getClashSubscription('http://example.com/clash-sample.yaml', true); t.deepEqual(config[0], { type: NodeTypeEnum.Shadowsocks, @@ -152,6 +153,6 @@ test('getClashSubscription udpRelay', async t => { test('getClashSubscription - invalid yaml', async t => { await t.throwsAsync(async () => { - await ClashProvider.getClashSubscription('http://example.com/test-v2rayn-sub.txt'); + await getClashSubscription('http://example.com/test-v2rayn-sub.txt'); }, {instanceOf: Error, message: 'http://example.com/test-v2rayn-sub.txt 不是一个合法的 YAML 文件'}); }); diff --git a/test/provider/ShadowsocksSubscribeProvider.test.ts b/test/provider/ShadowsocksSubscribeProvider.test.ts new file mode 100644 index 000000000..a729f887e --- /dev/null +++ b/test/provider/ShadowsocksSubscribeProvider.test.ts @@ -0,0 +1,63 @@ +import test from 'ava'; + +import { getShadowsocksSubscription } from '../../lib/provider/ShadowsocksSubscribeProvider'; +import { NodeTypeEnum } from '../../lib/types'; + +test('getShadowsocksSubscription with udp', async t => { + const { nodeList } = await getShadowsocksSubscription('http://example.com/test-ss-sub.txt', true); + + t.deepEqual(nodeList[0], { + type: NodeTypeEnum.Shadowsocks, + nodeName: '🇺🇸US 1', + hostname: 'us.example.com', + port: '443', + method: 'chacha20-ietf-poly1305', + password: 'password', + 'udp-relay': true, + obfs: 'tls', + 'obfs-host': 'gateway-carry.icloud.com', + }); + t.deepEqual(nodeList[1], { + nodeName: '🇺🇸US 2', + type: NodeTypeEnum.Shadowsocks, + hostname: 'us.example.com', + port: '443', + method: 'chacha20-ietf-poly1305', + password: 'password', + 'udp-relay': true, + }); + t.deepEqual(nodeList[2], { + nodeName: '🇺🇸US 3', + type: NodeTypeEnum.Shadowsocks, + hostname: 'us.example.com', + port: '443', + method: 'chacha20-ietf-poly1305', + password: 'password', + 'udp-relay': true, + obfs: 'wss', + 'obfs-host': 'gateway-carry.icloud.com', + }); +}); + +test('getShadowsocksSubscription without udp', async t => { + const { nodeList } = await getShadowsocksSubscription('http://example.com/test-ss-sub.txt'); + + t.deepEqual(nodeList[0], { + type: NodeTypeEnum.Shadowsocks, + nodeName: '🇺🇸US 1', + hostname: 'us.example.com', + port: '443', + method: 'chacha20-ietf-poly1305', + password: 'password', + obfs: 'tls', + 'obfs-host': 'gateway-carry.icloud.com', + }); + t.deepEqual(nodeList[1], { + nodeName: '🇺🇸US 2', + type: NodeTypeEnum.Shadowsocks, + hostname: 'us.example.com', + port: '443', + method: 'chacha20-ietf-poly1305', + password: 'password', + }); +}); diff --git a/test/provider/ShadowsocksrSubscribeProvider.test.ts b/test/provider/ShadowsocksrSubscribeProvider.test.ts new file mode 100644 index 000000000..63ca5cf4e --- /dev/null +++ b/test/provider/ShadowsocksrSubscribeProvider.test.ts @@ -0,0 +1,36 @@ +import test from 'ava'; + +import { NodeTypeEnum } from '../../lib/types'; +import { getShadowsocksrSubscription } from '../../lib/provider/ShadowsocksrSubscribeProvider'; + +test('getShadowsocksrSubscription', async t => { + const { nodeList } = await getShadowsocksrSubscription('http://example.com/test-ssr-sub.txt?v=1', false); + const { nodeList: nodeList2 } = await getShadowsocksrSubscription('http://example.com/test-ssr-sub.txt?v=2', true); + + t.deepEqual(nodeList[0], { + nodeName: '测试中文', + type: NodeTypeEnum.Shadowsocksr, + hostname: '127.0.0.1', + port: '1234', + method: 'aes-128-cfb', + password: 'aaabbb', + obfs: 'tls1.2_ticket_auth', + obfsparam: 'breakwa11.moe', + protocol: 'auth_aes128_md5', + protoparam: '', + 'udp-relay': false, + }); + t.deepEqual(nodeList2[0], { + nodeName: '测试中文', + type: NodeTypeEnum.Shadowsocksr, + hostname: '127.0.0.1', + port: '1234', + method: 'aes-128-cfb', + password: 'aaabbb', + obfs: 'tls1.2_ticket_auth', + obfsparam: 'breakwa11.moe', + protocol: 'auth_aes128_md5', + protoparam: '', + 'udp-relay': true, + }); +}); diff --git a/test/utils/index.test.ts b/test/utils/index.test.ts index cd72201c0..1b3767e36 100644 --- a/test/utils/index.test.ts +++ b/test/utils/index.test.ts @@ -1,7 +1,5 @@ // tslint:disable:no-expression-statement import test from 'ava'; -import fs from 'fs'; -import path from 'path'; import { NodeTypeEnum, @@ -1070,94 +1068,3 @@ test('formatV2rayConfig', t => { } }); }); - -test('getShadowsocksSubscription with udp', async t => { - const nodeList = await utils.getShadowsocksSubscription('http://example.com/test-ss-sub.txt', true); - - t.deepEqual(nodeList[0], { - type: NodeTypeEnum.Shadowsocks, - nodeName: '🇺🇸US 1', - hostname: 'us.example.com', - port: '443', - method: 'chacha20-ietf-poly1305', - password: 'password', - 'udp-relay': true, - obfs: 'tls', - 'obfs-host': 'gateway-carry.icloud.com', - }); - t.deepEqual(nodeList[1], { - nodeName: '🇺🇸US 2', - type: NodeTypeEnum.Shadowsocks, - hostname: 'us.example.com', - port: '443', - method: 'chacha20-ietf-poly1305', - password: 'password', - 'udp-relay': true, - }); - t.deepEqual(nodeList[2], { - nodeName: '🇺🇸US 3', - type: NodeTypeEnum.Shadowsocks, - hostname: 'us.example.com', - port: '443', - method: 'chacha20-ietf-poly1305', - password: 'password', - 'udp-relay': true, - obfs: 'wss', - 'obfs-host': 'gateway-carry.icloud.com', - }); -}); - -test('getShadowsocksSubscription without udp', async t => { - const nodeList = await utils.getShadowsocksSubscription('http://example.com/test-ss-sub.txt'); - - t.deepEqual(nodeList[0], { - type: NodeTypeEnum.Shadowsocks, - nodeName: '🇺🇸US 1', - hostname: 'us.example.com', - port: '443', - method: 'chacha20-ietf-poly1305', - password: 'password', - obfs: 'tls', - 'obfs-host': 'gateway-carry.icloud.com', - }); - t.deepEqual(nodeList[1], { - nodeName: '🇺🇸US 2', - type: NodeTypeEnum.Shadowsocks, - hostname: 'us.example.com', - port: '443', - method: 'chacha20-ietf-poly1305', - password: 'password', - }); -}); - -test('getShadowsocksrSubscription', async t => { - const nodeList = await utils.getShadowsocksrSubscription('http://example.com/test-ssr-sub.txt?v=1', false); - const nodeList2 = await utils.getShadowsocksrSubscription('http://example.com/test-ssr-sub.txt?v=2', true); - - t.deepEqual(nodeList[0], { - nodeName: '测试中文', - type: NodeTypeEnum.Shadowsocksr, - hostname: '127.0.0.1', - port: '1234', - method: 'aes-128-cfb', - password: 'aaabbb', - obfs: 'tls1.2_ticket_auth', - obfsparam: 'breakwa11.moe', - protocol: 'auth_aes128_md5', - protoparam: '', - 'udp-relay': false, - }); - t.deepEqual(nodeList2[0], { - nodeName: '测试中文', - type: NodeTypeEnum.Shadowsocksr, - hostname: '127.0.0.1', - port: '1234', - method: 'aes-128-cfb', - password: 'aaabbb', - obfs: 'tls1.2_ticket_auth', - obfsparam: 'breakwa11.moe', - protocol: 'auth_aes128_md5', - protoparam: '', - 'udp-relay': true, - }); -});