Compare commits

...

26 Commits
dev ... 2.12.4

Author SHA1 Message Date
Peng-YM
e93332048e fix: Occasional crashed when performing migration 2022-08-10 00:28:46 +08:00
Peng-YM
4dcb9ae79e feat: Include cron-sync-artifact in Stash configuration 2022-08-09 22:42:13 +08:00
Peng-YM
6ea3575101 fix: Rename subscription and collection will break artifacts 2022-08-09 22:28:45 +08:00
Peng-YM
26820ea892 fix: proxy duplicate issue 2022-08-04 20:40:40 +08:00
Peng-YM
f64e8ecfe4 fix: Loon shadowsocksr obfs-param incorrect 2022-08-02 09:23:34 +08:00
Peng-YM
77604a3544 perf (core): DomainResolveProcessor now cache results 2022-07-19 21:29:06 +08:00
Peng-YM
8f5d027080 fix (cron-sync-artifact): sync timeout due to missing await 2022-07-19 20:55:18 +08:00
Peng-YM
5244de4dba fix (config): Sub-Store url is incorrect in Surge.sgmodule 2022-07-13 15:14:11 +08:00
Peng-YM
4121ec2970 chore (gh-action): Trigger workflow run only if package.json have been modified 2022-07-13 15:10:30 +08:00
Peng-YM
a949c49192 perf: Use the latest release scripts in configs 2022-07-13 14:53:44 +08:00
Peng-YM
9fba3506f0 chore: Update GitHub action to automatically release new version 2022-07-13 14:41:34 +08:00
Peng-YM
9677c7ebbd fix (product): cron-sync-artifacts not working 2022-07-13 14:03:43 +08:00
github-actions@github.com
03149dcadb Release 2022-07-13 02:32:52 +00:00
Peng-YM
1bfa6ebb2c fix (core): trojan sni is lost when parsing Clash nodes
#build
2022-07-13 10:31:08 +08:00
Peng-YM
4cd525824e fix (restful): Add query field in IP-API 2022-07-12 23:15:29 +08:00
Peng-YM
813f2b839d perf: Add switch for cron-sync-artifacts 2022-07-12 18:56:24 +08:00
Peng-YM
3d58534dfe fix (restful): Intercept IP-API query failed message when querying node info 2022-07-12 18:34:31 +08:00
Peng-YM
f7333c0617 perf: Include cron script for syncing artifacts in configurations 2022-07-12 18:15:05 +08:00
Peng-YM
f7d4b66db6 feat (restful): Add /api/utils/node-info for querying proxy node info 2022-07-12 15:09:44 +08:00
Peng-YM
8c844eb23a chore: Use pnpm in GitHub action 2022-07-11 23:38:57 +08:00
Peng-YM
de892aaa2b fix: Vmess auto/none cipher parsed incorrectly 2022-07-11 23:33:06 +08:00
Peng-YM
b143476e71 fix (core): Clash Vmess servername does parse correctly 2022-07-11 23:20:21 +08:00
Peng-YM
2c4e47166d feat (restful): Add /api/utils/refresh
The API call does the following:
- Fetch GitHub avatar and update artifact store url
- Revoke all cached resources
2022-07-11 23:06:49 +08:00
Peng-YM
6881148021 perf: Use cache for all remote resources 2022-07-11 20:46:16 +08:00
Peng-YM
848491c0f8 feat: Add support for targetPlatform ShadowRocket 2022-07-11 18:23:56 +08:00
Peng-YM
49c8f2e521 perf: Modify revert.js to completely clear sub-store cache 2022-07-11 18:22:42 +08:00
38 changed files with 818 additions and 152 deletions

View File

@@ -1,13 +1,15 @@
name: update
name: build
on:
push:
branches:
- master
- dev
paths:
- 'backend/package.json'
pull_request:
branches:
- master
- dev
paths:
- 'backend/package.json'
jobs:
build:
runs-on: ubuntu-latest
@@ -21,16 +23,30 @@ jobs:
with:
node-version: "14"
- name: Install dependencies
run: cd backend && npm i
- name: Test
run: cd backend && npm test
- name: Build
run: cd backend && npm run build
- name: Commit changes
if: contains(github.event.head_commit.message, '#build')
run: |
git config user.email github-actions
git config user.name github-actions@github.com
git add .
git commit -m "Build sub-store.min.js" -a
git push
npm install -g pnpm
cd backend && pnpm i
- name: Test
run: |
cd backend
pnpm test
- name: Build
run: |
cd backend
pnpm run build
- id: tag
name: Generate release tag
run: |
cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "::set-output name=release_tag::$SUBSTORE_RELEASE"
- name: Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.tag.outputs.release_tag }}
files: |
./backend/sub-store.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store-parser.loon.min.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.8.6",
"version": "2.12.4",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {

View File

@@ -7,3 +7,5 @@ export const RULES_KEY = 'rules';
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';
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour

View File

@@ -50,6 +50,7 @@ function parse(raw) {
lastParser = parser;
success = true;
$.info(`${parser.name} is activated`);
break;
}
}
}
@@ -84,7 +85,7 @@ async function process(proxies, operators = [], targetPlatform) {
// if this is a remote script, download it
try {
script = await download(url.split('#')[0]);
$.info(`Script loaded: >>>\n ${script}`);
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,

View File

@@ -112,8 +112,11 @@ function URI_SSR() {
line = line.split('/?')[1].split('&');
if (line.length > 1) {
for (const item of line) {
const [key, val] = item.split('=');
other_params[key] = val.trim();
let [key, val] = item.split('=');
val = val.trim();
if (val.length > 0) {
other_params[key] = val;
}
}
}
proxy = {
@@ -242,14 +245,17 @@ function URI_Trojan() {
const name = decodeURIComponent(line.split('#')[1].trim());
let paramArr = line.split('?');
let scert = null;
let params;
const params = new Map();
if (paramArr.length > 1) {
paramArr = paramArr[1].split('#')[0].split('&');
params = new Map(
paramArr.map((item) => {
return item.split('=');
}),
);
for (const pair of paramArr) {
let [key, val] = pair.split('=');
// skip empty values
val = val.trim();
if (val.length > 0) {
params.set(key, val);
}
}
if (
params.get('allowInsecure') === '1' ||
params.get('allowInsecure') === 'true'
@@ -281,7 +287,32 @@ function Clash_All() {
}
return true;
};
const parse = (line) => JSON.parse(line);
const parse = (line) => {
const proxy = JSON.parse(line);
if (
![
'ss',
'ssr',
'vmess',
'socks',
'http',
'snell',
'trojan',
].includes(proxy.type)
) {
throw new Error(
`Clash does not support proxy with type: ${proxy.type}`,
);
}
// handle vmess sni
if (proxy.type === 'vmess') {
proxy.sni = proxy.servername;
delete proxy.servername;
}
return proxy;
};
return { name, test, parse };
}

View File

@@ -56,7 +56,7 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_ho
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "auto";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
@@ -112,10 +112,9 @@ port = digits:[0-9]+ {
}
method = comma cipher:cipher {
if (cipher !== 'none') proxy.cipher = cipher;
else proxy.cipher = 'auto';
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
username = & {
let j = peg$currPos;

View File

@@ -54,7 +54,7 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_ho
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "auto";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
@@ -110,10 +110,9 @@ port = digits:[0-9]+ {
}
method = comma cipher:cipher {
if (cipher !== 'none') proxy.cipher = cipher;
else proxy.cipher = 'auto';
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
username = & {
let j = peg$currPos;

View File

@@ -82,7 +82,7 @@ shadowsocks = "shadowsocks" equals address
vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "auto";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
proxy.alterId = 0;
} else {
@@ -140,8 +140,7 @@ password = comma "password" equals password:[^=,]+ { proxy.password = password.j
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); }
method = comma "method" equals cipher:cipher {
if (cipher !== 'none') proxy.cipher = cipher;
else proxy.cipher = 'auto';
proxy.cipher = cipher;
};
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }

View File

@@ -80,7 +80,7 @@ shadowsocks = "shadowsocks" equals address
vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "auto";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
proxy.alterId = 0;
} else {
@@ -138,8 +138,7 @@ password = comma "password" equals password:[^=,]+ { proxy.password = password.j
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); }
method = comma "method" equals cipher:cipher {
if (cipher !== 'none') proxy.cipher = cipher;
else proxy.cipher = 'auto';
proxy.cipher = cipher;
};
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }

View File

@@ -45,7 +45,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "auto";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
proxy.alterId = 0;
} else {
@@ -152,8 +152,7 @@ vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join("");
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
if (cipher !== 'none') proxy.cipher = cipher;
else proxy.cipher = 'auto';
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");

View File

@@ -43,7 +43,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "auto";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
proxy.alterId = 0;
} else {
@@ -150,8 +150,7 @@ vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join("");
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
if (cipher !== 'none') proxy.cipher = cipher;
else proxy.cipher = 'auto';
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");

View File

@@ -1,12 +1,14 @@
import resourceCache from '@/utils/resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo';
import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
operator: "AND",
child: [
{
@@ -21,7 +23,7 @@ The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed a
}
]
}
*/
*/
function ConditionalFilter({ rule }) {
return {
@@ -310,6 +312,9 @@ function ScriptOperator(script, targetPlatform, $arguments) {
const DOMAIN_RESOLVERS = {
Google: async function (domain) {
const id = hex_md5(`GOOGLE:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
@@ -326,9 +331,14 @@ const DOMAIN_RESOLVERS = {
if (answers.length === 0) {
throw new Error('No answers');
}
return answers[answers.length - 1].data;
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain) {
const id = hex_md5(`IP-API:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://ip-api.com/json/${encodeURIComponent(
domain,
@@ -338,9 +348,14 @@ const DOMAIN_RESOLVERS = {
if (body['status'] !== 'success') {
throw new Error(`Status is ${body['status']}`);
}
return body.query;
const result = body.query;
resourceCache.set(id, result);
return result;
},
Cloudflare: async function (domain) {
const id = hex_md5(`CLOUDFLARE:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain,
@@ -357,7 +372,9 @@ const DOMAIN_RESOLVERS = {
if (answers.length === 0) {
throw new Error('No answers');
}
return answers[answers.length - 1].data;
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
};

View File

@@ -1,16 +1,33 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
proxies.filter((proxy) => {
if (proxy.type === 'vless') return false;
return true;
});
// filter unsupported proxies
proxies = proxies.filter((proxy) =>
['ss', 'ssr', 'vmess', 'socks', 'http', 'snell', 'trojan'].includes(
proxy.type,
),
);
return (
'proxies:\n' +
proxies
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
delete proxy['tls-fingerprint'];
delete proxy['aead'];
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')

View File

@@ -72,7 +72,7 @@ function shadowsocksr(proxy) {
// obfs
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
result.appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param');
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');

View File

@@ -1,4 +1,5 @@
import { isPresent, Result } from './utils';
const targetPlatform = 'QX';
export default function QX_Producer() {
@@ -180,7 +181,16 @@ function vmess(proxy) {
const appendIfPresent = result.appendIfPresent.bind(result);
append(`vmess=${proxy.server}:${proxy.port}`);
append(`,method=${proxy.cipher === 'auto' ? 'none' : proxy.cipher}`);
// cipher
let cipher;
if (proxy.cipher === 'auto') {
cipher = 'chacha20-ietf-poly1305';
} else {
cipher = proxy.cipher;
}
append(`,method=${cipher}`);
append(`,password=${proxy.uuid}`);
// obfs

View File

@@ -1,3 +1,5 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Stash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
@@ -5,8 +7,21 @@ export default function Stash_Producer() {
'proxies:\n' +
proxies
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
delete proxy['tls-fingerprint'];
delete proxy['aead'];
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')

View File

@@ -1,16 +1,29 @@
import { syncToGist, produceArtifact } from '@/restful/artifacts';
import { version } from '../../package.json';
import { ARTIFACTS_KEY } from '@/constants';
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
import $ from '@/core/app';
console.log(
`
!(async function () {
const settings = $.read(SETTINGS_KEY);
// if GitHub token is not configured
if (!settings.githubUser || !settings.gistToken) return;
const artifacts = $.read(ARTIFACTS_KEY);
if (!artifacts || artifacts.length === 0) return;
const shouldSync = artifacts.some((artifact) => artifact.sync);
if (shouldSync) await doSync();
})().finally(() => $.done());
async function doSync() {
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Sub-Store -- v${version}
Sub-Store Sync -- v${version}
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
!(async function () {
);
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {};
@@ -46,9 +59,9 @@ console.log(
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.notify('🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆', '全部订阅同步成功!');
$.notify('🌍 Sub-Store', '全部订阅同步成功!');
} catch (err) {
$.notify('🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆', '同步订阅失败', `原因:${err}`);
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`);
$.error(`无法同步订阅配置到 Gist原因${err}`);
}
})().finally(() => $.done());
}

View File

@@ -282,6 +282,9 @@ async function syncToGist(files) {
async function produceArtifact({ type, name, platform }) {
platform = platform || 'JSON';
// produce Clash node format for ShadowRocket
if (platform === 'ShadowRocket') platform = 'Clash';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
@@ -304,7 +307,7 @@ async function produceArtifact({ type, name, platform }) {
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆',
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
@@ -385,7 +388,7 @@ async function produceArtifact({ type, name, platform }) {
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆',
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{

View File

@@ -1,5 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { COLLECTIONS_KEY } from '@/constants';
import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
@@ -67,6 +67,21 @@ function updateCollection(req, res) {
...collection,
};
$.info(`正在更新组合订阅:${name}...`);
if (name !== newCol.name) {
// update all artifacts referring this collection
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'collection' &&
artifact.source === oldCol.name
) {
artifact.source = newCol.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allCols, name, newCol);
$.write(allCols, COLLECTIONS_KEY);
success(res, newCol);

View File

@@ -48,7 +48,7 @@ async function downloadSubscription(req, res) {
}
} catch (err) {
$.notify(
`🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 下载订阅失败`,
`🌍 Sub-Store 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
);
@@ -63,7 +63,7 @@ async function downloadSubscription(req, res) {
);
}
} else {
$.notify(`🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 下载订阅失败`, `❌ 未找到订阅:${name}`);
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -117,7 +117,7 @@ async function downloadCollection(req, res) {
}
} catch (err) {
$.notify(
`🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 下载组合订阅失败`,
`🌍 Sub-Store 下载组合订阅失败`,
`❌ 下载组合订阅错误:${name}`,
`🤔 原因:${err}`,
);
@@ -132,7 +132,7 @@ async function downloadCollection(req, res) {
}
} else {
$.notify(
`🌍 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 下载组合订阅失败`,
`🌍 Sub-Store 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
failed(

View File

@@ -14,11 +14,20 @@ import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings';
import registerSettingRoutes, {
updateArtifactStore,
updateGitHubAvatar,
} from './settings';
import registerPreviewRoutes from './preview';
import registerSortingRoutes from './sort';
import { failed, success } from '@/restful/response';
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import {
InternalServerError,
NetworkError,
RequestInvalidError,
} from '@/restful/errors';
import resourceCache from '@/utils/resource-cache';
import producer from '@/core/proxy-utils/producers';
export default function serve() {
const $app = express({ substore: $ });
@@ -33,9 +42,10 @@ export default function serve() {
registerArtifactRoutes($app);
// utils
$app.get('/api/utils/IP_API/:server', IP_API); // IP-API reverse proxy
$app.post('/api/utils/node-info', getNodeInfo);
$app.get('/api/utils/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions
$app.get('/api/utils/refresh', refresh);
// Storage management
$app.route('/api/storage')
@@ -84,6 +94,16 @@ function getEnv(req, res) {
});
}
async function refresh(_, res) {
// 1. get GitHub avatar and artifact store
await updateGitHubAvatar();
await updateArtifactStore();
// 2. clear resource cache
resourceCache.revokeAll();
success(res);
}
async function gistBackup(req, res) {
const { action } = req.query;
// read token
@@ -153,11 +173,50 @@ async function gistBackup(req, res) {
}
}
async function IP_API(req, res) {
const server = decodeURIComponent(req.params.server);
const $http = HTTP();
const result = await $http
.get(`http://ip-api.com/json/${server}?lang=zh-CN`)
.then((resp) => JSON.parse(resp.body));
res.json(result);
async function getNodeInfo(req, res) {
const proxy = req.body;
const lang = req.query.lang || 'zh-CN';
let shareUrl;
try {
shareUrl = producer.URI.produce(proxy);
} catch (err) {
// do nothing
}
try {
const $http = HTTP();
const info = await $http
.get({
url: `http://ip-api.com/json/${encodeURIComponent(
proxy.server,
)}?lang=${lang}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',
},
})
.then((resp) => {
const data = JSON.parse(resp.body);
if (data.status !== 'success') {
throw new Error(data.message);
}
// remove unnecessary fields
delete data.status;
return data;
});
success(res, {
shareUrl,
info,
});
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_GET_NODE_INFO',
`Failed to get node info`,
`Reason: ${err}`,
),
);
}
}

View File

@@ -28,7 +28,7 @@ async function updateSettings(req, res) {
success(res, newSettings);
}
async function updateGitHubAvatar() {
export async function updateGitHubAvatar() {
const settings = $.read(SETTINGS_KEY);
const username = settings.githubUser;
if (username) {
@@ -50,8 +50,8 @@ async function updateGitHubAvatar() {
}
}
async function updateArtifactStore() {
console.log('Updating artifact store');
export async function updateArtifactStore() {
$.log('Updating artifact store');
const settings = $.read(SETTINGS_KEY);
const { githubUser, gistToken } = settings;
if (githubUser && gistToken) {

View File

@@ -5,7 +5,7 @@ import {
RequestInvalidError,
} from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY } from '@/constants';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders } from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
@@ -137,14 +137,28 @@ function updateSubscription(req, res) {
$.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);
// update all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY) || [];
for (const collection of allCols) {
const idx = collection.subscriptions.indexOf(name);
if (idx !== -1) {
collection.subscriptions[idx] = sub.name;
}
}
// update all artifacts referring this subscription
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'subscription' &&
artifact.source == name
) {
artifact.source = sub.name;
}
}
$.write(allCols, COLLECTIONS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY);

View File

@@ -251,7 +251,7 @@ function createTestCases() {
server,
port,
uuid,
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
alterId: 0,
},
},
@@ -288,7 +288,7 @@ function createTestCases() {
server,
port,
uuid,
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
alterId: 0,
aead: true,
},
@@ -339,7 +339,7 @@ function createTestCases() {
server,
port,
uuid,
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
network: 'ws',
'ws-opts': {
path: obfs_path,
@@ -402,7 +402,7 @@ function createTestCases() {
server,
port,
uuid,
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
network: 'ws',
'ws-opts': {
path: obfs_path,

View File

@@ -1,12 +1,14 @@
import { HTTP } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache';
const cache = new Map();
const tasks = new Map();
export default async function download(url, ua) {
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = ua + url;
if (cache.has(id)) {
return cache.get(id);
const id = hex_md5(ua + url);
if (tasks.has(id)) {
return tasks.get(id);
}
const http = HTTP({
@@ -16,18 +18,27 @@ export default async function download(url, ua) {
});
const result = new Promise((resolve, reject) => {
http.get(url)
.then((resp) => {
const body = resp.body;
if (body.replace(/\s/g, '').length === 0)
reject(new Error('订阅内容为空!'));
else resolve(body);
})
.catch(() => {
reject(new Error(`无法下载 URL${url}`));
});
// try to find in app cache
const cached = resourceCache.get(id);
if (cached) {
resolve(cached);
} else {
http.get(url)
.then((resp) => {
const body = resp.body;
if (body.replace(/\s/g, '').length === 0)
reject(new Error('远程资源内容为空!'));
else {
resourceCache.set(id, body);
resolve(body);
}
})
.catch(() => {
reject(new Error(`无法下载 URL${url}`));
});
}
});
cache.set(id, result);
tasks.set(id, result);
return result;
}

View File

@@ -81,7 +81,8 @@ function doMigrationV2() {
useless: 'DEFAULT',
},
};
processes.forEach((p) => {
for (const p of processes) {
if (!p.type) continue;
if (p.type === 'Useless Filter') {
quickSettingOperator.args.useless = 'ENABLED';
} else if (p.type === 'Set Property Operator') {
@@ -120,7 +121,7 @@ function doMigrationV2() {
} else {
newProcesses.push(p);
}
});
}
newProcesses.unshift(quickSettingOperator);
item.process = newProcesses;
}

View File

@@ -14,7 +14,7 @@ export function getPlatformFromHeaders(headers) {
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'Clash';
return 'ShadowRocket';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else {

View File

@@ -0,0 +1,55 @@
import $ from '@/core/app';
import { CACHE_EXPIRATION_TIME_MS, RESOURCE_CACHE_KEY } from '@/constants';
class ResourceCache {
constructor(expires) {
this.expires = expires;
if (!$.read(RESOURCE_CACHE_KEY)) {
$.write('{}', RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
this._cleanup();
}
_cleanup() {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (new Date().getTime() - updated > this.expires) {
$.delete(`#${id}`);
delete this.resourceCache[id];
clear = true;
}
});
if (clear) this._persist();
}
revokeAll() {
Object.keys(this.resourceCache).forEach((id) => {
$.delete(`#${id}`);
});
this.resourceCache = {};
this._persist();
}
_persist() {
$.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
}
get(id) {
const updated = this.resourceCache[id];
if (updated && new Date().getTime() - updated <= this.expires) {
return $.read(`#${id}`);
}
return null;
}
set(id, value) {
this.resourceCache[id] = new Date().getTime();
this._persist();
$.write(value, `#${id}`);
}
}
export default new ResourceCache(CACHE_EXPIRATION_TIME_MS);

387
backend/src/vendor/md5.js vendored Normal file
View File

@@ -0,0 +1,387 @@
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ''; /* base-64 pad character. "=" for strict RFC compliance */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
export function hex_md5(s) {
return rstr2hex(rstr_md5(str2rstr_utf8(s)));
}
export function b64_md5(s) {
return rstr2b64(rstr_md5(str2rstr_utf8(s)));
}
export function any_md5(s, e) {
return rstr2any(rstr_md5(str2rstr_utf8(s)), e);
}
export function hex_hmac_md5(k, d) {
return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
}
export function b64_hmac_md5(k, d) {
return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
}
export function any_hmac_md5(k, d, e) {
return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e);
}
/*
* Perform a simple self-test to see if the VM is working
*/
function md5_vm_test() {
return hex_md5('abc').toLowerCase() == '900150983cd24fb0d6963f7d28e17f72';
}
/*
* Calculate the MD5 of a raw string
*/
function rstr_md5(s) {
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
}
/*
* Calculate the HMAC-MD5, of a key and some data (raw strings)
*/
function rstr_hmac_md5(key, data) {
var bkey = rstr2binl(key);
if (bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
var ipad = Array(16),
opad = Array(16);
for (var i = 0; i < 16; i++) {
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5c5c5c5c;
}
var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
}
/*
* Convert a raw string to a hex string
*/
function rstr2hex(input) {
try {
hexcase;
} catch (e) {
hexcase = 0;
}
var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef';
var output = '';
var x;
for (var i = 0; i < input.length; i++) {
x = input.charCodeAt(i);
output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f);
}
return output;
}
/*
* Convert a raw string to a base-64 string
*/
function rstr2b64(input) {
try {
b64pad;
} catch (e) {
b64pad = '';
}
var tab =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var output = '';
var len = input.length;
for (var i = 0; i < len; i += 3) {
var triplet =
(input.charCodeAt(i) << 16) |
(i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) |
(i + 2 < len ? input.charCodeAt(i + 2) : 0);
for (var j = 0; j < 4; j++) {
if (i * 8 + j * 6 > input.length * 8) output += b64pad;
else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f);
}
}
return output;
}
/*
* Convert a raw string to an arbitrary string encoding
*/
function rstr2any(input, encoding) {
var divisor = encoding.length;
var i, j, q, x, quotient;
/* Convert to an array of 16-bit big-endian values, forming the dividend */
var dividend = Array(Math.ceil(input.length / 2));
for (i = 0; i < dividend.length; i++) {
dividend[i] =
(input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
}
/*
* Repeatedly perform a long division. The binary array forms the dividend,
* the length of the encoding is the divisor. Once computed, the quotient
* forms the dividend for the next step. All remainders are stored for later
* use.
*/
var full_length = Math.ceil(
(input.length * 8) / (Math.log(encoding.length) / Math.log(2)),
);
var remainders = Array(full_length);
for (j = 0; j < full_length; j++) {
quotient = Array();
x = 0;
for (i = 0; i < dividend.length; i++) {
x = (x << 16) + dividend[i];
q = Math.floor(x / divisor);
x -= q * divisor;
if (quotient.length > 0 || q > 0) quotient[quotient.length] = q;
}
remainders[j] = x;
dividend = quotient;
}
/* Convert the remainders to the output string */
var output = '';
for (i = remainders.length - 1; i >= 0; i--)
output += encoding.charAt(remainders[i]);
return output;
}
/*
* Encode a string as utf-8.
* For efficiency, this assumes the input is valid utf-16.
*/
function str2rstr_utf8(input) {
var output = '';
var i = -1;
var x, y;
while (++i < input.length) {
/* Decode utf-16 surrogate pairs */
x = input.charCodeAt(i);
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) {
x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff);
i++;
}
/* Encode output as utf-8 */
if (x <= 0x7f) output += String.fromCharCode(x);
else if (x <= 0x7ff)
output += String.fromCharCode(
0xc0 | ((x >>> 6) & 0x1f),
0x80 | (x & 0x3f),
);
else if (x <= 0xffff)
output += String.fromCharCode(
0xe0 | ((x >>> 12) & 0x0f),
0x80 | ((x >>> 6) & 0x3f),
0x80 | (x & 0x3f),
);
else if (x <= 0x1fffff)
output += String.fromCharCode(
0xf0 | ((x >>> 18) & 0x07),
0x80 | ((x >>> 12) & 0x3f),
0x80 | ((x >>> 6) & 0x3f),
0x80 | (x & 0x3f),
);
}
return output;
}
/*
* Encode a string as utf-16
*/
function str2rstr_utf16le(input) {
var output = '';
for (var i = 0; i < input.length; i++)
output += String.fromCharCode(
input.charCodeAt(i) & 0xff,
(input.charCodeAt(i) >>> 8) & 0xff,
);
return output;
}
function str2rstr_utf16be(input) {
var output = '';
for (var i = 0; i < input.length; i++)
output += String.fromCharCode(
(input.charCodeAt(i) >>> 8) & 0xff,
input.charCodeAt(i) & 0xff,
);
return output;
}
/*
* Convert a raw string to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*/
function rstr2binl(input) {
var output = Array(input.length >> 2);
for (var i = 0; i < output.length; i++) output[i] = 0;
for (var i = 0; i < input.length * 8; i += 8)
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
return output;
}
/*
* Convert an array of little-endian words to a string
*/
function binl2rstr(input) {
var output = '';
for (var i = 0; i < input.length * 32; i += 8)
output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
return output;
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length.
*/
function binl_md5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << len % 32;
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for (var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t) {
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
}
function md5_ff(a, b, c, d, x, s, t) {
return md5_cmn((b & c) | (~b & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t) {
return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t) {
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t) {
return md5_cmn(c ^ (b | ~d), a, b, x, s, t);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y) {
var lsw = (x & 0xffff) + (y & 0xffff);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}

View File

@@ -51,24 +51,20 @@ export class OpenAPI {
// create a json for root cache
let fpath = 'root.json';
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(
fpath,
JSON.stringify({}),
{ flag: 'wx' },
(err) => console.log(err),
);
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx',
});
this.root = {};
} else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
}
this.root = {};
// create a json file with the given name if not exists
fpath = `${this.name}.json`;
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(
fpath,
JSON.stringify({}),
{ flag: 'wx' },
(err) => console.log(err),
);
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx',
});
this.cache = {};
} else {
this.cache = JSON.parse(

File diff suppressed because one or more lines are too long

View File

@@ -9,4 +9,6 @@
hostname=sub.store
[Script]
http-request https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/sub-store.min.js, requires-body=true, timeout=120, tag=Sub-Store
http-request https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js, requires-body=true, timeout=120, tag=Sub-Store
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync

View File

@@ -1,3 +1,3 @@
hostname=sub.store
^https?:\/\/sub\.store url script-analyze-echo-response https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/sub-store.min.js
^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js

View File

@@ -10,7 +10,18 @@ http:
type: request
require-body: true
timeout: 120
cron:
script:
- name: cron-sync-artifacts
cron: "0 0 * * *"
timeout: 120
script-providers:
sub-store:
url: https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/sub-store.min.js
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js
interval: 86400
cron-sync-artifacts:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
interval: 86400

View File

@@ -4,4 +4,6 @@
hostname=%APPEND% sub.store
[Script]
Sub-Store = type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/sub-store.min.js,requires-body=true,timeout=120,max-size=131072
Sub-Store = type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js,requires-body=true,timeout=120,max-size=131072
Sub-Store Sync = type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -1,11 +1,5 @@
const $ = API("sub-store");
$.write({}, "subs");
$.write({}, "collections");
$.write({}, "artifacts");
const $ = API()
$.write("{}", "#sub-store")
$.done()
$.done();
// prettier-ignore
/*********************************** API *************************************/
function ENV(){const e="undefined"!=typeof $task,t="undefined"!=typeof $loon,s="undefined"!=typeof $httpClient&&!t,o="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:"function"==typeof require&&!o,isJSBox:o,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e,t={}){const{isQX:s,isLoon:o,isSurge:i,isScriptable:n,isNode:r}=ENV();const u={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(c=>u[c.toLowerCase()]=(u=>(function(u,c){(c="string"==typeof c?{url:c}:c).url=e?e+c.url:c.url;const h=(c={...t,...c}).timeout,l={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...c.events};let a,d;if(l.onRequest(u,c),s)a=$task.fetch({method:u,...c});else if(o||i||r)a=new Promise((e,t)=>{(r?require("request"):$httpClient)[u.toLowerCase()](c,(s,o,i)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:i})})});else if(n){const e=new Request(c.url);e.method=u,e.headers=c.headers,e.body=c.body,a=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}const f=h?new Promise((e,t)=>{d=setTimeout(()=>(l.onTimeout(),t(`${u} URL: ${c.url} exceeds the timeout ${h} ms`)),h)}):null;return(f?Promise.race([f,a]).then(e=>(clearTimeout(d),e)):a).then(e=>l.onResponse(e))})(c,u))),u}function API(e="untitled",t=!1){const{isQX:s,isLoon:o,isSurge:i,isNode:n,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(n){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(o||i)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),n){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache);s&&$prefs.setValueForKey(e,this.name),(o||i)&&$persistentStore.write(e,this.name),n&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root),{flag:"w"},e=>console.log(e)))}write(e,t){this.log(`SET ${t}`),-1!==t.indexOf("#")?(t=t.substr(1),i&o&&$persistentStore.write(e,t),s&&$prefs.setValueForKey(e,t),n&&(this.root[t]=e)):this.cache[t]=e,this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),i&o?$persistentStore.read(e):s?$prefs.valueForKey(e):n?this.root[e]:void 0)}delete(e){this.log(`DELETE ${e}`),-1!==e.indexOf("#")?(e=e.substr(1),i&o&&$persistentStore.write(null,e),s&&$prefs.removeValueForKey(e),n&&delete this.root[e]):delete this.cache[e],this.persistCache()}notify(e,t="",c="",h={}){const l=h["open-url"],a=h["media-url"];if(s&&$notify(e,t,c,h),i&&$notification.post(e,t,c+`${a?"\n多媒体:"+a:""}`,{url:l}),o){let s={};l&&(s.openUrl=l),a&&(s.mediaUrl=a),"{}"==JSON.stringify(s)?$notification.post(e,t,c):$notification.post(e,t,c,s)}if(n||u){const s=c+(l?`\n点击跳转: ${l}`:"")+(a?`\n多媒体: ${a}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(e)}info(e){console.log(e)}error(e){console.log("ERROR: "+e)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||i?$done(e):n&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}}(e,t)}
/*****************************************************************************/
function ENV(){const e="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:"undefined"!=typeof $task,isLoon:"undefined"!=typeof $loon,isSurge:"undefined"!=typeof $httpClient&&"undefined"!=typeof $utils,isBrowser:"undefined"!=typeof document,isNode:"function"==typeof require&&!e,isJSBox:e,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:o,isScriptable:n,isNode:i,isBrowser:r}=ENV(),u=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;const a={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(h=>a[h.toLowerCase()]=(a=>(function(a,h){h="string"==typeof h?{url:h}:h;const d=e.baseURL;d&&!u.test(h.url||"")&&(h.url=d?d+h.url:h.url),h.body&&h.headers&&!h.headers["Content-Type"]&&(h.headers["Content-Type"]="application/x-www-form-urlencoded");const l=(h={...e,...h}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...h.events};let f,p;if(c.onRequest(a,h),t)f=$task.fetch({method:a,...h});else if(s||o||i)f=new Promise((e,t)=>{(i?require("request"):$httpClient)[a.toLowerCase()](h,(s,o,n)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:n})})});else if(n){const e=new Request(h.url);e.method=a,e.headers=h.headers,e.body=h.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}else r&&(f=new Promise((e,t)=>{fetch(h.url,{method:a,headers:h.headers,body:h.body}).then(e=>e.json()).then(t=>e({statusCode:t.status,headers:t.headers,body:t.data})).catch(t)}));const y=l?new Promise((e,t)=>{p=setTimeout(()=>(c.onTimeout(),t(`${a} URL: ${h.url} exceeds the timeout ${l} ms`)),l)}):null;return(y?Promise.race([y,f]).then(e=>(clearTimeout(p),e)):f).then(e=>c.onResponse(e))})(h,a))),a}function API(e="untitled",t=!1){const{isQX:s,isLoon:o,isSurge:n,isNode:i,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(i){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(o||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),i){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(o||n)&&$persistentStore.write(e,this.name),i&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||o)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);i&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||o?$persistentStore.read(e):s?$prefs.valueForKey(e):i?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||o)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);i&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",a="",h={}){const d=h["open-url"],l=h["media-url"];if(s&&$notify(e,t,a,h),n&&$notification.post(e,t,a+`${l?"\n多媒体:"+l:""}`,{url:d}),o){let s={};d&&(s.openUrl=d),l&&(s.mediaUrl=l),"{}"===JSON.stringify(s)?$notification.post(e,t,a):$notification.post(e,t,a,s)}if(i||u){const s=a+(d?`\n点击跳转: ${d}`:"")+(l?`\n多媒体: ${l}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||n?$done(e):i&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)}