Minor changes

This commit is contained in:
Peng-YM
2022-05-25 11:00:00 +08:00
parent c389aa19a2
commit 0e46d8e14d
10 changed files with 41 additions and 38 deletions

View File

@@ -0,0 +1,413 @@
import { ProxyUtils } from '../core/proxy-utils';
import { RuleUtils } from '../core/rule-utils';
import download from '../utils/download';
import Gist from '../utils/gist';
import $ from '../core/app';
import {
SUBS_KEY,
ARTIFACTS_KEY,
ARTIFACT_REPOSITORY_KEY,
COLLECTIONS_KEY,
RULES_KEY,
SETTINGS_KEY,
} from './constants';
export default function register($app) {
// Initialization
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
// RESTful APIs
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
$app.route('/api/artifact/:name')
.get(getArtifact)
.patch(updateArtifact)
.delete(deleteArtifact);
// sync all artifacts
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
}
async function getArtifact(req, res) {
const name = req.params.name;
const action = req.query.action;
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = allArtifacts[name];
if (artifact) {
if (action) {
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({
type: artifact.type,
item,
platform: artifact.platform,
});
if (action === 'preview') {
res.send(output);
} else if (action === 'sync') {
$.info(`正在上传配置:${artifact.name}\n>>>`);
console.log(JSON.stringify(artifact, null, 2));
try {
const resp = await syncArtifact({
[artifact.name]: { content: output },
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
});
} catch (err) {
res.status(500).json({
status: 'failed',
message: err,
});
}
}
} else {
res.json({
status: 'success',
data: artifact,
});
}
} else {
res.status(404).json({
status: 'failed',
message: '未找到对应的配置!',
});
}
}
function createArtifact(req, res) {
const artifact = req.body;
$.info(`正在创建远程配置:${artifact.name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
if (allArtifacts[artifact.name]) {
res.status(500).json({
status: 'failed',
message: `远程配置${artifact.name}已存在!`,
});
} else {
if (/^[\w-_.]*$/.test(artifact.name)) {
allArtifacts[artifact.name] = artifact;
$.write(allArtifacts, ARTIFACTS_KEY);
res.status(201).json({
status: 'success',
data: artifact,
});
} else {
res.status(500).json({
status: 'failed',
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
}
function updateArtifact(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
const oldName = req.params.name;
const artifact = allArtifacts[oldName];
if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`);
const newArtifact = req.body;
if (
typeof newArtifact.name !== 'undefined' &&
!/^[\w-_.]*$/.test(newArtifact.name)
) {
res.status(500).json({
status: 'failed',
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
} else {
const merged = {
...artifact,
...newArtifact,
};
allArtifacts[merged.name] = merged;
if (merged.name !== oldName) delete allArtifacts[oldName];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
data: merged,
});
}
} else {
res.status(404).json({
status: 'failed',
message: '未找到对应的远程配置!',
});
}
}
async function cronSyncArtifacts(_, res) {
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {};
try {
await Promise.all(
Object.values(allArtifacts).map(async (artifact) => {
if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`);
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({
type: artifact.type,
item,
platform: artifact.platform,
});
files[artifact.name] = {
content: output,
};
}
}),
);
const resp = await syncArtifact(files);
const body = JSON.parse(resp.body);
for (const artifact of Object.values(allArtifacts)) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!');
res.status(200).end();
} catch (err) {
res.status(500).json({
error: err,
});
$.info(`同步订阅失败,原因:${err}`);
}
}
async function deleteArtifact(req, res) {
const name = req.params.name;
$.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
try {
const artifact = allArtifacts[name];
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) {
// delete gist
await syncArtifact({
filename: name,
content: '',
});
}
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
});
} catch (err) {
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.status(500).json({
status: 'failed',
message: `无法删除远程配置:${name}, 原因:${err}`,
});
}
}
function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({
status: 'success',
data: allArtifacts,
});
}
async function syncArtifact(files) {
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
return Promise.reject('未设置Gist Token');
}
const manager = new Gist({
token: gistToken,
key: ARTIFACT_REPOSITORY_KEY,
});
return manager.upload(files);
}
async function produceArtifact(
{ type, item, platform, noProcessor } = {
platform: 'JSON',
noProcessor: false,
},
) {
if (type === 'subscription') {
const sub = item;
const raw = await download(sub.url, sub.ua);
// parse proxies
let proxies = ProxyUtils.parse(raw);
if (!noProcessor) {
// apply processors
proxies = await ProxyUtils.process(
proxies,
sub.process || [],
platform,
);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const collection = item;
const subs = collection['subscriptions'];
const results = {};
let processed = 0;
await Promise.all(
subs.map(async (name) => {
const sub = allSubs[name];
try {
$.info(`正在处理子订阅:${sub.name}...`);
const raw = await download(sub.url, sub.ua);
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
if (!noProcessor) {
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
platform,
);
}
results[name] = currentProxies;
processed++;
$.info(
`✅ 子订阅:${sub.name}加载成功,进度--${
100 * (processed / subs.length).toFixed(1)
}% `,
);
} catch (err) {
processed++;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${err},该订阅已被跳过!进度--${
100 * (processed / subs.length).toFixed(1)
}%`,
);
}
}),
);
// merge proxies with the original order
let proxies = Array.prototype.concat.apply(
[],
subs.map((name) => results[name]),
);
if (!noProcessor) {
// apply own processors
proxies = await ProxyUtils.process(
proxies,
collection.process || [],
platform,
);
}
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点!`);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
}
return ProxyUtils.produce(proxies, platform);
} else if (type === 'rule') {
const rule = item;
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
$.info(
`正在处理URL${url},进度--${
100 * ((i + 1) / rule.urls.length).toFixed(1)
}% `,
);
try {
const { body } = await download(url);
const currentRules = RuleUtils.parse(body);
rules = rules.concat(currentRules);
} catch (err) {
$.error(
`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`,
);
}
}
// remove duplicates
rules = await RuleUtils.process(rules, [
{ type: 'Remove Duplicate Filter' },
]);
// produce output
return RuleUtils.produce(rules, platform);
}
}
export { produceArtifact };

View File

@@ -0,0 +1,166 @@
import { getPlatformFromHeaders, getFlowHeaders } from './subscriptions';
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
import { produceArtifact } from './artifacts';
import $ from '../core/app';
export default function register($app) {
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
$app.get('/download/collection/:name', downloadCollection);
$app.route('/api/collection/:name')
.get(getCollection)
.patch(updateCollection)
.delete(deleteCollection);
$app.route('/api/collections')
.get(getAllCollections)
.post(createCollection);
}
// collection API
async function downloadCollection(req, res) {
const { name } = req.params;
const { raw } = req.query || 'false';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCollections = $.read(COLLECTIONS_KEY);
const collection = allCollections[name];
$.info(`正在下载组合订阅:${name}`);
// forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY);
const subs = collection['subscriptions'];
if (subs.length > 0) {
const sub = allSubs[subs[0]];
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
if (collection) {
try {
const output = await produceArtifact({
type: 'collection',
item: collection,
platform,
noProcessor: raw,
});
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 下载组合订阅错误:${name}`,
`🤔 原因:${err}`,
);
res.status(500).json({
status: 'failed',
message: err,
});
}
} else {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
res.status(404).json({
status: 'failed',
});
}
}
function createCollection(req, res) {
const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`);
const allCol = $.read(COLLECTIONS_KEY);
if (allCol[collection.name]) {
res.status(500).json({
status: 'failed',
message: `订阅集${collection.name}已存在!`,
});
}
// validate name
if (/^[\w-_]*$/.test(collection.name)) {
allCol[collection.name] = collection;
$.write(allCol, COLLECTIONS_KEY);
res.status(201).json({
status: 'success',
data: collection,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
function getCollection(req, res) {
const { name } = req.params;
const collection = $.read(COLLECTIONS_KEY)[name];
if (collection) {
res.json({
status: 'success',
data: collection,
});
} else {
res.status(404).json({
status: 'failed',
message: `未找到订阅集:${name}!`,
});
}
}
function updateCollection(req, res) {
const { name } = req.params;
let collection = req.body;
const allCol = $.read(COLLECTIONS_KEY);
if (allCol[name]) {
const newCol = {
...allCol[name],
...collection,
};
$.info(`正在更新组合订阅:${name}...`);
// allow users to update collection name
delete allCol[name];
allCol[collection.name || name] = newCol;
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: 'success',
data: newCol,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅集${name}不存在,无法更新!`,
});
}
}
function deleteCollection(req, res) {
const { name } = req.params;
$.info(`正在删除组合订阅:${name}`);
let allCol = $.read(COLLECTIONS_KEY);
delete allCol[name];
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: 'success',
});
}
function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
res.json({
status: 'success',
data: allCols,
});
}

View File

@@ -0,0 +1,7 @@
export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections';
export const ARTIFACTS_KEY = 'artifacts';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

View File

@@ -0,0 +1,118 @@
import {
SETTINGS_KEY,
GIST_BACKUP_KEY,
GIST_BACKUP_FILE_NAME,
} from './constants';
import { ENV } from '../utils/open-api';
import express from '../utils/express';
import { IP_API } from '../utils/geo';
import Gist from '../utils/gist';
import $ from '../core/app';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerSettingRoutes from './settings';
export default function serve() {
const $app = express();
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
registerSettingRoutes($app);
registerArtifactRoutes($app);
// utils
$app.get('/api/utils/IP_API/:server', IP_API); // IP-API reverse proxy
$app.get('/api/utils/env', getEnv); // get runtime environment
$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.all('/', (_, res) => {
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
});
$app.start();
}
function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
res.json({
backend,
});
}
async function gistBackup(req, res) {
const { action } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
res.status(500).json({
status: 'failed',
message: '未找到Gist备份Token!',
});
} else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
});
try {
let content;
const settings = $.read(SETTINGS_KEY);
switch (action) {
case 'upload':
// update syncTime.
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content } });
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
Object.keys(content).forEach((key) => {
$.write(content[key], key);
});
}
break;
}
res.json({
status: 'success',
});
} catch (err) {
const msg = `${
action === 'upload' ? '上传' : '下载'
}备份失败!${err}`;
$.error(msg);
res.status(500).json({
status: 'failed',
message: msg,
});
}
}
}

View File

@@ -0,0 +1,27 @@
import { SETTINGS_KEY } from './constants';
import $ from '../core/app';
export default function register($app) {
if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY);
$app.route('/api/settings').get(getSettings).patch(updateSettings);
}
function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY);
res.json(settings);
}
function updateSettings(req, res) {
const data = req.body;
const settings = $.read(SETTINGS_KEY);
$.write(
{
...settings,
...data,
},
SETTINGS_KEY,
);
res.json({
status: 'success',
});
}

View File

@@ -0,0 +1,216 @@
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
import { produceArtifact } from './artifacts';
import $ from '../core/app';
export default function register($app) {
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
$app.get('/download/:name', downloadSubscription);
$app.route('/api/sub/:name')
.get(getSubscription)
.patch(updateSubscription)
.delete(deleteSubscription);
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
}
// subscriptions API
async function downloadSubscription(req, res) {
const { name } = req.params;
const { raw } = req.query || 'false';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name];
if (sub) {
try {
const output = await produceArtifact({
type: 'subscription',
item: sub,
platform,
noProcessor: raw,
});
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
);
$.error(JSON.stringify(err));
res.status(500).json({
status: 'failed',
message: err,
});
}
} else {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}`);
res.status(404).json({
status: 'failed',
});
}
}
function createSubscription(req, res) {
const sub = req.body;
const allSubs = $.read(SUBS_KEY);
$.info(`正在创建订阅: ${sub.name}`);
if (allSubs[sub.name]) {
res.status(500).json({
status: 'failed',
message: `订阅${sub.name}已存在!`,
});
}
// validate name
if (/^[\w-_]*$/.test(sub.name)) {
allSubs[sub.name] = sub;
$.write(allSubs, SUBS_KEY);
res.status(201).json({
status: 'success',
data: sub,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
function getSubscription(req, res) {
const { name } = req.params;
const sub = $.read(SUBS_KEY)[name];
if (sub) {
res.json({
status: 'success',
data: sub,
});
} else {
res.status(404).json({
status: 'failed',
message: `未找到订阅:${name}!`,
});
}
}
function updateSubscription(req, res) {
const { name } = req.params;
let sub = req.body;
const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) {
const newSub = {
...allSubs[name],
...sub,
};
$.info(`正在更新订阅: ${name}`);
// allow users to update the subscription name
if (name !== sub.name) {
// we need to find out all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) {
const idx = allCols[k].subscriptions.indexOf(name);
if (idx !== -1) {
allCols[k].subscriptions[idx] = sub.name;
}
}
// update subscriptions
delete allSubs[name];
allSubs[sub.name] = newSub;
} else {
allSubs[name] = newSub;
}
$.write(allSubs, SUBS_KEY);
res.json({
status: 'success',
data: newSub,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅${name}不存在,无法更新!`,
});
}
}
function deleteSubscription(req, res) {
const { name } = req.params;
$.info(`删除订阅:${name}...`);
// delete from subscriptions
let allSubs = $.read(SUBS_KEY);
delete allSubs[name];
$.write(allSubs, SUBS_KEY);
// delete from collections
let allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) {
allCols[k].subscriptions = allCols[k].subscriptions.filter(
(s) => s !== name,
);
}
$.write(allCols, COLLECTIONS_KEY);
res.json({
status: 'success',
});
}
function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY);
res.json({
status: 'success',
data: allSubs,
});
}
export async function getFlowHeaders(url) {
const { headers } = await $.http.get({
url,
headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
},
});
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
break;
}
}
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (UA.indexOf('Surge') !== -1) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (
UA.indexOf('Stash') !== -1 ||
UA.indexOf('Shadowrocket') !== -1
) {
return 'Clash';
} else {
return null;
}
}