组合订阅现在允许添加所有节点操作

This commit is contained in:
Peng-YM 2020-11-24 21:21:38 +08:00
parent f886bc11e9
commit ff4790f12e
7 changed files with 202 additions and 97 deletions

View File

@ -25,7 +25,7 @@ function startService() {
___/ // /_/ // /_/ //_____/___/ // /_ / /_/ // / / __/ ___/ // /_/ // /_/ //_____/___/ // /_ / /_/ // / / __/
/____/ \__,_//_.___/ /____/ \__/ \____//_/ \___/ /____/ \__,_//_.___/ /____/ \__/ \____//_/ \___/
*/ */
}); });
console.log(welcome); console.log(welcome);
const $app = express(); const $app = express();
// Constants // Constants
@ -90,22 +90,36 @@ function startService() {
$app.get("/api/utils/env", getEnv); // get runtime environment $app.get("/api/utils/env", getEnv); // get runtime environment
$app.get("/api/utils/backup", gistBackup); // gist backup actions $app.get("/api/utils/backup", gistBackup); // gist backup actions
// Redirect sub.store to vercel webpage
$app.get("/", async (req, res) => {
// 302 redirect
res.set("location", "https://sub-store.vercel.app/").status(302).end();
});
// handle preflight request for QX
if (ENV().isQX) {
$app.options("/", async (req, res) => {
res.status(200).end();
});
}
$app.start(); $app.start();
// subscriptions API // subscriptions API
async function downloadSubscription(req, res) { async function downloadSubscription(req, res) {
const {name} = req.params; const {name} = req.params;
const {cache} = req.query || false; const {cache} = req.query;
const platform = req.query.target || getPlatformFromHeaders(req.headers); const platform = req.query.target || getPlatformFromHeaders(req.headers);
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name]; const sub = allSubs[name];
if (sub) { if (sub) {
try { try {
const raw = await getResource(sub.url, cache); const useCache = typeof cache === 'undefined' ? (platform === 'JSON' || platform === 'URI') : cache;
const raw = await getResource(sub.url, useCache);
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
// apply processors // apply processors
proxies = await ProxyUtils.process(proxies, sub.process); proxies = await ProxyUtils.process(proxies, sub.process || []);
// produce // produce
const output = ProxyUtils.produce(proxies, platform); const output = ProxyUtils.produce(proxies, platform);
if (platform === 'JSON') { if (platform === 'JSON') {
@ -138,6 +152,7 @@ function startService() {
function createSubscription(req, res) { function createSubscription(req, res) {
const sub = req.body; const sub = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
$.info(`正在创建订阅: ${sub.name}`);
if (allSubs[sub.name]) { if (allSubs[sub.name]) {
res.status(500).json({ res.status(500).json({
status: "failed", status: "failed",
@ -185,6 +200,7 @@ function startService() {
...allSubs[name], ...allSubs[name],
...sub, ...sub,
}; };
$.info(`正在更新订阅: ${name}`);
// allow users to update the subscription name // allow users to update the subscription name
if (name !== sub.name) { if (name !== sub.name) {
// we need to find out all collections refer to this name // we need to find out all collections refer to this name
@ -216,6 +232,7 @@ function startService() {
function deleteSubscription(req, res) { function deleteSubscription(req, res) {
const {name} = req.params; const {name} = req.params;
$.info(`删除订阅:${name}...`);
// delete from subscriptions // delete from subscriptions
let allSubs = $.read(SUBS_KEY); let allSubs = $.read(SUBS_KEY);
delete allSubs[name]; delete allSubs[name];
@ -260,9 +277,12 @@ function startService() {
const sub = allSubs[subs[i]]; const sub = allSubs[subs[i]];
$.info(`正在处理子订阅:${sub.name},进度--${100 * (i + 1 / subs.length).toFixed(1)}% `); $.info(`正在处理子订阅:${sub.name},进度--${100 * (i + 1 / subs.length).toFixed(1)}% `);
try { try {
const raw = await getResource(sub.url, cache); const useCache = typeof cache === 'undefined' ? (platform === 'JSON' || platform === 'URI') : cache;
const raw = await getResource(sub.url, useCache);
// parse proxies // parse proxies
proxies = proxies.concat(ProxyUtils.parse(raw)); proxies = proxies.concat(ProxyUtils.parse(raw));
// apply processors
proxies = await ProxyUtils.process(proxies, sub.process || []);
} catch (err) { } catch (err) {
$.error(`处理组合订阅中的子订阅: ${sub.name}时出现错误:${err}! 该订阅已被跳过。`); $.error(`处理组合订阅中的子订阅: ${sub.name}时出现错误:${err}! 该订阅已被跳过。`);
} }
@ -312,6 +332,7 @@ function startService() {
function createCollection(req, res) { function createCollection(req, res) {
const collection = req.body; const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`);
const allCol = $.read(COLLECTIONS_KEY); const allCol = $.read(COLLECTIONS_KEY);
if (allCol[collection.name]) { if (allCol[collection.name]) {
res.status(500).json({ res.status(500).json({
@ -360,6 +381,7 @@ function startService() {
...allCol[name], ...allCol[name],
...collection, ...collection,
}; };
$.info(`正在更新组合订阅:${name}...`);
// allow users to update collection name // allow users to update collection name
delete allCol[name]; delete allCol[name];
allCol[collection.name || name] = newCol; allCol[collection.name || name] = newCol;
@ -378,6 +400,7 @@ function startService() {
function deleteCollection(req, res) { function deleteCollection(req, res) {
const {name} = req.params; const {name} = req.params;
$.info(`正在删除组合订阅:${name}`);
let allCol = $.read(COLLECTIONS_KEY); let allCol = $.read(COLLECTIONS_KEY);
delete allCol[name]; delete allCol[name];
$.write(allCol, COLLECTIONS_KEY); $.write(allCol, COLLECTIONS_KEY);
@ -423,6 +446,7 @@ function startService() {
async function refreshCache(req, res) { async function refreshCache(req, res) {
const {url} = req.body; const {url} = req.body;
$.info(`Refreshing cache for URL: ${url}`);
try { try {
const raw = await getResource(url, false); const raw = await getResource(url, false);
$.write(raw, `#${Base64.safeEncode(url)}`); $.write(raw, `#${Base64.safeEncode(url)}`);
@ -531,6 +555,9 @@ function startService() {
return resource; return resource;
} }
const {body} = await $http.get(url); const {body} = await $http.get(url);
if (body.replace(/\s/g, "").length === 0) {
throw new Error("订阅内容为空!");
}
$.write(body, key); $.write(body, key);
$.write(new Date().getTime(), timeKey); $.write(new Date().getTime(), timeKey);
return body; return body;
@ -666,7 +693,7 @@ var ProxyUtils = (function () {
return producer.produce(proxy); return producer.produce(proxy);
} catch (err) { } catch (err) {
$.error( $.error(
`ERROR: cannot produce proxy: ${JSON.stringify( `Cannot produce proxy: ${JSON.stringify(
proxy, null, 2 proxy, null, 2
)}\nReason: ${err}` )}\nReason: ${err}`
); );
@ -1385,7 +1412,7 @@ var PROXY_PARSERS = (function () {
if (params.length > 4) { if (params.length > 4) {
const [key, val] = params[4].split(":"); const [key, val] = params[4].split(":");
if (key === "tls-name") proxy.sni = val; if (key === "tls-name") proxy.sni = val;
else throw new Error(`ERROR: unknown option ${key} for line: \n${line}`); else throw new Error(`Unknown option ${key} for line: \n${line}`);
} }
return proxy; return proxy;
}; };
@ -2122,7 +2149,7 @@ var PROXY_PROCESSORS = (function () {
if (output_) output = output_; if (output_) output = output_;
} catch (err) { } catch (err) {
// print log and skip this operator // print log and skip this operator
console.log(`ERROR: cannot apply operator ${op.name}! Reason: ${err}`); console.log(`Cannot apply operator ${op.name}! Reason: ${err}`);
} }
return output; return output;
} }
@ -2413,9 +2440,8 @@ var PROXY_PRODUCERS = (function () {
} }
function URI_Producer() { function URI_Producer() {
const targetPlatform = "URI"; const type = "SINGLE";
const Base64 = new Base64Code(); const produce = (proxy) => {
const output = (proxy) => {
let result = ""; let result = "";
switch (proxy.type) { switch (proxy.type) {
case "ss": case "ss":
@ -2489,6 +2515,7 @@ var PROXY_PRODUCERS = (function () {
} }
return result; return result;
} }
return {type, produce};
} }
function JSON_Producer() { function JSON_Producer() {

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@
<v-icon left x-small>mdi-flash</v-icon> <v-icon left x-small>mdi-flash</v-icon>
TFO TFO
</v-chip> </v-chip>
<v-chip x-small v-if="proxy.scert" color="error" outlined> <v-chip x-small v-if="proxy['skip-cert-verify']" color="error" outlined>
<v-icon left x-small>error</v-icon> <v-icon left x-small>error</v-icon>
SCERT SCERT
</v-chip> </v-chip>
@ -117,20 +117,18 @@ export default {
}, },
methods: { methods: {
refresh() { refresh() {
axios.post(`/refresh`, {url: this.sub}).then(() => { axios.post(`/utils/refresh`, {url: this.sub}).then(() => {
this.fetch(); this.fetch();
}).catch(err => {
this.$store.commit("SET_ERROR_MESSAGE", err.response.data.message);
}) })
}, },
async fetch() { async fetch() {
await axios.get(this.url).then(resp => { await axios.get(this.url).then(resp => {
let {data} = resp; let {data} = resp;
if ((typeof data === 'string' || data instanceof String) && data.indexOf("\n") !== -1){ // eslint-disable-next-line no-debugger
this.proxies = data.split("\n").map(p => JSON.parse(p)); this.proxies = data;
}
else {
this.proxies = [data];
}
}).catch(err => { }).catch(err => {
this.$store.commit("SET_ERROR_MESSAGE", err); this.$store.commit("SET_ERROR_MESSAGE", err);
}); });
@ -150,7 +148,7 @@ export default {
async showInfo(idx) { async showInfo(idx) {
const {server, name} = this.proxies[idx]; const {server, name} = this.proxies[idx];
const res = await axios.get(`/IP_API/${encodeURIComponent(server)}`).then(resp => resp.data); const res = await axios.get(`/utils/IP_API/${encodeURIComponent(server)}`).then(resp => resp.data);
this.info.name = name; this.info.name = name;
this.info.isp = `ISP${res.isp}`; this.info.isp = `ISP${res.isp}`;
this.info.region = `地区:${flags.get(res.countryCode)} ${res.regionName} ${res.city}`; this.info.region = `地区:${flags.get(res.countryCode)} ${res.regionName} ${res.city}`;

View File

@ -6,7 +6,6 @@ import Subscription from "@/views/Subscription";
import Dashboard from "@/views/Dashboard"; import Dashboard from "@/views/Dashboard";
import User from "@/views/User"; import User from "@/views/User";
import SubEditor from "@/views/SubEditor"; import SubEditor from "@/views/SubEditor";
import CollectionEditor from "@/views/CollectionEditor";
Vue.use(Router); Vue.use(Router);
@ -40,7 +39,8 @@ const router = new Router({
{ {
path: "/collection-edit/:name", path: "/collection-edit/:name",
name: "collection-edit", name: "collection-edit",
component: CollectionEditor, component: SubEditor,
props: {isCollection: true},
meta: {title: "订阅编辑"} meta: {title: "订阅编辑"}
} }
] ]

View File

@ -45,21 +45,21 @@ const store = new Vuex.Store({
actions: { actions: {
// fetch subscriptions // fetch subscriptions
async FETCH_SUBSCRIPTIONS({state}) { async FETCH_SUBSCRIPTIONS({state}) {
return axios.get("/sub").then(resp => { return axios.get("/subs").then(resp => {
const {data} = resp.data; const {data} = resp.data;
state.subscriptions = data; state.subscriptions = data;
}); });
}, },
// fetch collections // fetch collections
async FETCH_COLLECTIONS({state}) { async FETCH_COLLECTIONS({state}) {
return axios.get("/collection").then(resp => { return axios.get("/collections").then(resp => {
const {data} = resp.data; const {data} = resp.data;
state.collections = data; state.collections = data;
}); });
}, },
// fetch env // fetch env
async FETCH_ENV({state}) { async FETCH_ENV({state}) {
return axios.get("/env").then(resp => { return axios.get("/utils/env").then(resp => {
state.env = resp.data; state.env = resp.data;
}) })
}, },
@ -72,7 +72,7 @@ const store = new Vuex.Store({
}, },
// new subscription // new subscription
async NEW_SUBSCRIPTION({dispatch}, sub) { async NEW_SUBSCRIPTION({dispatch}, sub) {
return axios.post(`/sub`, sub).then(() => { return axios.post(`/subs`, sub).then(() => {
dispatch("FETCH_SUBSCRIPTIONS"); dispatch("FETCH_SUBSCRIPTIONS");
}); });
}, },
@ -91,7 +91,7 @@ const store = new Vuex.Store({
}, },
// new collection // new collection
async NEW_COLLECTION({dispatch}, collection) { async NEW_COLLECTION({dispatch}, collection) {
return axios.post(`/collection`, collection).then(() => { return axios.post(`/collections`, collection).then(() => {
dispatch("FETCH_COLLECTIONS"); dispatch("FETCH_COLLECTIONS");
}) })
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<v-container> <v-container>
<v-card class="mb-4"> <v-card class="mb-4">
<v-subheader>基本信息</v-subheader> <v-subheader>订阅配置</v-subheader>
<v-form class="pl-4 pr-4 pb-0" v-model="formState.basicValid"> <v-form class="pl-4 pr-4 pb-0" v-model="formState.basicValid">
<v-text-field <v-text-field
v-model="options.name" v-model="options.name"
@ -13,7 +13,9 @@
clearable clearable
clear-icon="clear" clear-icon="clear"
/> />
<!--For Subscription-->
<v-textarea <v-textarea
v-if="!isCollection"
v-model="options.url" v-model="options.url"
class="mt-2" class="mt-2"
rows="2" rows="2"
@ -24,6 +26,27 @@
clearable clearable
clear-icon="clear" clear-icon="clear"
/> />
<!--For Collection-->
<v-list
v-if="isCollection"
dense
>
<v-subheader class="pl-0">包含的订阅</v-subheader>
<v-list-item v-for="sub in availableSubs" :key="sub.name">
<v-list-item-avatar dark>
<v-icon>mdi-cloud</v-icon>
</v-list-item-avatar>
<v-list-item-content>
{{ sub.name }}
</v-list-item-content>
<v-spacer></v-spacer>
<v-checkbox
:value="sub.name"
v-model="selected"
class="pr-1"
/>
</v-list-item>
</v-list>
</v-form> </v-form>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -46,7 +69,7 @@
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-textarea <v-textarea
v-model="importedSub" v-model="imported"
solo solo
label="粘贴配置以导入" label="粘贴配置以导入"
rows="5" rows="5"
@ -56,7 +79,7 @@
/> />
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn text color="primary" @click="importSub">确认</v-btn> <v-btn text color="primary" @click="importConf">确认</v-btn>
<v-btn text @click="showShareDialog = false">取消</v-btn> <v-btn text @click="showShareDialog = false">取消</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@ -105,7 +128,7 @@
</v-radio-group> </v-radio-group>
<v-radio-group <v-radio-group
v-model="options.scert" v-model="options['skip-cert-verify']"
dense dense
class="mt-0 mb-0" class="mt-0 mb-0"
> >
@ -258,6 +281,14 @@ const AVAILABLE_PROCESSORS = {
} }
export default { export default {
props: {
isCollection: {
type: Boolean,
default() {
return false;
}
}
},
components: { components: {
FlagOperator, FlagOperator,
KeywordFilter, KeywordFilter,
@ -277,7 +308,7 @@ export default {
return { return {
selectedProcess: null, selectedProcess: null,
showShareDialog: false, showShareDialog: false,
importedSub: "", imported: "",
dialog: false, dialog: false,
validations: { validations: {
nameRules: [ nameRules: [
@ -300,21 +331,37 @@ export default {
url: "", url: "",
useless: "KEEP", useless: "KEEP",
udp: "DEFAULT", udp: "DEFAULT",
scert: "DEFAULT", "skip-cert-verify": "DEFAULT",
tfo: "DEFAULT", tfo: "DEFAULT",
}, },
process: [], process: [],
selected: []
} }
}, },
created() { created() {
const name = this.$route.params.name; const name = this.$route.params.name;
const sub = (typeof name === 'undefined' || name === 'UNTITLED') ? {} : this.$store.state.subscriptions[name]; let source;
this.$store.commit("SET_NAV_TITLE", sub.name ? `订阅编辑 -- ${sub.name}` : "新建订阅"); if (this.isCollection) {
const {options, process} = loadSubscription(this.options, sub); source = (typeof name === 'undefined' || name === 'UNTITLED') ? {} : this.$store.state.collections[name];
this.$store.commit("SET_NAV_TITLE", source.name ? `组合订阅编辑 -- ${source.name}` : "新建组合订阅");
this.selected = source.subscriptions || [];
} else {
source = (typeof name === 'undefined' || name === 'UNTITLED') ? {} : this.$store.state.subscriptions[name];
this.$store.commit("SET_NAV_TITLE", source.name ? `订阅编辑 -- ${source.name}` : "新建订阅");
}
this.name = source.name;
const {options, process} = loadProcess(this.options, source);
this.options = options; this.options = options;
this.process = process; this.process = process;
}, },
computed: { computed: {
availableSubs() {
return this.$store.state.subscriptions;
},
availableProcessors() { availableProcessors() {
return AVAILABLE_PROCESSORS; return AVAILABLE_PROCESSORS;
}, },
@ -327,45 +374,101 @@ export default {
id: p.id id: p.id
} }
}); });
},
config() {
const output = {
name: this.options.name,
process: []
};
if (this.isCollection) {
output.subscriptions = this.selected;
} else {
output.url = this.options.url;
}
// useless filter
if (this.options.useless === 'REMOVE') {
output.process.push({
type: "Useless Filter"
});
}
// udp, tfo, scert
for (const opt of ['udp', 'tfo', 'skip-cert-verify']) {
if (this.options[opt] !== 'DEFAULT') {
output.process.push({
type: "Set Property Operator",
args: {key: opt, value: this.options[opt] === 'FORCE_OPEN'}
});
}
}
for (const p of this.process) {
output.process.push(p);
}
return output;
} }
}, },
methods: { methods: {
save() { save() {
if (this.isCollection) {
if (this.options.name && this.selected) {
if (this.$route.params.name === 'UNTITLED') {
this.$store.dispatch("NEW_COLLECTION", this.config).then(() => {
showInfo(`成功创建组合订阅:${this.name}`)
}).catch(() => {
showError(`发生错误,无法创建组合订阅!`)
});
} else {
this.$store.dispatch("UPDATE_COLLECTION", {
name: this.$route.params.name,
collection: this.config
}).then(() => {
showInfo(`成功保存组合订阅:${this.name}`)
}).catch(() => {
showError(`发生错误,无法保存组合订阅!`)
});
}
}
} else {
console.log("Saving subscription...");
if (this.options.name && this.options.url) { if (this.options.name && this.options.url) {
const sub = buildSubscription(this.options, this.process);
if (this.$route.params.name !== "UNTITLED") { if (this.$route.params.name !== "UNTITLED") {
this.$store.dispatch("UPDATE_SUBSCRIPTION", { this.$store.dispatch("UPDATE_SUBSCRIPTION", {
name: this.$route.params.name, name: this.$route.params.name,
sub sub: this.config
}).then(() => { }).then(() => {
showInfo(`成功保存订阅:${this.options.name}`); showInfo(`成功保存订阅:${this.options.name}`);
}).catch(() => { }).catch(() => {
showError(`发生错误,无法保存订阅!`); showError(`发生错误,无法保存订阅!`);
}); });
} else { } else {
this.$store.dispatch("NEW_SUBSCRIPTION", sub).then(() => { this.$store.dispatch("NEW_SUBSCRIPTION", this.config).then(() => {
showInfo(`成功创建订阅:${this.options.name}`); showInfo(`成功创建订阅:${this.options.name}`);
}).catch(() => { }).catch(() => {
showError(`发生错误,无法创建订阅!`); showError(`发生错误,无法创建订阅!`);
}); });
} }
} }
}
}, },
share() { share() {
let sub = buildSubscription(this.options, this.process); let config = this.config;
sub.name = "「订阅名称」"; config.name = "「订阅名称」";
sub.url = "「订阅链接」"; if (this.isCollection) {
sub = JSON.stringify(sub); config.subscriptions = [];
this.$clipboard(sub); } else {
config.url = "「订阅链接」";
}
config = JSON.stringify(config);
this.$clipboard(config);
this.$store.commit("SET_SUCCESS_MESSAGE", "导出成功,订阅已复制到剪贴板!"); this.$store.commit("SET_SUCCESS_MESSAGE", "导出成功,订阅已复制到剪贴板!");
this.showShareDialog = false; this.showShareDialog = false;
}, },
importSub() { importConf() {
if (this.importedSub) { if (this.imported) {
const sub = JSON.parse(this.importedSub); const sub = JSON.parse(this.imported);
const {options, process} = loadSubscription(this.options, sub); const {options, process} = loadProcess(this.options, sub);
delete options.name; delete options.name;
delete options.url; delete options.url;
@ -433,16 +536,20 @@ export default {
} }
} }
function loadSubscription(options, sub) { function loadProcess(options, source, isCollection = false) {
options = { options = {
...options, ...options,
name: sub.name, name: source.name,
url: sub.url, };
if (isCollection) {
options.subscriptions = source.subscriptions;
} else {
options.url = source.url;
} }
let process = [] let process = []
// flag // flag
for (const p of (sub.process || [])) { for (const p of (source.process || [])) {
switch (p.type) { switch (p.type) {
case 'Useless Filter': case 'Useless Filter':
options.useless = "REMOVE"; options.useless = "REMOVE";
@ -458,33 +565,6 @@ function loadSubscription(options, sub) {
return {options, process}; return {options, process};
} }
function buildSubscription(options, process) {
const sub = {
name: options.name,
url: options.url,
process: []
};
// useless filter
if (options.useless === 'REMOVE') {
sub.process.push({
type: "Useless Filter"
});
}
// udp, tfo, scert
for (const opt of ['udp', 'tfo', 'scert']) {
if (options[opt] !== 'DEFAULT') {
sub.process.push({
type: "Set Property Operator",
args: {key: opt, value: options[opt] === 'FORCE_OPEN'}
});
}
}
for (const p of process) {
sub.process.push(p);
}
return sub;
}
function uuidv4() { function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);

View File

@ -52,7 +52,7 @@ export default {
return; return;
} }
this.save(); this.save();
axios.get(`/backup?action=${action}`).then(resp => { axios.get(`/utils/backup?action=${action}`).then(resp => {
if (resp.data.status === 'success') { if (resp.data.status === 'success') {
this.$store.commit("SET_SUCCESS_MESSAGE", `${action === 'upload' ? "备份" : "还原"}成功!`); this.$store.commit("SET_SUCCESS_MESSAGE", `${action === 'upload' ? "备份" : "还原"}成功!`);
this.updateStore(this.$store); this.updateStore(this.$store);