├── .editorconfig
├── .env
├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .umirc.js
├── LICENSE
├── README.md
├── functions
├── QuantumultXScriptAddDeviceID.js
├── QuantumultXScriptSubscriptionAddDeviceID.js
├── SSR2Clash.js
├── SSRDecode.js
├── SurgeProfile2SurgeList.js
├── TestGetEnvInfo.js
├── ds
│ └── Surge.js
├── protect
│ └── password.js
└── testDownload.js
├── mock
└── .gitkeep
├── netlify.toml
├── package.json
├── src
├── app.js
├── assets
│ └── yay.jpg
├── global.css
├── layouts
│ ├── __tests__
│ │ └── index.test.js
│ ├── index.css
│ └── index.js
├── models
│ └── .gitkeep
└── pages
│ ├── 404.css
│ ├── 404.js
│ ├── __tests__
│ └── index.test.js
│ ├── generator
│ ├── QuantumultXScriptAddDeviceID.css
│ ├── QuantumultXScriptAddDeviceID.js
│ ├── QuantumultXScriptSubscriptionAddDeviceID.css
│ ├── QuantumultXScriptSubscriptionAddDeviceID.js
│ ├── QuantumultXScriptSubscriptionAddDeviceIDPreset.css
│ └── QuantumultXScriptSubscriptionAddDeviceIDPreset.js
│ ├── index.css
│ └── index.js
├── webpack.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | BROWSER=none
2 | ESLINT=1
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-umi"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # netlify
2 | .netlify/
3 |
4 | # node
5 | node_modules/
6 | /npm-debug.log*
7 | /yarn-error.log
8 |
9 | # production
10 | /dist
11 |
12 | # misc
13 | .DS_Store
14 |
15 | # umi
16 | .umi
17 | .umi-production
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.svg
3 | **/*.ejs
4 | **/*.html
5 | package.json
6 | .umi
7 | .umi-production
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 100,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.umirc.js:
--------------------------------------------------------------------------------
1 | // ref: https://umijs.org/config/
2 | export default {
3 | treeShaking: true,
4 | routes: [
5 | {
6 | path: '/',
7 | component: '../layouts/index',
8 | routes: [
9 | {
10 | path: '/generator/QuantumultXScriptAddDeviceID',
11 | component: './generator/QuantumultXScriptAddDeviceID',
12 | },
13 | {
14 | path: '/generator/QuantumultXScriptSubscriptionAddDeviceID',
15 | component: './generator/QuantumultXScriptSubscriptionAddDeviceID',
16 | },
17 | {
18 | path: '/generator/QuantumultXScriptSubscriptionAddDeviceIDPreset',
19 | component: './generator/QuantumultXScriptSubscriptionAddDeviceIDPreset',
20 | },
21 | {
22 | path: '/',
23 | component: '../pages/index',
24 | },
25 | {
26 | path: '/404',
27 | component: '../pages/404',
28 | },
29 | ],
30 | },
31 | ],
32 | plugins: [
33 | // ref: https://umijs.org/plugin/umi-plugin-react.html
34 | [
35 | 'umi-plugin-react',
36 | {
37 | antd: true,
38 | dva: true,
39 | dynamicImport: false,
40 | title: 'CC',
41 | dll: true,
42 | routes: {
43 | exclude: [
44 | /models\//,
45 | /services\//,
46 | /model\.(t|j)sx?$/,
47 | /service\.(t|j)sx?$/,
48 | /components\//,
49 | ],
50 | },
51 | },
52 | ],
53 | ]
54 | };
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Singee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ConfigConverter
2 | [](https://app.netlify.com/sites/config-converter/deploys)
3 |
4 | 将各种代理软件的配置文件进行转换
5 |
6 | ## API Endpoint
7 |
8 | 使用方式请参考 [部署与说明文档](https://www.markeditor.com/file/get/eb581bd61fad7c345853e2ac1a5482f8?t=1574667122)
9 |
10 | - `/api/SurgeProfile2SurgeList` 将 Surge 配置文件转换为 List
11 | - `/api/QuantumultXScriptAddDeviceID` 将 QX 脚本中添加设备 ID
12 | - `/api/QuantumultXScriptSubscriptionAddDeviceID` 自动为 QX 脚本订阅添加设备 ID 行
13 |
14 |
15 | ## 自部署
16 |
17 | [](https://app.netlify.com/start/deploy?repository=https://github.com/ImSingee/ConfigConverter)
18 |
19 | 相关更新将在 [Singee 的日常](https://t.me/singee_daily) 中发布
20 |
21 | 有问题可以通过 [@Bryan](https://t.me/atbryanbot) 联系我
--------------------------------------------------------------------------------
/functions/QuantumultXScriptAddDeviceID.js:
--------------------------------------------------------------------------------
1 | const request = require('flyio');
2 | const isUrl = require('is-url');
3 | const { checkPassword } = require('./protect/password');
4 |
5 | exports.handler = function (event, context, callback) {
6 | if (!checkPassword(event)) {
7 | return callback(null, {
8 | headers: {
9 | "Content-Type": "text/plain; charset=utf-8"
10 | },
11 | statusCode: 401,
12 | body: "未提供密码或提供的密码不正确。"
13 | });
14 | }
15 |
16 | const { queryStringParameters } = event;
17 | const url = queryStringParameters['src'];
18 | const deviceId = queryStringParameters['id'];
19 |
20 | console.log('url: ', url);
21 | console.log('deviceId: ', deviceId);
22 |
23 | if (!isUrl(url)) {
24 | console.log('URL is invlid');
25 | return callback(null, {
26 | headers: {
27 | "Content-Type": "text/plain; charset=utf-8"
28 | },
29 | statusCode: 400,
30 | body: "参数 src 无效,请检查是否提供了正确的脚本文件托管地址。"
31 | });
32 | }
33 | if (!deviceId) {
34 | console.log('deviceId is not found');
35 | return callback(null, {
36 | headers: {
37 | "Content-Type": "text/plain; charset=utf-8"
38 | },
39 | statusCode: 400,
40 | body: "参数 id 无效,请检查是否提供了正确的设备 ID。"
41 | });
42 | }
43 |
44 | request.get(url).then(({ data }) => {
45 | console.log('File fetched success.');
46 | const result = `/**\n * @supported ${deviceId}\n */\n\n` + data;
47 |
48 | return callback(null, {
49 | headers: {
50 | "Content-Type": "text/plain; charset=utf-8"
51 | },
52 | statusCode: 200,
53 | body: result
54 | });
55 | }).catch(err => {
56 | return callback(null, {
57 | headers: {
58 | "Content-Type": "text/plain; charset=utf-8"
59 | },
60 | statusCode: 400,
61 | body: err
62 | });
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/functions/QuantumultXScriptSubscriptionAddDeviceID.js:
--------------------------------------------------------------------------------
1 | const request = require('flyio');
2 | const isUrl = require('is-url');
3 |
4 | const URLSafeBase64 = require('urlsafe-base64');
5 | const QueryString = require('query-string');
6 | const { checkPassword } = require('./protect/password');
7 |
8 | const { URL: HOST, PRESET_NUMBER } = process.env;
9 |
10 | const PRESETS = {};
11 | if (Number(PRESET_NUMBER) > 0) {
12 | for (let i = 1; i <= PRESET_NUMBER; i++) {
13 | PRESETS[i] = process.env[`PRESET_${i}`];
14 | }
15 | }
16 |
17 | exports.handler = function (event, context, callback) {
18 | if (!checkPassword(event)) {
19 | return callback(null, {
20 | headers: {
21 | "Content-Type": "text/plain; charset=utf-8"
22 | },
23 | statusCode: 401,
24 | body: "未提供密码或提供的密码不正确。"
25 | });
26 | }
27 |
28 | const { queryStringParameters } = event;
29 | const preset = Number(queryStringParameters['preset']);
30 | const paramsB64 = queryStringParameters['b64'];
31 | const password = queryStringParameters['pwd'] || '';
32 | let url, deviceId;
33 |
34 | if (isNaN(preset)) {
35 | if (paramsB64) {
36 | const params = QueryString.parse(URLSafeBase64.decode(paramsB64).toString());
37 | url = params.src;
38 | deviceId = params.id;
39 | } else {
40 | url = queryStringParameters['src'];
41 | const deviceIdRaw = queryStringParameters['id'];
42 | if (deviceIdRaw) {
43 | deviceId = deviceIdRaw.replace(/\./g, '');
44 | } else {
45 | const deviceIdB64 = queryStringParameters['idb64'];
46 | if (deviceIdB64) {
47 | deviceId = URLSafeBase64.decode(deviceIdB64).toString();
48 | } else {
49 | console.log('deviceIdB64 is not found');
50 | return callback(null, {
51 | headers: {
52 | "Content-Type": "text/plain; charset=utf-8"
53 | },
54 | statusCode: 400,
55 | body: "参数 id (或其他替换结果)无效,请检查是否提供了正确的设备 ID。"
56 | });
57 | }
58 |
59 | }
60 |
61 | }
62 | } else {
63 | if (preset > 0 && preset <= PRESET_NUMBER) {
64 | const paramsStr = PRESETS[preset];
65 | if (!paramsStr) {
66 | return callback(null, {
67 | headers: {
68 | "Content-Type": "text/plain; charset=utf-8"
69 | },
70 | statusCode: 400,
71 | body: "参数 preset 对应的预设不存在。"
72 | });
73 | }
74 |
75 | const params = QueryString.parse(paramsStr);
76 | url = params.src;
77 | deviceId = params.id;
78 | } else {
79 | return callback(null, {
80 | headers: {
81 | "Content-Type": "text/plain; charset=utf-8"
82 | },
83 | statusCode: 400,
84 | body: "参数 preset 不在允许的范围内。"
85 | });
86 | }
87 | }
88 |
89 | console.log('url: ', url);
90 | console.log('deviceId: ', deviceId);
91 |
92 |
93 | if (!isUrl(url)) {
94 | console.log('URL is invlid');
95 | return callback(null, {
96 | headers: {
97 | "Content-Type": "text/plain; charset=utf-8"
98 | },
99 | statusCode: 400,
100 | body: "参数 src 无效,请检查是否提供了正确的脚本订阅文件托管地址。"
101 | });
102 | }
103 | if (!deviceId) {
104 | console.log('deviceId is not found');
105 | return callback(null, {
106 | headers: {
107 | "Content-Type": "text/plain; charset=utf-8"
108 | },
109 | statusCode: 400,
110 | body: "参数 id (或其他替换结果)无效,请检查是否提供了正确的设备 ID。"
111 | });
112 | }
113 |
114 | request.get(url).then(({ data }) => {
115 | console.log('File fetched success.');
116 | const allLines = data.split('\n');
117 | const resultLines = [];
118 |
119 | for (const singleLine of allLines) {
120 | const singleLineTrimed = singleLine.trim();
121 | if (singleLineTrimed === '') {
122 | ;// Do nothing
123 | } else if (singleLineTrimed.startsWith('hostname')) {
124 | resultLines.push(singleLineTrimed);
125 | } else if (singleLineTrimed.startsWith('#')) {
126 | ;// Do nothing
127 | } else if (singleLineTrimed.startsWith(';')) {
128 | ;// Do nothing
129 | } else {
130 | const currentLineElements = singleLineTrimed.split(/\s+/);
131 | if (currentLineElements.length < 4) {
132 | resultLines.push(singleLineTrimed);
133 | } else if (currentLineElements[2] !== 'script-response-body') {
134 | resultLines.push(singleLineTrimed);
135 | } else {
136 | currentLineElements[3] = `${HOST}/api/QuantumultXScriptAddDeviceID?id=${encodeURIComponent(deviceId)}&src=${encodeURIComponent(currentLineElements[3])}`;
137 | if (password) {
138 | currentLineElements[3] += `&pwd=${encodeURIComponent(password)}`;
139 | }
140 | resultLines.push(currentLineElements.join(' '));
141 | }
142 | }
143 |
144 | }
145 |
146 | return callback(null, {
147 | headers: {
148 | "Content-Type": "text/plain; charset=utf-8"
149 | },
150 | statusCode: 200,
151 | body: `;deviceId = ${deviceId}\n;url = ${url}\n\n` + resultLines.join('\n')
152 | });
153 | }).catch(err => {
154 | return callback(null, {
155 | headers: {
156 | "Content-Type": "text/plain; charset=utf-8"
157 | },
158 | statusCode: 400,
159 | body: err
160 | });
161 | })
162 | }
163 |
--------------------------------------------------------------------------------
/functions/SSR2Clash.js:
--------------------------------------------------------------------------------
1 | const fly = require("flyio");
2 | const atob = require('atob');
3 | const isUrl = require('is-url');
4 |
5 | exports.handler = function(event, context, callback) {
6 | const { queryStringParameters } = event;
7 |
8 | const url = queryStringParameters['src'];
9 |
10 | if (!isUrl(url)) {
11 | return callback(null, {
12 | headers: {
13 | "Content-Type": "text/plain; charset=utf-8"
14 | },
15 | statusCode: 400,
16 | body: "参数 src 无效,请检查是否提供了正确的节点订阅地址。"
17 | });
18 | }
19 |
20 | fly.get(url).then(response => {
21 | const bodyDecoded = atob(response.data);
22 | const links = bodyDecoded.split('\n');
23 | const filteredLinks = links.filter(link => {
24 | // Only support ss & ssr now
25 | if (link.startsWith('ss://')) return true;
26 | if (link.startsWith('ssr://')) return true;
27 | return false;
28 | });
29 |
30 | if (filteredLinks.length == 0) {
31 | return callback(null, {
32 | headers: {
33 | "Content-Type": "text/plain; charset=utf-8"
34 | },
35 | statusCode: 400,
36 | body: "订阅地址中没有节点信息。"
37 | });
38 | }
39 | const processedLinks = new Array();
40 | filteredLinks.forEach(link => {
41 | // 将订阅链接包装为对象
42 | if (link.startsWith('ss://')) {
43 |
44 | }
45 |
46 | // 过滤非 origin、plain 的 SSR 节点(Clash 暂时只支持 SS)
47 |
48 | // DEBUG
49 | processedLinks.push(link);
50 | })
51 |
52 | if (processedLinks.length == 0) {
53 | return callback(null, {
54 | statusCode: 400,
55 | body: "订阅地址中没有节点信息。"
56 | });
57 | }
58 |
59 |
60 |
61 | // DEBUG
62 | return callback(null, {
63 | headers: {
64 | "Content-Type": "text/plain; charset=utf-8"
65 | },
66 | statusCode: 200,
67 | body: JSON.stringify(processedLinks)
68 | });
69 | }).catch(error => {
70 | // 404
71 | if (error && !isNaN(error.status)) {
72 | return callback(null, {
73 | headers: {
74 | "Content-Type": "text/plain; charset=utf-8"
75 | },
76 | statusCode: 400,
77 | body: "订阅地址网站出现了一个 " + String(error.status) + " 错误。"
78 | });
79 | }
80 |
81 | // Unknown
82 | return callback(null, {
83 | headers: {
84 | "Content-Type": "text/plain; charset=utf-8"
85 | },
86 | statusCode: 500,
87 | body: "Unexpected Error.\n" + JSON.stringify(error)
88 | });
89 | })
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/functions/SSRDecode.js:
--------------------------------------------------------------------------------
1 | const URLSafeBase64 = require('urlsafe-base64');
2 |
3 | exports.handler = function(event, context, callback) {
4 | const { queryStringParameters } = event;
5 | const encodedStr = queryStringParameters['s'];
6 | if (!URLSafeBase64.validate(encodedStr)) {
7 | return callback(null, {
8 | headers: {
9 | "Content-Type": "text/plain; charset=utf-8"
10 | },
11 | statusCode: 400,
12 | body: "参数无效"
13 | })
14 | }
15 |
16 | const decodedStr = URLSafeBase64.decode(encodedStr).toString();
17 |
18 | let host, port, protocol, method, obfs, base64password, password;
19 | let base64obfsparam, obfsparam, base64protoparam, protoparam, base64remarks, remarks, base64group, group, udpport, uot;
20 |
21 | const requiredParams = decodedStr.split(':');
22 | if (requiredParams.length != 6) {
23 | return callback(null, {
24 | headers: {
25 | "Content-Type": "text/plain; charset=utf-8"
26 | },
27 | statusCode: 400,
28 | body: "参数无效"
29 | })
30 | }
31 |
32 | host = requiredParams[0];
33 | port = requiredParams[1];
34 | protocol = requiredParams[2];
35 | method = requiredParams[3];
36 | obfs = requiredParams[4];
37 |
38 | const tempGroup = requiredParams[5].split('/?')
39 | base64password = tempGroup[0];
40 | password = URLSafeBase64.decode(base64password).toString();
41 |
42 | if (tempGroup.length > 1) {
43 | const optionalParams = tempGroup[1];
44 | optionalParams.split('&').forEach(param => {
45 | const temp = param.split('=');
46 | let key = temp[0], value;
47 | if (temp.length > 1) {
48 | value = temp[1];
49 | }
50 |
51 | if (value) {
52 | switch (key) {
53 | case 'obfsparam':
54 | base64obfsparam = value;
55 | obfsparam = URLSafeBase64.decode(base64obfsparam).toString();
56 | break;
57 | case 'protoparam':
58 | base64protoparam = value;
59 | protoparam = URLSafeBase64.decode(base64protoparam).toString();
60 | break;
61 | case 'remarks':
62 | base64remarks = value;
63 | remarks = URLSafeBase64.decode(base64remarks).toString();
64 | break;
65 | case 'group':
66 | base64group = value;
67 | group = URLSafeBase64.decode(base64group).toString();
68 | break;
69 | case 'udpport':
70 | udpport = value;
71 | break;
72 | case 'uot':
73 | uot = value;
74 | break;
75 | }
76 | }
77 | })
78 | }
79 |
80 | result = {
81 | type: 'ss/ssr',
82 | host, port, protocol, method, obfs, base64password, password,
83 | base64obfsparam, obfsparam, base64protoparam, protoparam, base64remarks, remarks, base64group, group, udpport, uot
84 | }
85 |
86 |
87 |
88 | callback(null, {
89 | headers: {
90 | "Content-Type": "text/plain; charset=utf-8"
91 | },
92 | statusCode: 200,
93 | body: JSON.stringify(result)
94 | })
95 | }
--------------------------------------------------------------------------------
/functions/SurgeProfile2SurgeList.js:
--------------------------------------------------------------------------------
1 | const request = require('flyio');
2 | const isUrl = require('is-url');
3 | const Surge = require('./ds/Surge');
4 | const { checkPassword } = require('./protect/password');
5 |
6 | exports.handler = function (event, context, callback) {
7 | if (!checkPassword(event)) {
8 | return callback(null, {
9 | headers: {
10 | "Content-Type": "text/plain; charset=utf-8"
11 | },
12 | statusCode: 401,
13 | body: "未提供密码或提供的密码不正确。"
14 | });
15 | }
16 |
17 | const { queryStringParameters } = event;
18 | const url = queryStringParameters['src'];
19 | const preset = queryStringParameters['preset'];
20 | const filter = queryStringParameters['filter'];
21 | const filterURL = queryStringParameters['filter_url'];
22 |
23 | console.log('url: ', url);
24 |
25 | if (!isUrl(url)) {
26 | console.log('URL is invlid');
27 | return callback(null, {
28 | headers: {
29 | "Content-Type": "text/plain; charset=utf-8"
30 | },
31 | statusCode: 400,
32 | body: "参数 src 无效,请检查是否提供了正确的 Surge Profile 托管地址。"
33 | });
34 | }
35 |
36 | request.get(url).then(({ data }) => {
37 | console.log('Profile fetched success.');
38 | const surge = new Surge(data);
39 | console.log('Build Surge object success.');
40 | let result;
41 |
42 | if (preset) {
43 | result = surge.preset(preset);
44 | } else {
45 | result = surge.list();
46 | if (filter) {
47 | result = result.filter(filter);
48 | }
49 | if (filterURL) {
50 | result = result.filterURL(filterURL);
51 | }
52 | result = result.generate();
53 | }
54 |
55 | return callback(null, {
56 | headers: {
57 | "Content-Type": "text/plain; charset=utf-8"
58 | },
59 | statusCode: 200,
60 | body: result
61 | });
62 | }).catch(err => {
63 | return callback(null, {
64 | headers: {
65 | "Content-Type": "text/plain; charset=utf-8"
66 | },
67 | statusCode: 400,
68 | body: err
69 | });
70 | })
71 | }
72 |
--------------------------------------------------------------------------------
/functions/TestGetEnvInfo.js:
--------------------------------------------------------------------------------
1 | const { checkPassword } = require('./protect/password');
2 |
3 | exports.handler = function (event, context, callback) {
4 | if (!checkPassword(event, true)) {
5 | return callback(null, {
6 | headers: {
7 | "Content-Type": "text/plain; charset=utf-8"
8 | },
9 | statusCode: 401,
10 | body: "未提供密码或提供的密码不正确。"
11 | });
12 | }
13 |
14 | const env = process.env;
15 |
16 | return callback(null, {
17 | headers: {
18 | "Content-Type": "text/plain; charset=utf-8"
19 | },
20 | statusCode: 200,
21 | body: JSON.stringify({ event, context, env })
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/functions/ds/Surge.js:
--------------------------------------------------------------------------------
1 | const ini = require('ini');
2 |
3 | class Names {
4 | constructor(names, Proxy) {
5 | this.names = names;
6 | this.Proxy = Proxy;
7 | }
8 |
9 | generate() {
10 | let result = '';
11 | for (let name of this.names) {
12 | result += `${name} = ${this.Proxy[name]}\n`
13 | }
14 | return result;
15 | }
16 |
17 | filter(keyword) {
18 | const keywords = keyword.split('+');
19 | const names = this.names.filter(name => {
20 | for (const kw of keywords) {
21 | if (name.indexOf(kw) !== -1) {
22 | return true;
23 | }
24 | }
25 | })
26 |
27 | return new Names(names, this.Proxy);
28 | }
29 |
30 | filterURL(keyword) {
31 | const keywords = keyword.split('+');
32 | const names = this.names.filter(name => {
33 | const splitResult = this.Proxy[name].split(',');
34 | const url = splitResult[1].trim();
35 |
36 | for (const kw of keywords) {
37 | if (url.indexOf(kw) !== -1) {
38 | return true;
39 | }
40 | }
41 | })
42 |
43 | return new Names(names, this.Proxy);
44 | }
45 |
46 | static join(...exists) {
47 | const names = [];
48 | const set = new Set();
49 | for (const exist of exists) {
50 | exist.names.map(name => {
51 | if (!set.has(name)) {
52 | names.push(name);
53 | set.add(name);
54 | }
55 | })
56 | }
57 |
58 | return new Names(names, exists[0].Proxy);
59 | }
60 | }
61 |
62 | class Surge {
63 | constructor(profile) {
64 | const config = ini.parse(profile);
65 | const { Proxy } = config;
66 | // console.log(Proxy);
67 | // console.log(Object.keys(Proxy));
68 | this.Proxy = Proxy;
69 | }
70 |
71 | list() {
72 | return new Names(Object.keys(this.Proxy), this.Proxy);
73 | }
74 |
75 | preset(presetName) {
76 | if (presetName === 'netflix') {
77 | // default: HK+MO+TW
78 | return this.list().filter('HKT+HKBN+CTM+江苏中转+HINET').generate();
79 | } else if (presetName === 'netflix_hk') {
80 | return this.list().filter('HKT+HKBN').generate();
81 | } else if (presetName === 'netflix_mo') {
82 | return this.list().filter('CTM+江苏中转').generate();
83 | } else if (presetName === 'netflix_tw') {
84 | return this.list().filter('HINET').generate();
85 | } else if (presetName === 'netflix_jp') {
86 | return this.list().filter('IDCF+软银').generate();
87 | } else if (presetName === 'netflix_us') {
88 | return this.list().filter('美国').filter('CN2').generate();
89 | } else if (presetName === 'netflix_iplc') {
90 | const listBase = this.list().filter('IPLC');
91 | return Names.join(listBase.filter('深港+沪港'), listBase.filterURL('hanggang.001+hanggang.002')).generate();
92 | }
93 |
94 | return this.list().generate();
95 | }
96 |
97 | }
98 |
99 | module.exports = Surge;
--------------------------------------------------------------------------------
/functions/protect/password.js:
--------------------------------------------------------------------------------
1 | const { FORCE_PASSWORD: FORCE_PASSWORD_str, PASSWORD } = process.env;
2 | const FORCE_PASSWORD = FORCE_PASSWORD_str === 'True' ? true : false;
3 |
4 | function checkPassword(event, force = false) {
5 | const { queryStringParameters } = event;
6 | const { pwd: password } = queryStringParameters;
7 |
8 | if (!force && !FORCE_PASSWORD) return true;
9 |
10 | return password === PASSWORD;
11 | }
12 |
13 | module.exports = { checkPassword };
--------------------------------------------------------------------------------
/functions/testDownload.js:
--------------------------------------------------------------------------------
1 | const request = require('request-promise');
2 | const isUrl = require('is-url');
3 |
4 | exports.handler = function (event, context, callback) {
5 | const { queryStringParameters } = event;
6 | const url = queryStringParameters['src'];
7 |
8 | console.log('url: ', url);
9 |
10 | if (!isUrl(url)) {
11 | console.log('URL is invlid');
12 | return callback(null, {
13 | headers: {
14 | "Content-Type": "text/plain; charset=utf-8"
15 | },
16 | statusCode: 400,
17 | body: "参数 src 无效,请检查是否提供了正确的网址。"
18 | });
19 | }
20 |
21 | request.get(url).then(data => {
22 | console.log('File fetched success.');
23 |
24 | return callback(null, {
25 | headers: {
26 | "Content-Type": "text/plain; charset=utf-8"
27 | },
28 | statusCode: 200,
29 | body: data
30 | });
31 | }).catch(err => {
32 | console.error('Error', err);
33 | return callback(null, {
34 | headers: {
35 | "Content-Type": "text/plain; charset=utf-8"
36 | },
37 | statusCode: 400,
38 | body: '请求错误,请查看日志'
39 | });
40 | })
41 | }
42 |
--------------------------------------------------------------------------------
/mock/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImSingee/ConfigConverter/56d2e380925b5d57d6a400a61905492a2a430a08/mock/.gitkeep
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | functions = "./functions"
3 | command = "yarn build"
4 |
5 | [build.environment]
6 | NODE_VERSION = "12"
7 |
8 | [template.environment]
9 | FORCE_PASSWORD = "设置为 True 表示强制设置密码"
10 | PASSWORD = "设置一个密码"
11 | PRESET_NUMBER = "启用的预设数量"
12 | PRESET_1 = "预设 1 的参数"
13 | PRESET_2 = "预设 2 的参数"
14 | PRESET_3 = "预设 3 的参数"
15 | PRESET_4 = "预设 4 的参数"
16 | PRESET_5 = "预设 5 的参数"
17 |
18 | [[redirects]]
19 | from = "/api/*"
20 | to = "/.netlify/functions/:splat"
21 | status = 200
22 | force = true
23 |
24 | [[redirects]]
25 | from = "/umi.css"
26 | to = "/dist/umi.css"
27 | status = 200
28 |
29 | [[redirects]]
30 | from = "/umi.js"
31 | to = "/dist/umi.js"
32 | status = 200
33 |
34 | [[redirects]]
35 | from = "/*"
36 | to = "/dist/index.html"
37 | status = 200
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "umi dev",
4 | "build": "umi build",
5 | "test": "umi test",
6 | "lint": "eslint --ext .js src mock tests",
7 | "precommit": "lint-staged"
8 | },
9 | "dependencies": {
10 | "antd": "^3.19.5",
11 | "atob": "^2.1.2",
12 | "copy-to-clipboard": "^3.2.0",
13 | "dva": "^2.6.0-beta.6",
14 | "flyio": "^0.6.14",
15 | "ini": "^1.3.5",
16 | "is-url": "^1.2.4",
17 | "query-string": "^6.9.0",
18 | "react": "^16.8.6",
19 | "react-dom": "^16.8.6",
20 | "request": "^2.88.0",
21 | "request-promise": "^4.2.5",
22 | "urlsafe-base64": "^1.0.0"
23 | },
24 | "devDependencies": {
25 | "babel-eslint": "^9.0.0",
26 | "eslint": "^5.4.0",
27 | "eslint-config-umi": "^1.4.0",
28 | "eslint-plugin-flowtype": "^2.50.0",
29 | "eslint-plugin-import": "^2.14.0",
30 | "eslint-plugin-jsx-a11y": "^5.1.1",
31 | "eslint-plugin-react": "^7.11.1",
32 | "husky": "^0.14.3",
33 | "lint-staged": "^7.2.2",
34 | "react-test-renderer": "^16.7.0",
35 | "umi": "^2.7.7",
36 | "umi-plugin-react": "^1.8.4"
37 | },
38 | "lint-staged": {
39 | "*.{js,jsx}": [
40 | "eslint --fix",
41 | "git add"
42 | ]
43 | },
44 | "engines": {
45 | "node": ">=8.0.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | export const dva = {
2 | config: {
3 | onError(err) {
4 | err.preventDefault();
5 | console.error(err.message);
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/assets/yay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImSingee/ConfigConverter/56d2e380925b5d57d6a400a61905492a2a430a08/src/assets/yay.jpg
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | html, body, #root {
2 | height: 100%;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/layouts/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | import BasicLayout from '..';
2 | import renderer from 'react-test-renderer';
3 |
4 | describe('Layout: BasicLayout', () => {
5 | it('Render correctly', () => {
6 | const wrapper = renderer.create(