mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-09-14 12:24:20 +08:00
296 lines
9.7 KiB
JavaScript
296 lines
9.7 KiB
JavaScript
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
'use strict';
|
||
'require baseclass';
|
||
|
||
function utf8Base64(value) {
|
||
const bytes = new TextEncoder().encode(String(value));
|
||
let binary = '';
|
||
for (let i = 0; i < bytes.length; i++)
|
||
binary += String.fromCharCode(bytes[i]);
|
||
return btoa(binary);
|
||
}
|
||
|
||
function utf8FromBase64(value) {
|
||
const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
|
||
const bytes = Uint8Array.from(binary, function(ch) { return ch.charCodeAt(0); });
|
||
return new TextDecoder('utf-8').decode(bytes);
|
||
}
|
||
|
||
function base64Url(value) {
|
||
return utf8Base64(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||
}
|
||
|
||
function endpoint(server, port) {
|
||
const host = String(server).indexOf(':') >= 0 && String(server)[0] !== '['
|
||
? '[' + server + ']'
|
||
: server;
|
||
return host + ':' + port;
|
||
}
|
||
|
||
function requireFields(node, fields) {
|
||
for (let i = 0; i < fields.length; i++) {
|
||
const key = fields[i];
|
||
if (node[key] === undefined || node[key] === null || node[key] === '')
|
||
throw new Error('Missing required field: ' + key);
|
||
}
|
||
}
|
||
|
||
function setIf(params, key, value) {
|
||
if (value !== undefined && value !== null && value !== '')
|
||
params.set(key, String(value));
|
||
}
|
||
|
||
function arrayValue(value) {
|
||
return Array.isArray(value) ? value.join(',') : value;
|
||
}
|
||
|
||
function addTransport(params, node) {
|
||
const network = node.network || 'tcp';
|
||
params.set('type', network);
|
||
|
||
if (network === 'ws') {
|
||
const opts = node['ws-opts'] || {};
|
||
setIf(params, 'path', opts.path);
|
||
setIf(params, 'host', opts.headers && (opts.headers.Host || opts.headers.host));
|
||
} else if (network === 'grpc') {
|
||
const opts = node['grpc-opts'] || {};
|
||
setIf(params, 'serviceName', opts['grpc-service-name'] || opts.serviceName);
|
||
} else if (network === 'http' || network === 'h2') {
|
||
const opts = node['h2-opts'] || node['http-opts'] || {};
|
||
setIf(params, 'path', Array.isArray(opts.path) ? opts.path[0] : opts.path);
|
||
setIf(params, 'host', arrayValue(opts.host));
|
||
}
|
||
}
|
||
|
||
function makeResult(node, link) {
|
||
return {
|
||
ok: true,
|
||
name: String(node.name || 'Node'),
|
||
type: String(node.type || '').toLowerCase(),
|
||
link: link,
|
||
error: ''
|
||
};
|
||
}
|
||
|
||
function convertSs(node) {
|
||
requireFields(node, [ 'server', 'port', 'cipher', 'password' ]);
|
||
const auth = base64Url(node.cipher + ':' + node.password);
|
||
let query = '';
|
||
if (node.plugin) {
|
||
const fields = [ String(node.plugin) ];
|
||
const opts = node['plugin-opts'] || {};
|
||
Object.keys(opts).forEach(function(key) {
|
||
const value = opts[key];
|
||
if (value === true)
|
||
fields.push(key);
|
||
else if (value !== false && value !== undefined && value !== null && value !== '')
|
||
fields.push(key + '=' + value);
|
||
});
|
||
const params = new URLSearchParams();
|
||
params.set('plugin', fields.join(';'));
|
||
query = '?' + params.toString();
|
||
}
|
||
return 'ss://' + auth + '@' + endpoint(node.server, node.port) + query + '#' + encodeURIComponent(node.name || 'Node');
|
||
}
|
||
|
||
function convertSsr(node) {
|
||
requireFields(node, [ 'server', 'port', 'cipher', 'password', 'protocol', 'obfs' ]);
|
||
const query = [];
|
||
query.push('remarks=' + base64Url(node.name || 'Node'));
|
||
if (node['protocol-param'])
|
||
query.push('protoparam=' + base64Url(node['protocol-param']));
|
||
if (node['obfs-param'])
|
||
query.push('obfsparam=' + base64Url(node['obfs-param']));
|
||
|
||
const payload = [
|
||
node.server,
|
||
node.port,
|
||
node.protocol,
|
||
node.cipher,
|
||
node.obfs,
|
||
base64Url(node.password)
|
||
].join(':') + '/?' + query.join('&');
|
||
return 'ssr://' + base64Url(payload);
|
||
}
|
||
|
||
function convertVmess(node) {
|
||
requireFields(node, [ 'server', 'port', 'uuid' ]);
|
||
const network = node.network || 'tcp';
|
||
const ws = node['ws-opts'] || {};
|
||
const grpc = node['grpc-opts'] || {};
|
||
const h2 = node['h2-opts'] || {};
|
||
const payload = {
|
||
v: '2',
|
||
ps: String(node.name || 'Node'),
|
||
add: String(node.server),
|
||
port: String(node.port),
|
||
id: String(node.uuid),
|
||
aid: Number(node.alterId || node.alter_id || 0),
|
||
scy: String(node.cipher || 'auto'),
|
||
net: network,
|
||
type: 'none',
|
||
host: String((ws.headers && (ws.headers.Host || ws.headers.host)) || arrayValue(h2.host) || ''),
|
||
path: String(ws.path || grpc['grpc-service-name'] || (Array.isArray(h2.path) ? h2.path[0] : h2.path) || ''),
|
||
tls: node.tls ? 'tls' : 'none',
|
||
sni: String(node.servername || node.sni || ''),
|
||
alpn: String(arrayValue(node.alpn) || ''),
|
||
fp: String(node.fingerprint || node['client-fingerprint'] || '')
|
||
};
|
||
return 'vmess://' + utf8Base64(JSON.stringify(payload));
|
||
}
|
||
|
||
function convertVless(node) {
|
||
requireFields(node, [ 'server', 'port', 'uuid' ]);
|
||
const params = new URLSearchParams();
|
||
addTransport(params, node);
|
||
params.set('encryption', String(node.encryption || 'none'));
|
||
setIf(params, 'flow', node.flow);
|
||
|
||
const reality = node['reality-opts'];
|
||
if (reality || node.tls) {
|
||
params.set('security', reality ? 'reality' : 'tls');
|
||
setIf(params, 'sni', node.servername || node.sni);
|
||
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
|
||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||
if (node['skip-cert-verify'])
|
||
params.set('allowInsecure', '1');
|
||
if (reality) {
|
||
setIf(params, 'pbk', reality['public-key']);
|
||
setIf(params, 'sid', reality['short-id']);
|
||
setIf(params, 'spx', reality['spider-x']);
|
||
}
|
||
}
|
||
|
||
return 'vless://' + encodeURIComponent(node.uuid) + '@' + endpoint(node.server, node.port) +
|
||
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
|
||
}
|
||
|
||
function convertTrojan(node) {
|
||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||
const params = new URLSearchParams();
|
||
addTransport(params, node);
|
||
setIf(params, 'sni', node.sni || node.servername);
|
||
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
|
||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||
if (node['skip-cert-verify'])
|
||
params.set('allowInsecure', '1');
|
||
return 'trojan://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
|
||
}
|
||
|
||
function convertTuic(node) {
|
||
requireFields(node, [ 'server', 'port', 'uuid', 'password' ]);
|
||
const params = new URLSearchParams();
|
||
setIf(params, 'sni', node.sni);
|
||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||
setIf(params, 'congestion_control', node['congestion-controller'] || node['congestion-control']);
|
||
setIf(params, 'udp_relay_mode', node['udp-relay-mode']);
|
||
if (node['skip-cert-verify'])
|
||
params.set('allow_insecure', '1');
|
||
return 'tuic://' + encodeURIComponent(node.uuid) + ':' + encodeURIComponent(node.password) + '@' +
|
||
endpoint(node.server, node.port) + (params.toString() ? '?' + params.toString() : '') +
|
||
'#' + encodeURIComponent(node.name || 'Node');
|
||
}
|
||
|
||
function convertHysteria2(node) {
|
||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||
const params = new URLSearchParams();
|
||
setIf(params, 'sni', node.sni);
|
||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||
setIf(params, 'obfs', node.obfs);
|
||
setIf(params, 'obfs-password', node['obfs-password']);
|
||
if (node['skip-cert-verify'])
|
||
params.set('insecure', '1');
|
||
return 'hysteria2://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
|
||
}
|
||
|
||
function convertAnytls(node) {
|
||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||
const params = new URLSearchParams();
|
||
setIf(params, 'sni', node.sni || node.servername);
|
||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||
setIf(params, 'client-fingerprint', node['client-fingerprint'] || node.fingerprint);
|
||
if (node['skip-cert-verify'])
|
||
params.set('insecure', '1');
|
||
if (node.udp)
|
||
params.set('udp', '1');
|
||
return 'anytls://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
|
||
}
|
||
|
||
function isMetadataProxy(node) {
|
||
const name = String(node && node.name || '').trim();
|
||
if (!name)
|
||
return false;
|
||
|
||
return /(?:官网|QQ|流量|续费|应急|重置|到期|过期|剩余|套餐)/i.test(name) ||
|
||
/^(?:traffic|bandwidth|expire|expiry|expiration|subscription(?:\s+info)?|official\s+website|website)\s*[::|]/i.test(name) ||
|
||
/^(?:(?:剩余|可用|已用|总)?流量|套餐到期|到期时间|到期日|过期时间|有效期|订阅信息)\s*[::|]/i.test(name) ||
|
||
/^(?:官方网站|官网地址|官方网址|网站地址)\s*[::|]/i.test(name) ||
|
||
/^(?:加入|联系|客服)?\s*QQ\s*(?:群|交流群|客服|联系)\s*[::|]/i.test(name);
|
||
}
|
||
|
||
function convertProxy(node) {
|
||
const type = String(node && node.type || '').toLowerCase();
|
||
const converters = {
|
||
ss: convertSs,
|
||
ssr: convertSsr,
|
||
vmess: convertVmess,
|
||
vless: convertVless,
|
||
trojan: convertTrojan,
|
||
tuic: convertTuic,
|
||
hysteria2: convertHysteria2,
|
||
hy2: convertHysteria2,
|
||
anytls: convertAnytls
|
||
};
|
||
|
||
try {
|
||
if (!node || typeof node !== 'object')
|
||
throw new Error('Node must be an object');
|
||
if (!converters[type])
|
||
throw new Error('Unsupported protocol: ' + (type || 'unknown'));
|
||
return makeResult(node, converters[type](node));
|
||
} catch (e) {
|
||
return {
|
||
ok: false,
|
||
name: String(node && node.name || 'Node'),
|
||
type: type || 'unknown',
|
||
link: '',
|
||
error: String(e && e.message || e)
|
||
};
|
||
}
|
||
}
|
||
|
||
function convertProxies(proxies) {
|
||
return Array.isArray(proxies) ? proxies.map(convertProxy) : [];
|
||
}
|
||
|
||
function normalizeLink(link) {
|
||
const value = String(link || '').trim();
|
||
if (value.indexOf('vmess://') === 0) {
|
||
try {
|
||
const payload = JSON.parse(utf8FromBase64(value.slice(8)));
|
||
delete payload.ps;
|
||
return 'vmess://' + utf8Base64(JSON.stringify(payload));
|
||
} catch (e) {}
|
||
}
|
||
return value.replace(/#.*$/, '');
|
||
}
|
||
|
||
const api = {
|
||
convertProxy: convertProxy,
|
||
convertProxies: convertProxies,
|
||
isMetadataProxy: isMetadataProxy,
|
||
normalizeLink: normalizeLink
|
||
};
|
||
|
||
if (typeof module !== 'undefined' && module.exports)
|
||
module.exports = api;
|
||
|
||
if (typeof baseclass !== 'undefined')
|
||
return baseclass.extend(api);
|
||
|
||
return api;
|