Skip to content

Commit

Permalink
feat: 支持转换 Surge 的 Script 部分规则到 Quantumult X 格式
Browse files Browse the repository at this point in the history
支持类型:

- response-body
  • Loading branch information
geekdada committed Nov 24, 2019
1 parent 4456d2e commit 82633d2
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 28 deletions.
2 changes: 1 addition & 1 deletion lib/gateway/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class Server {

this.config = config;
this.artifactList = config.artifacts;
this.templateEngine = getEngine(config.templateDir);
this.templateEngine = getEngine(config.templateDir, config.publicUrl);
if (fs.existsSync(pkgFile)) {
this.pkgFile = require(pkgFile);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async function run(config: CommandConfig): Promise<void> {
const distPath = config.output;
const remoteSnippetsConfig = config.remoteSnippets || [];
const remoteSnippetList = await loadRemoteSnippetList(remoteSnippetsConfig);
const templateEngine = getEngine(config.templateDir);
const templateEngine = getEngine(config.templateDir, config.publicUrl);

await fs.remove(distPath);
await fs.mkdir(distPath);
Expand Down Expand Up @@ -88,7 +88,7 @@ export async function generate(
config: CommandConfig,
artifact: ArtifactConfig,
remoteSnippetList: ReadonlyArray<RemoteSnippet>,
templateEngine: Environment = getEngine(config.templateDir),
templateEngine: Environment,
): Promise<string> {
const {
name: artifactName,
Expand Down
65 changes: 52 additions & 13 deletions lib/template.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import nunjucks from 'nunjucks';
import { JsonObject } from 'type-fest';
import YAML from 'yaml';
import { toBase64 } from './utils';
import { CLASH_UNSUPPORTED_RULE, MELLOW_UNSUPPORTED_RULE, QUANTUMULT_X_UNSUPPORTED_RULE } from './utils/constant';
import { URL } from 'url';

export default function getEngine(templateDir: string): nunjucks.Environment {
import { decodeStringList, toBase64 } from './utils';
import {
CLASH_UNSUPPORTED_RULE,
MELLOW_UNSUPPORTED_RULE,
QUANTUMULT_X_SUPPORTED_RULE } from './utils/constant';

export default function getEngine(templateDir: string, publicUrl: string): nunjucks.Environment {
const engine = nunjucks.configure(templateDir, {
autoescape: false,
});

engine.addFilter('patchYamlArray', (str: string) => {
const clashFilter = (str: string): string => {
const array = str.split('\n');

return array
.filter(item => {
const testString: string = (!!item && item.trim() !== '') ? item.toUpperCase() : '';

return testString !== '' &&
CLASH_UNSUPPORTED_RULE.every(s => !testString.startsWith(s));
return CLASH_UNSUPPORTED_RULE.every(s => !testString.startsWith(s));
})
.map((item: string) => {
if (item.startsWith('#')) {
if (item.startsWith('#') || str.trim() === '') {
return item;
}
return `- ${item}`
Expand All @@ -29,7 +33,10 @@ export default function getEngine(templateDir: string): nunjucks.Environment {
.trim();
})
.join('\n');
});
};

engine.addFilter('patchYamlArray', clashFilter);
engine.addFilter('clash', clashFilter);

engine.addFilter('quantumultx', (str: string) => {
const array = str.split('\n');
Expand All @@ -38,8 +45,18 @@ export default function getEngine(templateDir: string): nunjucks.Environment {
.filter(item => {
const testString: string = (!!item && item.trim() !== '') ? item.toUpperCase() : '';

return testString !== '' &&
QUANTUMULT_X_UNSUPPORTED_RULE.every(s => !testString.startsWith(s));
if (testString.startsWith('#') || testString === '') {
return item;
}

// 过滤出支持的规则类型
return QUANTUMULT_X_SUPPORTED_RULE.some(s => testString.startsWith(s));
})
.map((item: string) => {
if (item.startsWith('http-response')) {
return convertSurgeScriptRuleToQuantumultXRewriteRule(item, publicUrl);
}
return item;
})
.join('\n');
});
Expand All @@ -51,11 +68,10 @@ export default function getEngine(templateDir: string): nunjucks.Environment {
.filter(item => {
const testString: string = (!!item && item.trim() !== '') ? item.toUpperCase() : '';

return testString !== '' &&
MELLOW_UNSUPPORTED_RULE.every(s => !testString.startsWith(s));
return MELLOW_UNSUPPORTED_RULE.every(s => !testString.startsWith(s));
})
.map((item: string) => {
if (item.startsWith('#')) {
if (item.startsWith('#') || str.trim() === '') {
return item;
}
return item
Expand All @@ -73,3 +89,26 @@ export default function getEngine(templateDir: string): nunjucks.Environment {

return engine;
};

export const convertSurgeScriptRuleToQuantumultXRewriteRule = (str: string, publicUrl: string): string => {
const parts = str.split(' ');
const result = [];

switch (parts[0]) {
case 'http-response':
const params = decodeStringList(parts.splice(2).join('').split(','));
const scriptPath = params['script-path'];
const apiEndpoint = new URL(publicUrl);
apiEndpoint.pathname = '/qx-script';
apiEndpoint.searchParams.set('url', `${scriptPath}`);

// parts[1] => Effective URL Rule
result.push(parts[1], 'url', 'script-response-body', apiEndpoint.toString());

return result.join(' ');

default:
return '';
}

};
3 changes: 2 additions & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface CommandConfig {
readonly artifacts: ReadonlyArray<ArtifactConfig>;
readonly remoteSnippets?: ReadonlyArray<RemoteSnippetConfig>;
readonly urlBase: string;
publicUrl: string; // tslint:disable-line:readonly-keyword
readonly providerDir: string;
readonly templateDir: string;
readonly configDir: string;
Expand All @@ -37,7 +38,7 @@ export interface CommandConfig {
readonly binPath?: {
readonly shadowsocksr?: string;
readonly v2ray?: string;
vmess?: string; // tslint:disable-line
vmess?: string; // tslint:disable-line:readonly-keyword
};
readonly surgeConfig?: {
readonly v2ray?: 'native'|'external';
Expand Down
13 changes: 11 additions & 2 deletions lib/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Joi from '@hapi/joi';
import fs from 'fs-extra';
import { url } from 'inspector';
import _ from 'lodash';
import path from 'path';
import { URL } from 'url';

import { CommandConfig } from '../types';
import { PROXY_TEST_INTERVAL, PROXY_TEST_URL } from './constant';
Expand Down Expand Up @@ -47,11 +49,18 @@ export const normalizeConfig = (cwd: string, userConfig: Partial<CommandConfig>)

// istanbul ignore next
if (!fs.existsSync(config.templateDir)) {
throw new Error(`You must create ${config.templateDir} first.`);
throw new Error(`仓库内缺少 ${config.templateDir} 目录`);
}
// istanbul ignore next
if (!fs.existsSync(config.providerDir)) {
throw new Error(`You must create ${config.providerDir} first.`);
throw new Error(`仓库内缺少 ${config.providerDir} 目录`);
}

if (/http/i.test(config.urlBase)) {
const urlObject = new URL(config.urlBase);
config.publicUrl = urlObject.origin + '/';
} else {
config.publicUrl = '/';
}

return config;
Expand Down
18 changes: 15 additions & 3 deletions lib/utils/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,21 @@ export const CLASH_UNSUPPORTED_RULE: ReadonlyArray<string> = [
'SRC-IP'
];

export const QUANTUMULT_X_UNSUPPORTED_RULE: ReadonlyArray<string> = [
'URL-REGEX', 'PROCESS-NAME', 'IP-CIDR6', 'AND', 'OR', 'NOT',
'DEST-PORT', 'SRC-IP', 'IN-PORT', 'RULE-SET'
export const QUANTUMULT_X_SUPPORTED_RULE: ReadonlyArray<string> = [
// 原生支持
'USER-AGENT',
'HOST',
'HOST-KEYWORD',
'HOST-SUFFIX',
'DOMAIN',
'DOMAIN-SUFFIX',
'DOMAIN-KEYWORD',
'IP-CIDR',
'GEOIP',
'FINAL',

// 转换
'HTTP-RESPONSE',
];

export const MELLOW_UNSUPPORTED_RULE: ReadonlyArray<string> = [
Expand Down
4 changes: 2 additions & 2 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export const getShadowsocksSubscription = async (
debug('SS URI', item);
const scheme = legacyUrl.parse(item, true);
const userInfo = fromUrlSafeBase64(scheme.auth).split(':');
const pluginInfo = typeof scheme.query.plugin === 'string' ? decodeStringList<any>(scheme.query.plugin.split(';')) : {};
const pluginInfo = typeof scheme.query.plugin === 'string' ? decodeStringList(scheme.query.plugin.split(';')) : {};

return {
type: NodeTypeEnum.Shadowsocks,
Expand Down Expand Up @@ -1038,7 +1038,7 @@ export const pickAndFormatStringList = (obj: object, keyList: readonly string[])
return result;
};

export const decodeStringList = <T = object>(stringList: ReadonlyArray<string>): T => {
export const decodeStringList = <T = Record<string, string|boolean>>(stringList: ReadonlyArray<string>): T => {
const result = {};
stringList.forEach(item => {
const pair = item.split('=');
Expand Down
13 changes: 13 additions & 0 deletions test/asset/surge-script-list.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# RULE 1
http-response ^https?://m?api\.weibo\.c(n|om)/2/(statuses/(unread|extend|positives/get|(friends|video)(/|_)timeline)|stories/(video_stream|home_list)|(groups|fangle)/timeline|profile/statuses|comments/build_comments|photo/recommend_list|service/picfeed|searchall|cardlist|page|\!/photos/pic_recommend_status) script-path=https://raw.githubusercontent.com/yichahucha/surge/master/wb_ad.js,requires-body=true
http-response ^https?://(sdk|wb)app\.uve\.weibo\.com(/interface/sdk/sdkad.php|/wbapplua/wbpullad.lua) script-path=https://raw.githubusercontent.com/yichahucha/surge/master/wb_launch.js,requires-body=true

# RULE 2
http-response https://api.zhihu.com/moments\?(action|feed_type) requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20feed.js,script-update-interval=-1
http-response https://api.zhihu.com/topstory/recommend requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20recommend.js,script-update-interval=-1
http-response https://api.zhihu.com/.*/questions requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20answer.js,script-update-interval=-1
http-response https://api.zhihu.com/market/header requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20market.js,script-update-interval=-1
http-response https://api.zhihu.com/people/ requires-body=1,max-size=0,script-path=https://raw.githubusercontent.com/onewayticket255/Surge-Script/master/surge%20zhihu%20people.js,script-update-interval=-1

# RULE 3
dns dnspod script-path=https://example.com/dnspod.js
15 changes: 15 additions & 0 deletions test/snapshots/template.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,18 @@ Generated by [AVA](https://ava.li).
DOMAIN-SUFFIX,googlevideo.com␊
DOMAIN-SUFFIX,youtube.com␊
DOMAIN,youtubei.googleapis.com`

## quantumultx filter 2

> Snapshot 1
`# RULE 1␊
^https?://m?api\\.weibo\\.c(n|om)/2/(statuses/(unread|extend|positives/get|(friends|video)(/|_)timeline)|stories/(video_stream|home_list)|(groups|fangle)/timeline|profile/statuses|comments/build_comments|photo/recommend_list|service/picfeed|searchall|cardlist|page|\\!/photos/pic_recommend_status) url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fyichahucha%2Fsurge%2Fmaster%2Fwb_ad.js␊
^https?://(sdk|wb)app\\.uve\\.weibo\\.com(/interface/sdk/sdkad.php|/wbapplua/wbpullad.lua) url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fyichahucha%2Fsurge%2Fmaster%2Fwb_launch.js␊
# RULE 2␊
https://api.zhihu.com/moments\\?(action|feed_type) url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fonewayticket255%2FSurge-Script%2Fmaster%2Fsurge%2520zhihu%2520feed.js␊
https://api.zhihu.com/topstory/recommend url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fonewayticket255%2FSurge-Script%2Fmaster%2Fsurge%2520zhihu%2520recommend.js␊
https://api.zhihu.com/.*/questions url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fonewayticket255%2FSurge-Script%2Fmaster%2Fsurge%2520zhihu%2520answer.js␊
https://api.zhihu.com/market/header url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fonewayticket255%2FSurge-Script%2Fmaster%2Fsurge%2520zhihu%2520market.js␊
https://api.zhihu.com/people/ url script-response-body https://example.com/qx-script?url=https%3A%2F%2Fraw.githubusercontent.com%2Fonewayticket255%2FSurge-Script%2Fmaster%2Fsurge%2520zhihu%2520people.js␊
# RULE 3`
Binary file modified test/snapshots/template.test.ts.snap
Binary file not shown.
18 changes: 14 additions & 4 deletions test/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from 'fs-extra';
import path from 'path';
import getEngine from '../lib/template';

const templateEngine = getEngine(process.cwd());
const templateEngine = getEngine(process.cwd(), 'https://example.com/');

test('renderString #1', t => {
const body = `{{ str | patchYamlArray }}`;
Expand Down Expand Up @@ -87,6 +87,16 @@ test('quantumultx filter 1', t => {
t.is(result, '');
});

test('quantumultx filter 2', t => {
const body = `{{ str | quantumultx }}`;
const str = fs.readFileSync(path.join(__dirname, './asset/surge-script-list.txt'), { encoding: 'utf8' });
const result = templateEngine.renderString(body, {
str,
});

t.snapshot(result);
});

test('mellow filter 1', t => {
const body = `{{ str | mellow }}`;
const str = `IP-CIDR,67.198.55.0/24,Proxy,no-resolve // test rule`;
Expand Down Expand Up @@ -120,9 +130,9 @@ test('mellow filter 3', t => {
test('spaces in string', t => {
const str = ` `;

t.is(templateEngine.renderString(`{{ str | mellow }}`, { str }), '');
t.is(templateEngine.renderString(`{{ str | quantumultx }}`, { str }), '');
t.is(templateEngine.renderString(`{{ str | patchYamlArray }}`, { str }), '');
t.is(templateEngine.renderString(`{{ str | mellow }}`, { str }), ' ');
t.is(templateEngine.renderString(`{{ str | quantumultx }}`, { str }), ' ');
t.is(templateEngine.renderString(`{{ str | patchYamlArray }}`, { str }), ' ');
});

test('ForeignMedia', t => {
Expand Down

0 comments on commit 82633d2

Please sign in to comment.