Skip to content

Commit

Permalink
feat: 支持读取 Clash 订阅
Browse files Browse the repository at this point in the history
  • Loading branch information
geekdada committed Nov 1, 2019
1 parent 5cc52f1 commit 45ef59f
Show file tree
Hide file tree
Showing 15 changed files with 500 additions and 78 deletions.
2 changes: 1 addition & 1 deletion lib/class/BlackSSLProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ export default class BlackSSLProvider extends Provider {
}

public getNodeList(): ReturnType<typeof getBlackSSLConfig> {
return getBlackSSLConfig(this);
return getBlackSSLConfig(this.username, this.password);
}
}
162 changes: 162 additions & 0 deletions lib/class/ClashProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import Joi from '@hapi/joi';
import assert from 'assert';
import axios from 'axios';
import chalk from 'chalk';
import yaml from 'yaml';
import _ from 'lodash';

import {
ClashProviderConfig,
HttpsNodeConfig,
NodeTypeEnum,
ShadowsocksNodeConfig,
ShadowsocksrNodeConfig,
SnellNodeConfig,
VmessNodeConfig,
} from '../types';
import { ConfigCache } from '../utils';
import { NETWORK_TIMEOUT } from '../utils/constant';
import Provider from './Provider';

type SupportConfigTypes = ShadowsocksNodeConfig|VmessNodeConfig|HttpsNodeConfig|ShadowsocksrNodeConfig|SnellNodeConfig;

export default class ClashProvider extends Provider {
public static async getClashSubscription(url: string): Promise<ReadonlyArray<SupportConfigTypes>> {
assert(url, '未指定订阅地址 url');

return ConfigCache.has(url) ?
ConfigCache.get(url) :
await requestConfigFromRemote(url);
}

public readonly url: string;

constructor(config: ClashProviderConfig) {
super(config);

const schema = Joi.object({
url: Joi
.string()
.uri({
scheme: [
/https?/,
],
})
.required(),
})
.unknown();

const { error } = schema.validate(config);

if (error) {
throw error;
}

this.url = config.url;
}

public getNodeList(): ReturnType<typeof ClashProvider.getClashSubscription> {
return ClashProvider.getClashSubscription(this.url);
}
}

async function requestConfigFromRemote(url): Promise<ReadonlyArray<SupportConfigTypes>> {
const response = await axios.get(url, {
timeout: NETWORK_TIMEOUT,
responseType: 'text',
});
const clashConfig = yaml.parse(response.data);
const proxyList: any[] = clashConfig.Proxy;
const result = proxyList.map<SupportConfigTypes>(item => {
switch (item.type) {
case 'ss':
if (item.plugin && item.plugin !== 'obfs') {
console.log();
console.log(`不支持读取 ${item.plugin} 类型的 Clash 节点,节点 ${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': item.udp ? 'true' : 'false',
...(item.plugin && item.plugin === 'obfs' ? {
obfs: item['plugin-opts'].mode,
'obfs-host': item['plugin-opts'].host || 'www.bing.com',
} : null),
};

case 'vmess':
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: item.udp !== void 0 ? item.udp : false,
tls: item.tls !== void 0 ? item.tls : false,
network: item.network || 'tcp',
...(item.network === 'ws' ? {
path: _.get(item, 'ws-path', '/'),
host: _.get(item, 'ws-headers.Host', ''),
} : null),
};

case 'http':
if (item.tls !== 'https') {
console.log();
console.log(`不支持读取 HTTP 类型的 Clash 节点,节点 ${item.name} 会被省略`);
return null;
}

return {
type: NodeTypeEnum.HTTPS,
nodeName: item.name,
hostname: item.server,
port: item.port,
username: item.username || '',
password: item.password || '',
};

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'),
};

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:
console.log();
console.log(chalk.yellow(`不支持读取 ${item.type} 的节点,节点 ${item.name} 会被省略`));
return null;
}
})
.filter(item => !!item);

ConfigCache.set(url, result);

return result;
}
5 changes: 1 addition & 4 deletions lib/class/ShadowsocksJsonSubscribeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ export default class ShadowsocksJsonSubscribeProvider extends Provider {
}

public getNodeList(): ReturnType<typeof getShadowsocksJSONConfig> {
return getShadowsocksJSONConfig({
url: this.url,
udpRelay: this.udpRelay,
});
return getShadowsocksJSONConfig(this.url, this.udpRelay);
}
}
5 changes: 1 addition & 4 deletions lib/class/ShadowsocksSubscribeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ export default class ShadowsocksSubscribeProvider extends Provider {
}

public getNodeList(): ReturnType<typeof getShadowsocksSubscription> {
return getShadowsocksSubscription({
url: this.url,
udpRelay: this.udpRelay,
});
return getShadowsocksSubscription(this.url, this.udpRelay);
}
}
2 changes: 1 addition & 1 deletion lib/class/ShadowsocksrSubscribeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ export default class ShadowsocksrSubscribeProvider extends Provider {
}

public getNodeList(): ReturnType<typeof getShadowsocksrSubscription> {
return getShadowsocksrSubscription(this);
return getShadowsocksrSubscription(this.url);
}
}
2 changes: 1 addition & 1 deletion lib/class/V2rayNSubscribeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ export default class V2rayNSubscribeProvider extends Provider {
}

public getNodeList(): ReturnType<typeof getV2rayNSubscription> {
return getV2rayNSubscription(this);
return getV2rayNSubscription(this.url);
}
}
13 changes: 11 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Provider from './class/Provider';

export enum NodeTypeEnum {
HTTPS = 'https',
Shadowsocks = 'shadowsocks',
Expand All @@ -13,6 +15,7 @@ export enum SupportProviderEnum {
ShadowsocksSubscribe = 'shadowsocks_subscribe',
ShadowsocksrSubscribe = 'shadowsocksr_subscribe',
Custom = 'custom',
Clash = 'clash',
}

export interface CommandConfig {
Expand Down Expand Up @@ -98,6 +101,10 @@ export interface V2rayNSubscribeProviderConfig extends ProviderConfig {
readonly url: string;
}

export interface ClashProviderConfig extends ProviderConfig {
readonly url: string;
}

export interface CustomProviderConfig extends ProviderConfig {
readonly nodeList: ReadonlyArray<PossibleNodeConfigType>;
}
Expand Down Expand Up @@ -150,13 +157,15 @@ export interface VmessNodeConfig extends SimpleNodeConfig {
readonly alterId: string;
readonly network: 'tcp' | 'kcp' | 'ws' | 'http' ;
readonly tls: boolean;
readonly host: string;
readonly path: string;
readonly host?: string;
readonly path?: string;
readonly udp?: boolean;
}

export interface SimpleNodeConfig {
readonly type: NodeTypeEnum;
readonly enable?: boolean;
readonly tfo?: boolean; // TCP Fast Open
nodeName: string; // tslint:disable-line
binPath?: string; // tslint:disable-line
localPort?: number; // tslint:disable-line
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const NETWORK_TIMEOUT = process.env.SURGIO_NETWORK_TIMEOUT ? Number(process.env.SURGIO_NETWORK_TIMEOUT) : 20000;
export const OBFS_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148';
6 changes: 5 additions & 1 deletion lib/utils/get-provider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import assert from "assert";

import BlackSSLProvider from '../class/BlackSSLProvider';
import ClashProvider from '../class/ClashProvider';
import CustomProvider from '../class/CustomProvider';
import ShadowsocksJsonSubscribeProvider from '../class/ShadowsocksJsonSubscribeProvider';
import ShadowsocksrSubscribeProvider from '../class/ShadowsocksrSubscribeProvider';
import ShadowsocksSubscribeProvider from '../class/ShadowsocksSubscribeProvider';
import V2rayNSubscribeProvider from '../class/V2rayNSubscribeProvider';
import { SupportProviderEnum } from '../types';

export default function(config: any): BlackSSLProvider|ShadowsocksJsonSubscribeProvider|ShadowsocksSubscribeProvider|CustomProvider|V2rayNSubscribeProvider|ShadowsocksrSubscribeProvider {
export default function(config: any): BlackSSLProvider|ShadowsocksJsonSubscribeProvider|ShadowsocksSubscribeProvider|CustomProvider|V2rayNSubscribeProvider|ShadowsocksrSubscribeProvider|ClashProvider {
switch (config.type) {
case SupportProviderEnum.BlackSSL:
return new BlackSSLProvider(config);
Expand All @@ -29,6 +30,9 @@ export default function(config: any): BlackSSLProvider|ShadowsocksJsonSubscribeP
case SupportProviderEnum.V2rayNSubscribe:
return new V2rayNSubscribeProvider(config);

case SupportProviderEnum.Clash:
return new ClashProvider(config);

default:
throw new Error(`Unsupported provider type: ${config.type}`);
}
Expand Down
Loading

0 comments on commit 45ef59f

Please sign in to comment.