Compare commits

...

26 Commits

Author SHA1 Message Date
xream
32eb069ab2 feat: GEO 增加 JNB, SJC, SEL 2024-04-02 21:06:56 +08:00
xream
4c9f8011c7 fix: 修复内蒙古识别为蒙古的问题 2024-04-02 20:56:07 +08:00
xream
bd26b0a561 feat: 区域过滤增加韩国德国 2024-04-02 20:40:13 +08:00
xream
958d1e52c8 chore: 日志输出增加订阅的来源 User-Agent 2024-03-31 10:59:49 +08:00
xream
e7a2e60963 feat: sing-box 订阅格式修改(如需原始格式 请使用 target=sing-box&produceType=internal); 清理 Clash 系无效字段 2024-03-31 09:36:32 +08:00
xream
fa6a274f79 feat: ProxyUtils 增加 getFlag, getISO 方法 2024-03-31 08:20:51 +08:00
xream
e40b3f88d5 chore: geo 增加关键词 德意志 2024-03-31 06:59:31 +08:00
xream
163ad9ee09 feat: JSON 输出支持 produceType internal 2024-03-31 04:45:57 +08:00
xream
abb6f2dec1 feat: 处理 sni off 的情况. 若出现问题, 麻烦大家及时反馈 2024-03-30 01:12:34 +08:00
xream
56870bbd5f fix: 修复组合订阅子订阅失败导致预览失败 2024-03-26 14:59:24 +08:00
xream
efbc6ecd84 feat: 流量单位显示由 EB 提升到 YB 2024-03-25 03:50:47 +08:00
xream
c27c589024 chore: 调整部分日志 2024-03-25 02:47:06 +08:00
xream
0efed4f1a0 feat: 处理传入 httpClient 的 timeout 参数 2024-03-24 07:28:16 +08:00
xream
e3a514d1fb feat: hysteria2 支持 mport, clash.meta(mihomo) 支持 ports 2024-03-21 20:07:24 +08:00
xream
64478c7a27 feat: 刷新时 清除所有缓存 2024-03-21 02:37:21 +08:00
xream
dc8f19f350 doc: demo.js 2024-03-21 01:52:42 +08:00
xream
b4ccfc7e07 chore: Sub-Store Simple 脚本增加脚本超时(默认 120) 可能会影响某些逻辑 待观察 2024-03-19 23:48:26 +08:00
xream
3f1940630a chore: 增加 gist 错误日志 2024-03-19 21:30:52 +08:00
xream
5a0bdb1276 doc: demo.js 2024-03-18 20:24:18 +08:00
xream
a1b86e26a2 chore: 增加上传同步配置的详细日志 2024-03-16 02:47:01 +08:00
xream
6ec8c29f6a feat: 规则中处理 GEOIP/GEOSITE, Loon 已支持 SRC-PORT/DEST-PORT/PROTOCOL 2024-03-15 08:49:36 +08:00
xream
bbb9602f9f release: backend version 2.14.256 2024-03-15 08:12:06 +08:00
xream
6db6153672 Merge pull request #295 from makaspacex/master
[Feat]规则转换增加对GEOIP与GEOSITE的支持
2024-03-15 08:10:30 +08:00
makabaka
b66189948a 规则转换增加对GEOIP与GEOSITE的支持 2024-03-14 22:07:45 +08:00
xream
2611dccc73 feat: 支持设置查询远程订阅流量信息时的 User-Agent 2024-03-14 19:45:39 +08:00
xream
25d3cf6ca4 feat: 通过代理/节点/策略获取订阅 现已支持 Surge, Loon, Stash, Shadowrocket, QX, Node.js 2024-03-14 01:54:07 +08:00
32 changed files with 406 additions and 195 deletions

View File

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

View File

@@ -15,6 +15,7 @@ import $ from '@/core/app';
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import { getFlag, getISO } from '@/utils/geo';
function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) {
@@ -159,7 +160,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
continue;
}
$.info(
$.log(
`Applying "${item.type}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || 'None'
}`,
@@ -186,6 +187,10 @@ function produce(proxies, targetPlatform, type, opts = {}) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
}
const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
targetPlatform,
);
// filter unsupported proxies
proxies = proxies.filter(
(proxy) =>
@@ -196,10 +201,22 @@ function produce(proxies, targetPlatform, type, opts = {}) {
if (!isNotBlank(proxy.name)) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
if (proxy['disable-sni']) {
if (sni_off_supported) {
proxy.sni = 'off';
} else if (!['tuic'].includes(proxy.type)) {
$.error(
`Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`,
);
proxy.sni = '';
proxy['skip-cert-verify'] = true;
delete proxy['tls-fingerprint'];
}
}
return proxy;
});
$.info(`Producing proxies for target: ${targetPlatform}`);
$.log(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
const list = proxies
@@ -242,6 +259,8 @@ export const ProxyUtils = {
isIPv6,
isIP,
yaml: YAML,
getFlag,
getISO,
};
function tryParse(parser, line) {
@@ -396,6 +415,9 @@ function lastParse(proxy) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
}
if (['', 'off'].includes(proxy.sni)) {
proxy['disable-sni'] = true;
}
return proxy;
}

View File

@@ -547,6 +547,7 @@ function URI_Hysteria2() {
proxy.obfs = params.obfs;
}
proxy.ports = params.mport;
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);

View File

@@ -177,7 +177,13 @@ username = & {
password = comma match:[^,]+ { proxy.password = match.join(""); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:domain { proxy.sni = sni; }
sni = comma "sni" equals sni:("off"/domain) {
if (sni === "off") {
proxy["disable-sni"] = true;
} else {
proxy.sni = sni;
}
}
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }

View File

@@ -175,7 +175,13 @@ username = & {
password = comma match:[^,]+ { proxy.password = match.join(""); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:domain { proxy.sni = sni; }
sni = comma "sni" equals sni:("off"/domain) {
if (sni === "off") {
proxy["disable-sni"] = true;
} else {
proxy.sni = sni;
}
}
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }

View File

@@ -617,6 +617,8 @@ function RegionFilter(regions) {
SG: '🇸🇬',
JP: '🇯🇵',
UK: '🇬🇧',
DE: '🇩🇪',
KR: '🇰🇷',
};
return {
name: 'Region Filter',

View File

@@ -150,6 +150,13 @@ export default function Clash_Producer() {
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null) {
delete proxy[key];
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]

View File

@@ -165,6 +165,13 @@ export default function ClashMeta_Producer() {
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null) {
delete proxy[key];
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]

View File

@@ -13,7 +13,8 @@ import singbox_Producer from './sing-box';
function JSON_Producer() {
const type = 'ALL';
const produce = (proxies) => JSON.stringify(proxies, null, 2);
const produce = (proxies, type) =>
type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);
return { type, produce };
}

View File

@@ -168,6 +168,13 @@ export default function ShadowRocket_Producer() {
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null) {
delete proxy[key];
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]

View File

@@ -789,7 +789,9 @@ export default function singbox_Producer() {
$.error(e.message ?? e);
}
});
return type === 'internal' ? list : JSON.stringify(list, null, 2);
return type === 'internal'
? list
: JSON.stringify({ outbounds: list }, null, 2);
};
return { type, produce };
}

View File

@@ -257,6 +257,13 @@ export default function Stash_Producer() {
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null) {
delete proxy[key];
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]

View File

@@ -8,6 +8,13 @@ export default function URI_Producer() {
let result = '';
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null) {
delete proxy[key];
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
delete proxy.tls;
}
@@ -274,6 +281,9 @@ export default function URI_Producer() {
`sni=${encodeURIComponent(proxy.sni)}`,
);
}
if (proxy.ports) {
hysteria2params.push(`mport=${proxy.ports}`);
}
if (proxy['tls-fingerprint']) {
hysteria2params.push(
`pinSHA256=${encodeURIComponent(

View File

@@ -10,6 +10,8 @@ const RULE_TYPES_MAPPING = [
[/^PROTOCOL$/, 'PROTOCOL'],
[/^IP-CIDR$/i, 'IP-CIDR'],
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
[/^GEOIP$/i, 'GEOIP'],
[/^GEOSITE$/i, 'GEOSITE'],
];
function AllRuleParser() {
@@ -37,8 +39,7 @@ function AllRuleParser() {
content: params[1],
};
if (
rule.type === 'IP-CIDR' ||
rule.type === 'IP-CIDR6'
['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)
) {
rule.options = params.slice(2);
}

View File

@@ -10,6 +10,8 @@ function QXFilter() {
'SRC-IP',
'IN-PORT',
'PROTOCOL',
'GEOSITE',
'GEOIP',
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
@@ -29,6 +31,8 @@ function QXFilter() {
function SurgeRuleSet() {
const type = 'SINGLE';
const func = (rule) => {
const UNSUPPORTED = ['GEOSITE', 'GEOIP'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
let output = `${rule.type},${rule.content}`;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
output +=
@@ -43,7 +47,7 @@ function LoonRules() {
const type = 'SINGLE';
const func = (rule) => {
// skip unsupported rules
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
// Loon only supports the no-resolve option
@@ -69,7 +73,7 @@ function ClashRuleProvider() {
let output = `${TRANSFORM[rule.type] || rule.type},${
rule.content
}`;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
if (rule.options) {
// Clash only supports the no-resolve option
rule.options = rule.options.filter((option) =>

View File

@@ -128,10 +128,19 @@ async function doSync() {
files.map((item) => [item.path, item]),
);
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
const raw_url =
files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
}
}

View File

@@ -1,4 +1,7 @@
import { getPlatformFromHeaders } from '@/utils/platform';
import {
getUserAgentFromHeaders,
getPlatformFromUserAgent,
} from '@/utils/user-agent';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
@@ -16,10 +19,15 @@ async function downloadSubscription(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const userAgent = getUserAgentFromHeaders(req.headers);
const platform =
req.query.target || getPlatformFromUserAgent(userAgent) || 'JSON';
$.info(
`正在下载订阅:${name}\ntarget: ${platform}\n来源 User-Agent: ${userAgent.UA}`,
);
$.info(`正在下载订阅:${name}`);
let {
url,
ua,
@@ -79,8 +87,7 @@ async function downloadSubscription(req, res) {
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ||
url
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
url = `${url || sub.url}`
@@ -113,7 +120,7 @@ async function downloadSubscription(req, res) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
undefined,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
@@ -173,13 +180,17 @@ async function downloadCollection(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const userAgent = getUserAgentFromHeaders(req.headers);
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
req.query.target || getPlatformFromUserAgent(userAgent) || 'JSON';
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
$.info(`正在下载组合订阅:${name}`);
$.info(
`正在下载组合订阅:${name}\ntarget: ${platform}\n来源 User-Agent: ${userAgent.UA}`,
);
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } =
req.query;
@@ -250,7 +261,7 @@ async function downloadCollection(req, res) {
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
undefined,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);

View File

@@ -3,6 +3,8 @@ import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
import { updateArtifactStore, updateAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import {
GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY,
@@ -73,6 +75,8 @@ async function refresh(_, res) {
// 2. clear resource cache
resourceCache.revokeAll();
scriptResourceCache.revokeAll();
headersResourceCache.revokeAll();
success(res);
}
@@ -153,11 +157,14 @@ async function gistBackup(req, res) {
}
success(res);
} catch (err) {
$.error(
`Failed to ${action} gist data.\nReason: ${err.message ?? err}`,
);
failed(
res,
new InternalServerError(
'BACKUP_FAILED',
`Failed to ${action} data to gist!`,
`Failed to ${action} gist data!`,
`Reason: ${err.message ?? err}`,
),
);

View File

@@ -253,11 +253,7 @@ async function compareCollection(req, res) {
errors[name] = err;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
`❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name}时出现错误:${err}`,
);
}
}),

View File

@@ -115,7 +115,7 @@ async function getFlowInfo(req, res) {
} else {
const flowHeaders = await getFlowHeaders(
url,
undefined,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);

View File

@@ -563,10 +563,19 @@ async function syncArtifacts() {
files.map((item) => [item.path, item]),
);
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
const raw_url =
files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
}
}
@@ -660,10 +669,18 @@ async function syncArtifact(req, res) {
isGitLab = true;
files = Object.fromEntries(files.map((item) => [item.path, item]));
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {

View File

@@ -53,7 +53,7 @@ export default async function download(rawUrl, ua, timeout, proxy) {
// return item.content;
// }
const { isNode, isStash } = ENV();
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
@@ -66,8 +66,10 @@ export default async function download(rawUrl, ua, timeout, proxy) {
const http = HTTP({
headers: {
'User-Agent': userAgent,
'X-Stash-Selected-Proxy':
isStash && proxy ? encodeURIComponent(proxy) : undefined,
...(isStash && proxy
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
: {}),
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
},
timeout: requestTimeout,
});
@@ -86,8 +88,10 @@ export default async function download(rawUrl, ua, timeout, proxy) {
try {
const { body, headers } = await http.get({
url,
proxy,
...getPolicyDescriptor(proxy),
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
if (headers) {
@@ -125,7 +129,12 @@ export default async function download(rawUrl, ua, timeout, proxy) {
if ($arguments?.validCheck) {
await validCheck(
parseFlowHeaders(
await getFlowHeaders(url, undefined, undefined, proxy),
await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy,
),
),
);
}

View File

@@ -34,7 +34,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
if ($arguments?.noFlow) {
return;
}
const { isStash } = ENV();
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url);
let flowInfo;
if (!$arguments?.noCache && cached) {
@@ -49,7 +49,11 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
try {
// $.info(`使用 HEAD 方法获取流量信息: ${url}`);
$.info(
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
@@ -57,23 +61,36 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
'X-Stash-Selected-Proxy':
isStash && proxy
? encodeURIComponent(proxy)
: undefined,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
proxy,
...getPolicyDescriptor(proxy),
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法获取流量信息失败: ${url}: ${e.message ?? e}`,
`使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
);
}
if (!flowInfo) {
$.info(`使用 GET 方法获取流量信息: ${url}`);
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
@@ -121,10 +138,10 @@ export function parseFlowHeaders(flowHeaders) {
return { expires, total, usage: { upload, download } };
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024
return flow < 1024 || unitIndex === unitList.length - 1
? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]);
}

View File

@@ -1,3 +1,105 @@
const ISOFlags = {
'🏳️‍🌈': ['EXP', 'BAND'],
'🇸🇱': ['TEST', 'SOS'],
'🇦🇩': ['AD', 'AND'],
'🇦🇪': ['AE', 'ARE'],
'🇦🇫': ['AF', 'AFG'],
'🇦🇱': ['AL', 'ALB'],
'🇦🇲': ['AM', 'ARM'],
'🇦🇷': ['AR', 'ARG'],
'🇦🇹': ['AT', 'AUT'],
'🇦🇺': ['AU', 'AUS'],
'🇦🇿': ['AZ', 'AZE'],
'🇧🇦': ['BA', 'BIH'],
'🇧🇩': ['BD', 'BGD'],
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
'🇨🇱': ['CL', 'CHL'],
'🇨🇴': ['CO', 'COL'],
'🇨🇷': ['CR', 'CRI'],
'🇨🇾': ['CY', 'CYP'],
'🇨🇿': ['CZ', 'CZE'],
'🇩🇪': ['DE', 'DEU'],
'🇩🇰': ['DK', 'DNK'],
'🇪🇨': ['EC', 'ECU'],
'🇪🇪': ['EE', 'EST'],
'🇪🇬': ['EG', 'EGY'],
'🇪🇸': ['ES', 'ESP'],
'🇪🇺': ['EU'],
'🇫🇮': ['FI', 'FIN'],
'🇫🇷': ['FR', 'FRA'],
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],
'🇰🇵': ['KP', 'PRK'],
'🇰🇷': ['KR', 'KOR', 'SEL'],
'🇰🇿': ['KZ', 'KAZ'],
'🇮🇩': ['ID', 'IDN'],
'🇮🇪': ['IE', 'IRL'],
'🇮🇱': ['IL', 'ISR'],
'🇮🇲': ['IM', 'IMN'],
'🇮🇳': ['IN', 'IND'],
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
'🇲🇹': ['MT', 'MLT'],
'🇲🇽': ['MX', 'MEX'],
'🇲🇾': ['MY', 'MYS'],
'🇳🇱': ['NL', 'NLD'],
'🇳🇴': ['NO', 'NOR'],
'🇳🇵': ['NP', 'NPL'],
'🇳🇿': ['NZ', 'NZL'],
'🇵🇦': ['PA', 'PAN'],
'🇵🇪': ['PE', 'PER'],
'🇵🇭': ['PH', 'PHL'],
'🇵🇰': ['PK', 'PAK'],
'🇵🇱': ['PL', 'POL'],
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
'🇷🇺': ['RU', 'RUS'],
'🇸🇦': ['SA', 'SAU'],
'🇸🇪': ['SE', 'SWE'],
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'],
'🇺🇾': ['UY', 'URY'],
'🇻🇪': ['VE', 'VEN'],
'🇻🇳': ['VN', 'VNM'],
'🇿🇦': ['ZA', 'ZAF', 'JNB'],
'🇨🇳': ['CN', 'CHN', 'BACK'],
};
// get proxy flag according to its name
export function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
@@ -65,6 +167,7 @@ export function getFlag(name) {
'广德',
'法兰克福',
'Frankfurt',
'德意志',
],
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
'🇪🇨': ['Ecuador', '厄瓜多尔'],
@@ -283,108 +386,6 @@ export function getFlag(name) {
],
};
const ISOFlags = {
'🏳️‍🌈': ['EXP', 'BAND'],
'🇸🇱': ['TEST', 'SOS'],
'🇦🇩': ['AD', 'AND'],
'🇦🇪': ['AE', 'ARE'],
'🇦🇫': ['AF', 'AFG'],
'🇦🇱': ['AL', 'ALB'],
'🇦🇲': ['AM', 'ARM'],
'🇦🇷': ['AR', 'ARG'],
'🇦🇹': ['AT', 'AUT'],
'🇦🇺': ['AU', 'AUS'],
'🇦🇿': ['AZ', 'AZE'],
'🇧🇦': ['BA', 'BIH'],
'🇧🇩': ['BD', 'BGD'],
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
'🇨🇱': ['CL', 'CHL'],
'🇨🇴': ['CO', 'COL'],
'🇨🇷': ['CR', 'CRI'],
'🇨🇾': ['CY', 'CYP'],
'🇨🇿': ['CZ', 'CZE'],
'🇩🇪': ['DE', 'DEU'],
'🇩🇰': ['DK', 'DNK'],
'🇪🇨': ['EC', 'ECU'],
'🇪🇪': ['EE', 'EST'],
'🇪🇬': ['EG', 'EGY'],
'🇪🇸': ['ES', 'ESP'],
'🇪🇺': ['EU'],
'🇫🇮': ['FI', 'FIN'],
'🇫🇷': ['FR', 'FRA'],
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],
'🇰🇵': ['KP', 'PRK'],
'🇰🇷': ['KR', 'KOR'],
'🇰🇿': ['KZ', 'KAZ'],
'🇮🇩': ['ID', 'IDN'],
'🇮🇪': ['IE', 'IRL'],
'🇮🇱': ['IL', 'ISR'],
'🇮🇲': ['IM', 'IMN'],
'🇮🇳': ['IN', 'IND'],
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
'🇲🇹': ['MT', 'MLT'],
'🇲🇽': ['MX', 'MEX'],
'🇲🇾': ['MY', 'MYS'],
'🇳🇱': ['NL', 'NLD'],
'🇳🇴': ['NO', 'NOR'],
'🇳🇵': ['NP', 'NPL'],
'🇳🇿': ['NZ', 'NZL'],
'🇵🇦': ['PA', 'PAN'],
'🇵🇪': ['PE', 'PER'],
'🇵🇭': ['PH', 'PHL'],
'🇵🇰': ['PK', 'PAK'],
'🇵🇱': ['PL', 'POL'],
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
'🇷🇺': ['RU', 'RUS'],
'🇸🇦': ['SA', 'SAU'],
'🇸🇪': ['SE', 'SWE'],
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
'🇺🇾': ['UY', 'URY'],
'🇻🇪': ['VE', 'VEN'],
'🇻🇳': ['VN', 'VNM'],
'🇿🇦': ['ZA', 'ZAF'],
'🇨🇳': ['CN', 'CHN', 'BACK'],
};
// 原旗帜或空
let Flag =
name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
@@ -398,7 +399,9 @@ export function getFlag(name) {
// 不精确匹配(只要包含就算,忽略大小写)
keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
) {
//console.log(`newFlag = ${flag}`)
if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {
return (Flag = '🇨🇳');
}
return (Flag = flag);
}
}
@@ -416,6 +419,11 @@ export function getFlag(name) {
return (Flag = flag);
}
}
//console.log(`Final Flag = ${Flag}`)
return Flag;
}
export function getISO(name) {
return ISOFlags[getFlag(name)]?.[0];
}

View File

@@ -1,4 +1,4 @@
export function getPlatformFromHeaders(headers) {
export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
@@ -9,6 +9,9 @@ export function getPlatformFromHeaders(headers) {
break;
}
}
return { UA, ua };
}
export function getPlatformFromUserAgent({ ua, UA }) {
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (UA.indexOf('Surfboard') !== -1) {
@@ -38,3 +41,7 @@ export function getPlatformFromHeaders(headers) {
return 'JSON';
}
}
export function getPlatformFromHeaders(headers) {
const { UA, ua } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA });
}

View File

@@ -316,42 +316,53 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
url: options.url,
headers: options.headers,
body: options.body,
opts: options.opts,
});
} else if (isLoon || isSurge || isNode) {
worker = new Promise((resolve, reject) => {
const request = isNode
? eval("require('request')")
: $httpClient;
request[method.toLowerCase()](
JSON.parse(JSON.stringify(options)),
(err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
const opts = JSON.parse(JSON.stringify(options));
if (!isNode && opts.timeout) {
opts.timeout++;
let unit = 'ms';
// 这些客户端单位为 s
if (isSurge || isStash || isShadowRocket) {
opts.timeout = Math.ceil(opts.timeout / 1000);
unit = 's';
}
// Loon 为 ms
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
}
request[method.toLowerCase()](opts, (err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
if (err) reject(err);
else
resolve({
statusCode:
response.status || response.statusCode,
headers: response.headers,
body,
});
},
);
if (err) reject(err);
else
resolve({
statusCode: response.status || response.statusCode,
headers: response.headers,
body,
});
});
});
}
let timeoutid;
const timer = timeout
? new Promise((_, reject) => {
// console.log(`[request timeout] ${timeout}ms`);
timeoutid = setTimeout(() => {
events.onTimeout();
return reject(

View File

@@ -17,4 +17,4 @@ hostname=sub.store
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync

View File

@@ -10,6 +10,6 @@ hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability="{{{ability}}}"
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",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

@@ -8,6 +8,6 @@ hostname = %APPEND% sub.store
[Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,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

@@ -7,6 +7,6 @@ hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,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

@@ -7,6 +7,6 @@ hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,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

@@ -22,6 +22,9 @@ function operator(proxies = [], targetPlatform, context) {
// scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache
// cache.set(id, data)
// cache.get(id)
// ProxyUtils 为节点处理工具
// 可参考 https://t.me/zhetengsha/1066
@@ -33,8 +36,20 @@ function operator(proxies = [], targetPlatform, context) {
// isIPv6,
// isIP,
// yaml, // yaml 解析和生成
// getFlag, // 获取 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// }
// 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 从 sni 文件中读取内容并进行节点操作
// const sni = await produceArtifact({
// type: 'file',
// name: 'sni' // 文件名
// });
// $server.sni = sni
// 1. Surge 输出 WireGuard 完整配置
// let proxies = await produceArtifact({
@@ -49,7 +64,10 @@ function operator(proxies = [], targetPlatform, context) {
// 2. sing-box
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
// 但是一般不需要这样用, 可参考
// 1. https://t.me/zhetengsha/1111
// 2. https://t.me/zhetengsha/1070
// 3. https://t.me/zhetengsha/1241
// let singboxProxies = await produceArtifact({
// type: 'subscription', // type: 'subscription' 或 'collection'
@@ -63,24 +81,42 @@ function operator(proxies = [], targetPlatform, context) {
// 3. clash.meta
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
// 但是一般不需要这样用, 可参考
// 1. https://t.me/zhetengsha/1111
// 2. https://t.me/zhetengsha/1070
// 3. https://t.me/zhetengsha/1234
// let clashMetaProxies = await produceArtifact({
// type: 'subscription',
// name: 'sub',
// platform: 'ClashMeta',
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
// }))
// })
// // YAML
// ProxyUtils.yaml.load('YAML String')
// ProxyUtils.yaml.safeLoad('YAML String')
// $content = ProxyUtils.yaml.safeDump({})
// $content = ProxyUtils.yaml.dump({})
// 一个往文件里插入本地节点的例子:
// const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0])
// let clashMetaProxies = await produceArtifact({
// type: 'collection',
// name: '机场',
// platform: 'ClashMeta',
// produceType: 'internal'
// })
// yaml.proxies.unshift(...clashMetaProxies)
// $content = ProxyUtils.yaml.dump(yaml)
// { $content, $files } will be passed to the next operator
// $content is the final content of the file
// flowUtils 为机场订阅流量信息处理工具
// 可参考 https://t.me/zhetengsha/948
// https://github.com/sub-store-org/Sub-Store/blob/31b6dd0507a9286d6ab834ec94ad3050f6bdc86b/backend/src/utils/download.js#L104
// 可参考:
// 1. https://t.me/zhetengsha/948
// context 为传入的上下文
// 有三种情况, 按需判断