diff --git a/lib/class/BlackSSLProvider.ts b/lib/class/BlackSSLProvider.ts new file mode 100644 index 000000000..408d4b6d0 --- /dev/null +++ b/lib/class/BlackSSLProvider.ts @@ -0,0 +1,18 @@ +import { BlackSSLProviderConfig } from '../types'; +import { getBlackSSLConfig } from '../utils'; +import Provider from './Provider'; + +export default class BlackSSLProvider extends Provider { + public readonly username: string; + public readonly password: string; + + constructor(config: BlackSSLProviderConfig) { + super(config); + this.username = config.username; + this.password = config.password; + } + + public getNodeList(): ReturnType { + return getBlackSSLConfig(this); + } +} diff --git a/lib/class/CustomProvider.ts b/lib/class/CustomProvider.ts new file mode 100644 index 000000000..bbec00a4f --- /dev/null +++ b/lib/class/CustomProvider.ts @@ -0,0 +1,17 @@ +import assert from 'assert'; +import { CustomProviderConfig, PossibleNodeConfigType } from '../types'; +import Provider from './Provider'; + +export default class CustomProvider extends Provider { + public readonly nodeList: ReadonlyArray; + + constructor(config: CustomProviderConfig) { + super(config); + assert(config.nodeList, 'Lack of nodeList.'); + this.nodeList = config.nodeList; + } + + public async getNodeList(): Promise> { + return this.nodeList; + } +} diff --git a/lib/class/Provider.ts b/lib/class/Provider.ts new file mode 100644 index 000000000..b6eda4625 --- /dev/null +++ b/lib/class/Provider.ts @@ -0,0 +1,34 @@ +import assert from 'assert'; + +import { + NodeFilterType, + NodeNameFilterType, + ProviderConfig, + SupportProviderEnum, +} from '../types'; + +let globalPort: number = 61100; + +export default class Provider { + public readonly type: SupportProviderEnum; + public readonly nodeFilter?: NodeFilterType; + public readonly netflixFilter?: NodeNameFilterType; + public readonly youtubePremiumFilter?: NodeNameFilterType; + private startPort?: number; + + constructor(config: ProviderConfig) { + assert(config.type, 'You must specify a provider type.'); + this.type = config.type; + this.nodeFilter = config.nodeFilter; + this.netflixFilter = config.netflixFilter; + this.youtubePremiumFilter = config.youtubePremiumFilter; + this.startPort = config.startPort; + } + + public get nextPort(): number { + if (this.startPort) { + return this.startPort++; + } + return globalPort++; + } +} diff --git a/lib/class/ShadowsocksJsonSubscribeProvider.ts b/lib/class/ShadowsocksJsonSubscribeProvider.ts new file mode 100644 index 000000000..3f5b8a8e7 --- /dev/null +++ b/lib/class/ShadowsocksJsonSubscribeProvider.ts @@ -0,0 +1,21 @@ +import { ShadowsocksJsonSubscribeProviderConfig } from '../types'; +import { getShadowsocksJSONConfig } from '../utils'; +import Provider from './Provider'; + +export default class ShadowsocksJsonSubscribeProvider extends Provider { + public readonly url: string; + public readonly udpRelay?: boolean; + + constructor(config: ShadowsocksJsonSubscribeProviderConfig) { + super(config); + this.url = config.url; + this.udpRelay = config.udpRelay; + } + + public getNodeList(): ReturnType { + return getShadowsocksJSONConfig({ + url: this.url, + udpRelay: this.udpRelay, + }); + } +} diff --git a/lib/class/ShadowsocksSubscribeProvider.ts b/lib/class/ShadowsocksSubscribeProvider.ts new file mode 100644 index 000000000..0b624b7c6 --- /dev/null +++ b/lib/class/ShadowsocksSubscribeProvider.ts @@ -0,0 +1,21 @@ +import { ShadowsocksSubscribeProviderConfig } from '../types'; +import { getShadowsocksSubscription } from '../utils'; +import Provider from './Provider'; + +export default class ShadowsocksSubscribeProvider extends Provider { + public readonly url: string; + public readonly udpRelay?: boolean; + + constructor(config: ShadowsocksSubscribeProviderConfig) { + super(config); + this.url = config.url; + this.udpRelay = config.udpRelay; + } + + public getNodeList(): ReturnType { + return getShadowsocksSubscription({ + url: this.url, + udpRelay: this.udpRelay, + }); + } +} diff --git a/lib/class/ShadowsocksrSubscribeProvider.ts b/lib/class/ShadowsocksrSubscribeProvider.ts new file mode 100644 index 000000000..95724ed7b --- /dev/null +++ b/lib/class/ShadowsocksrSubscribeProvider.ts @@ -0,0 +1,16 @@ +import { ShadowsocksrSubscribeProviderConfig } from '../types'; +import { getShadowsocksrSubscription } from '../utils'; +import Provider from './Provider'; + +export default class ShadowsocksrSubscribeProvider extends Provider { + public readonly url: string; + + constructor(config: ShadowsocksrSubscribeProviderConfig) { + super(config); + this.url = config.url; + } + + public getNodeList(): ReturnType { + return getShadowsocksrSubscription(this); + } +} diff --git a/lib/class/V2rayNSubscribeProvider.ts b/lib/class/V2rayNSubscribeProvider.ts new file mode 100644 index 000000000..22be38632 --- /dev/null +++ b/lib/class/V2rayNSubscribeProvider.ts @@ -0,0 +1,16 @@ +import { V2rayNSubscribeProviderConfig } from '../types'; +import { getV2rayNSubscription } from '../utils'; +import Provider from './Provider'; + +export default class V2rayNSubscribeProvider extends Provider { + public readonly url: string; + + constructor(config: V2rayNSubscribeProviderConfig) { + super(config); + this.url = config.url; + } + + public getNodeList(): ReturnType { + return getV2rayNSubscription(this); + } +} diff --git a/lib/command/check.ts b/lib/command/check.ts index 9e8f0aea7..f7a406be1 100644 --- a/lib/command/check.ts +++ b/lib/command/check.ts @@ -5,18 +5,9 @@ import fs from 'fs'; import path from 'path'; import { - BlackSSLProviderConfig, - CustomProviderConfig, - PossibleNodeConfigType, - ProviderConfig, - ShadowsocksJsonSubscribeProviderConfig, - SupportProviderEnum, -} from '../types'; -import { - getBlackSSLConfig, - getShadowsocksJSONConfig, loadConfig } from '../utils'; +import getProvider from '../utils/getProvider'; class CheckCommand extends Command { private options: object; @@ -38,36 +29,21 @@ class CheckCommand extends Command { const providerName = ctx.argv._[0]; const config = loadConfig(ctx.cwd, ctx.argv.config); const filePath = path.resolve(config.providerDir, `./${providerName}.js`); - const file: ProviderConfig|Error = fs.existsSync(filePath) ? require(filePath) : new Error('Provider file cannot be found.'); + const file: any|Error = fs.existsSync(filePath) ? require(filePath) : new Error('Provider file cannot be found.'); if (file instanceof Error) { throw file; } - const configList = await this.requestRemoteFile(file); + const provider = getProvider(file); + const nodeList = await provider.getNodeList(); - console.log(JSON.stringify(configList, null ,2)); + console.log(JSON.stringify(nodeList, null ,2)); } public get description(): string { return 'Check configurations from provider'; } - - private async requestRemoteFile(file: ProviderConfig): Promise> { - switch (file.type) { - case SupportProviderEnum.BlackSSL: - return getBlackSSLConfig(file as BlackSSLProviderConfig); - - case SupportProviderEnum.ShadowsocksJsonSubscribe: - return getShadowsocksJSONConfig(file as ShadowsocksJsonSubscribeProviderConfig); - - case SupportProviderEnum.Custom: - return (file as CustomProviderConfig).nodeList; - - default: - throw new Error(`Unsupported provider type: ${file.type}`); - } - } } export = CheckCommand; diff --git a/lib/command/speed.ts b/lib/command/speed.ts index 7992d0770..3b4fc3db8 100644 --- a/lib/command/speed.ts +++ b/lib/command/speed.ts @@ -14,9 +14,11 @@ import path from 'path'; import shelljs from 'shelljs'; import speedTest from 'speedtest-net'; import winston, { format, Logger } from 'winston'; +import Provider from '../class/Provider'; -import { ShadowsocksNodeConfig, ProviderConfig, SupportProviderEnum, ShadowsocksJsonSubscribeProviderConfig, CustomProviderConfig } from '../types'; -import { getClashNodes, toYaml, getShadowsocksJSONConfig, loadConfig } from '../utils'; +import { NodeTypeEnum, PossibleNodeConfigType, ShadowsocksNodeConfig } from '../types'; +import { getClashNodes, loadConfig, toYaml } from '../utils'; +import getProvider from '../utils/getProvider'; const { combine, timestamp, printf } = format; const speedDebug = debug('speed'); @@ -51,14 +53,16 @@ class SpeedCommand extends Command { const providerName = ctx.argv._[0]; const config = loadConfig(ctx.cwd, ctx.argv.config); const filePath = path.resolve(config.providerDir, `./${providerName}.js`); - const file: ProviderConfig|Error = fs.existsSync(filePath) ? require(filePath) : new Error('Provider file cannot be found.'); + const file: any|Error = fs.existsSync(filePath) ? require(filePath) : new Error('Provider file cannot be found.'); if (file instanceof Error) { throw file; } - const configList = await this.requestRemoteFile(file); - const nodeConfig = await this.promptSelections(configList); + const provider = getProvider(file); + + const nodeList = await provider.getNodeList(); + const nodeConfig = await this.promptSelections(nodeList); await this.runTest(nodeConfig); } @@ -201,36 +205,28 @@ class SpeedCommand extends Command { }); } - private async promptSelections(arr: ReadonlyArray): Promise { + private async promptSelections(arr: ReadonlyArray): Promise { + const choices = arr + .filter(item => { + return item.type === NodeTypeEnum.Shadowsocks; + }) + .map(item => { + return { + name: `${item.nodeName} - ${chalk.gray(item.hostname + ':' + item.port)}`, + value: item, + }; + }); const answer = await inquirer.prompt([ { name: 'server', message: 'Which server?', type: 'list', - choices: arr.map(item => { - return { - name: `${item.nodeName} - ${chalk.gray(item.hostname + ':' + item.port)}`, - value: item, - }; - }), + choices, } ]); return (answer as any).server as ShadowsocksNodeConfig; } - - private async requestRemoteFile(file: ProviderConfig): Promise> { - switch (file.type) { - case SupportProviderEnum.ShadowsocksJsonSubscribe: - return getShadowsocksJSONConfig(file as ShadowsocksJsonSubscribeProviderConfig); - - case SupportProviderEnum.Custom: - return (file as CustomProviderConfig).nodeList as ReadonlyArray; - - default: - throw new Error(`Unsupported provider type: ${file.type}`); - } - } } export = SpeedCommand; diff --git a/lib/generate.ts b/lib/generate.ts index 814a675c0..66577f96d 100644 --- a/lib/generate.ts +++ b/lib/generate.ts @@ -10,36 +10,23 @@ import path from 'path'; import getEngine from './template'; import { ArtifactConfig, - BlackSSLProviderConfig, CommandConfig, - CustomProviderConfig, NodeNameFilterType, - NodeTypeEnum, PossibleNodeConfigType, - ProviderConfig, RemoteSnippet, - ShadowsocksJsonSubscribeProviderConfig, - ShadowsocksrSubscribeProviderConfig, - ShadowsocksSubscribeProviderConfig, SimpleNodeConfig, SupportProviderEnum, - V2rayNSubscribeProviderConfig, } from './types'; import { - getBlackSSLConfig, getClashNodeNames, getClashNodes, getDownloadUrl, getNodeNames, getQuantumultNodes, - getShadowsocksJSONConfig, getShadowsocksNodes, getShadowsocksNodesJSON, getShadowsocksrNodes, - getShadowsocksrSubscription, - getShadowsocksSubscription, getSurgeNodes, - getV2rayNSubscription, hkFilter, loadRemoteSnippetList, netflixFilter as defaultNetflixFilter, @@ -49,6 +36,7 @@ import { usFilter, youtubePremiumFilter as defaultYoutubePremiumFilter, } from './utils'; +import getProvider from './utils/getProvider'; const spinner = ora(); @@ -81,15 +69,13 @@ export async function generate( const { name: artifactName, template, - provider, customParams, } = artifact; assert(artifactName, 'You must specify the artifact\'s name.'); assert(template, 'You must specify the artifact\'s template.'); - assert(provider, 'You must specify the artifact\'s provider.'); + assert(artifact.provider, 'You must specify the artifact\'s provider.'); - const tplBuffer = await fs.readFile(path.resolve(config.templateDir, `${template}.tpl`)); const recipeList = artifact.recipe ? artifact.recipe : [artifact.provider]; const nodeList: PossibleNodeConfigType[] = []; const nodeNameList: SimpleNodeConfig[] = []; @@ -98,84 +84,49 @@ export async function generate( youtubePremiumFilter?: NodeNameFilterType; } = {}; + if (config.binPath && config.binPath.v2ray) { + config.binPath.vmess = config.binPath.v2ray; + } + for (const providerName of recipeList) { const filePath = path.resolve(config.providerDir, `${providerName}.js`); - const recipeConfigList: Array> = []; if (!fs.existsSync(filePath)) { throw new Error(`${filePath} cannot be found.`); } - const file: ProviderConfig = require(filePath); + const provider = getProvider(require(filePath)); + const nodeConfigList = await provider.getNodeList(); if (!customFilters.netflixFilter) { - customFilters.netflixFilter = file.netflixFilter || defaultNetflixFilter; + customFilters.netflixFilter = provider.netflixFilter || defaultNetflixFilter; } if (!customFilters.youtubePremiumFilter) { - customFilters.youtubePremiumFilter = file.youtubePremiumFilter || defaultYoutubePremiumFilter; + customFilters.youtubePremiumFilter = provider.youtubePremiumFilter || defaultYoutubePremiumFilter; } - assert(file.type, 'You must specify a type.'); + nodeConfigList.forEach(nodeConfig => { + let isValid = false; - switch (file.type) { - case SupportProviderEnum.BlackSSL: - recipeConfigList.push(await getBlackSSLConfig(file as BlackSSLProviderConfig)); - break; - - case SupportProviderEnum.ShadowsocksJsonSubscribe: - recipeConfigList.push(await getShadowsocksJSONConfig(file as ShadowsocksJsonSubscribeProviderConfig)); - break; - - case SupportProviderEnum.ShadowsocksSubscribe: - recipeConfigList.push(await getShadowsocksSubscription(file as ShadowsocksSubscribeProviderConfig)); - break; - - case SupportProviderEnum.ShadowsocksrSubscribe: - recipeConfigList.push(await getShadowsocksrSubscription(file as ShadowsocksrSubscribeProviderConfig)); - break; - - case SupportProviderEnum.Custom: { - assert((file as CustomProviderConfig).nodeList, 'Lack of nodeList.'); - recipeConfigList.push((file as CustomProviderConfig).nodeList); - break; + if (!provider.nodeFilter) { + isValid = true; + } else if (provider.nodeFilter(nodeConfig)) { + isValid = true; } - case SupportProviderEnum.V2rayNSubscribe: - recipeConfigList.push(await getV2rayNSubscription(file as V2rayNSubscribeProviderConfig)); - break; - - default: - throw new Error(`Unsupported provider type: ${file.type}`); - } + if (config.binPath && config.binPath[nodeConfig.type]) { + nodeConfig.binPath = config.binPath[nodeConfig.type]; + nodeConfig.localPort = provider.nextPort; + } - recipeConfigList.forEach(recipeConfig => { - recipeConfig.forEach(nodeConfig => { - let isValid = false; - - if (!file.nodeFilter) { - isValid = true; - } else if (file.nodeFilter(nodeConfig)) { - isValid = true; - } - - if (config.binPath) { - if (config.binPath.v2ray) { - config.binPath.vmess = config.binPath.v2ray; - } - if (config.binPath[nodeConfig.type]) { - nodeConfig.binPath = config.binPath[nodeConfig.type]; - } - } - - if (isValid) { - nodeNameList.push({ - type: nodeConfig.type, - enable: nodeConfig.enable, - nodeName: nodeConfig.nodeName, - }); - nodeList.push(nodeConfig); - } - }); + if (isValid) { + nodeNameList.push({ + type: nodeConfig.type, + enable: nodeConfig.enable, + nodeName: nodeConfig.nodeName, + }); + nodeList.push(nodeConfig); + } }); } @@ -188,8 +139,8 @@ export async function generate( return item.name; }), nodeList, - provider, - providerName: provider, + provider: artifact.provider, + providerName: artifact.provider, artifactName, getDownloadUrl: (name: string) => getDownloadUrl(config.urlBase, name), getNodeNames, diff --git a/lib/types.ts b/lib/types.ts index 0fdccf32f..ab4925699 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,4 @@ -import { cst } from 'yaml'; -import Node = cst.Node; +import Provider from './class/Provider'; export enum NodeTypeEnum { HTTPS = 'https', @@ -63,6 +62,7 @@ export interface ProviderConfig { readonly nodeFilter?: NodeFilterType; readonly netflixFilter?: NodeNameFilterType; readonly youtubePremiumFilter?: NodeNameFilterType; + readonly startPort?: number; } export interface BlackSSLProviderConfig extends ProviderConfig { @@ -149,6 +149,7 @@ export interface SimpleNodeConfig { readonly enable?: boolean; readonly nodeName: string; binPath?: string; // tslint:disable-line + localPort?: number; // tslint:disable-line } export interface PlainObject { readonly [name: string]: any } diff --git a/lib/utils/getProvider.ts b/lib/utils/getProvider.ts new file mode 100644 index 000000000..db71d1383 --- /dev/null +++ b/lib/utils/getProvider.ts @@ -0,0 +1,37 @@ +import assert from "assert"; + +import BlackSSLProvider from '../class/BlackSSLProvider'; +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 { + assert(config.type, 'You must specify a type.'); + + switch (config.type) { + case SupportProviderEnum.BlackSSL: + return new BlackSSLProvider(config); + + case SupportProviderEnum.ShadowsocksJsonSubscribe: + return new ShadowsocksJsonSubscribeProvider(config); + + case SupportProviderEnum.ShadowsocksSubscribe: + return new ShadowsocksSubscribeProvider(config); + + case SupportProviderEnum.ShadowsocksrSubscribe: + return new ShadowsocksrSubscribeProvider(config); + + case SupportProviderEnum.Custom: { + return new CustomProvider(config); + } + + case SupportProviderEnum.V2rayNSubscribe: + return new V2rayNSubscribeProvider(config); + + default: + throw new Error(`Unsupported provider type: ${config.type}`); + } +} diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 0052dbfd9..486fef7ff 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -13,7 +13,6 @@ import YAML from 'yaml'; import os from 'os'; import { - BlackSSLProviderConfig, CommandConfig, HttpsNodeConfig, NodeFilterType, @@ -40,7 +39,10 @@ export const resolveRoot = (...args: readonly string[]): string => path.join(__d export const getDownloadUrl = (baseUrl: string = '/', artifactName: string): string => `${baseUrl}${artifactName}`; -export const getBlackSSLConfig = async (config: BlackSSLProviderConfig): Promise> => { +export const getBlackSSLConfig = async (config: { + readonly username: string; + readonly password: string; +}): Promise> => { assert(config.username, 'Lack of BlackSSL username.'); assert(config.password, 'Lack of BlackSSL password.'); @@ -80,8 +82,8 @@ export const getBlackSSLConfig = async (config: BlackSSLProviderConfig): Promise }; export const getShadowsocksJSONConfig = async (config: { - readonly url: string, - readonly udpRelay?: boolean, + readonly url: string; + readonly udpRelay: boolean; }): Promise> => { assert(config.url, 'Lack of subscription url.'); @@ -221,7 +223,7 @@ export const getShadowsocksrSubscription = async (config: { }; export const getV2rayNSubscription = async (config: { - readonly url: string, + readonly url: string; }): Promise> => { assert(config.url, 'Lack of subscription url.'); @@ -269,8 +271,6 @@ export const getSurgeNodes = ( list: ReadonlyArray, filter?: NodeFilterType, ): string => { - let portNumber = 61100; - const result: string[] = list .filter(item => filter ? filter(item) : true) .map(nodeConfig => { @@ -338,7 +338,7 @@ export const getSurgeNodes = ( '-o', config.obfs, '-O', config.protocol, '-k', config.password, - '-l', `${portNumber}`, + '-l', `${config.localPort}`, '-b', '127.0.0.1', ]; @@ -353,12 +353,10 @@ export const getSurgeNodes = ( 'external', `exec = ${JSON.stringify(config.binPath)}`, ...(args).map(arg => `args = ${JSON.stringify(arg)}`), - `local-port = ${portNumber}`, + `local-port = ${config.localPort}`, `addresses = ${config.hostname}`, ].join(', '); - portNumber++; - return ([ config.nodeName, configString, @@ -375,7 +373,7 @@ export const getSurgeNodes = ( const jsonFileName = `v2ray_${config.hostname}_${config.port}.json`; const jsonFilePath = path.join(ensureConfigFolder(), jsonFileName); - const jsonFile = formatV2rayConfig(portNumber, nodeConfig); + const jsonFile = formatV2rayConfig(config.localPort, nodeConfig); const args = [ '--config', jsonFilePath.replace(os.homedir(), '$HOME'), ]; @@ -383,7 +381,7 @@ export const getSurgeNodes = ( 'external', `exec = ${JSON.stringify(config.binPath)}`, ...(args).map(arg => `args = ${JSON.stringify(arg)}`), - `local-port = ${portNumber}`, + `local-port = ${config.localPort}`, `addresses = ${config.hostname}`, ].join(', '); @@ -391,8 +389,6 @@ export const getSurgeNodes = ( fs.writeJSONSync(jsonFilePath, jsonFile); } - portNumber++; - return ([ config.nodeName, configString,