mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
511 lines
15 KiB
Plaintext
511 lines
15 KiB
Plaintext
#!/usr/bin/ucode
|
|
|
|
'use strict';
|
|
|
|
import { access, lsdir, lstat, mkstemp, popen, readfile, writefile } from 'fs';
|
|
import {
|
|
shellQuote, isBinary, executeCommand, yqRead, yqReadFile,
|
|
HM_DIR, EXE_DIR, SDL_DIR, RUN_DIR
|
|
} from 'fchomo';
|
|
|
|
function hasKernelModule(kmod) {
|
|
return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellQuote(kmod))) === 0);
|
|
}
|
|
|
|
function wGET(url, header, filepath) {
|
|
if (!url || type(url) !== 'string')
|
|
return null;
|
|
|
|
let ua = 'Wget/1.21 (FullCombo Shark!)';
|
|
|
|
if (header) {
|
|
header = json(trim(header) || '{}');
|
|
|
|
header = join(' ', filter(map(keys(header), (k) => {
|
|
let v = join(', ', type(header[k]) === 'array' ? filter(header[k], v => v) : []);
|
|
if (k === 'User-Agent') {
|
|
ua = v;
|
|
v = null;
|
|
}
|
|
return v ? '--header=' + shellQuote(`${k}: ${v}`) : null;
|
|
}), v => v));
|
|
} else
|
|
header = '';
|
|
|
|
let exitcode = system(`wget --tries=1 --timeout=10 --user-agent ${shellQuote(ua)} ${header} -q ${shellQuote(url)} -O ${shellQuote(filepath)}`);
|
|
|
|
return exitcode;
|
|
}
|
|
|
|
const RES_TYPE = ['certs', 'node', 'provider', 'ruleset', 'resources', 'templates'];
|
|
|
|
const methods = {
|
|
mihomo_generator: {
|
|
args: { type: 'type', params: 'params' },
|
|
call: function(req) {
|
|
/* https://github.com/MetaCubeX/mihomo/blob/Alpha/component/generator/cmd.go */
|
|
if (!(req.args?.type in ['uuid',
|
|
'reality-keypair', 'wg-keypair', 'sudoku-keypair',
|
|
'age-x25519', 'age-mlkem768-x25519', 'age-convert',
|
|
'vless-mlkem768', 'vless-x25519',
|
|
'ech-keypair'])) {
|
|
return { result: false, error: 'illegal type' };
|
|
}
|
|
|
|
const type = req.args?.type;
|
|
let result = {};
|
|
|
|
let cmd = `generate ${type}`;
|
|
switch (type) {
|
|
case 'age-x25519':
|
|
cmd = 'age keygen';
|
|
break;
|
|
case 'age-mlkem768-x25519':
|
|
cmd = 'age keygen-pq';
|
|
break;
|
|
case 'age-convert':
|
|
cmd = 'age convert';
|
|
break;
|
|
}
|
|
|
|
const fd = popen(`/usr/bin/mihomo ${cmd} ${shellQuote(req.args.params ?? '')}`);
|
|
if (fd) {
|
|
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
|
if (type === 'uuid')
|
|
result.uuid = trim(line);
|
|
else if (type in ['reality-keypair', 'wg-keypair', 'sudoku-keypair']) {
|
|
let priv = match(trim(line), /PrivateKey: (.*)/);
|
|
if (priv)
|
|
result.private_key = priv[1];
|
|
let pub = match(trim(line), /PublicKey: (.*)/);
|
|
if (pub)
|
|
result.public_key = pub[1];
|
|
} else if (type in ['age-x25519', 'age-mlkem768-x25519']) {
|
|
let priv = match(trim(line), /AGE-SECRET-KEY-.*/);
|
|
if (priv)
|
|
result.private_key = priv[0];
|
|
let pub = match(trim(line), /# public key: (.*)/);
|
|
if (pub)
|
|
result.public_key = pub[1];
|
|
} else if (type in ['age-convert']) {
|
|
result.public_key = trim(line);
|
|
} else if (type in ['vless-x25519', 'vless-mlkem768']) {
|
|
let priv = match(trim(line), /PrivateKey: (.*)/);
|
|
if (priv)
|
|
result.private_key = priv[1];
|
|
let pass = match(trim(line), /Password: (.*)/);
|
|
if (pass)
|
|
result.password = pass[1];
|
|
let seed = match(trim(line), /Seed: (.*)/);
|
|
if (seed)
|
|
result.seed = seed[1];
|
|
let client = match(trim(line), /Client: (.*)/);
|
|
if (client)
|
|
result.client = client[1];
|
|
let hash = match(trim(line), /Hash32: (.*)/);
|
|
if (hash)
|
|
result.hash32 = hash[1];
|
|
} else if (type in ['ech-keypair']) {
|
|
result.ech_key = result.ech_key ? result.ech_key + '\n' + trim(line) : '';
|
|
let cfg = match(trim(line), /Config: (.*)/);
|
|
if (cfg)
|
|
result.ech_cfg = cfg[1];
|
|
let key = match(trim(line), /Key: (.*)/);
|
|
if (key)
|
|
result.ech_key = key[1];
|
|
}
|
|
}
|
|
|
|
fd.close();
|
|
}
|
|
|
|
return { result };
|
|
}
|
|
},
|
|
|
|
age_crypto: {
|
|
args: { action: 'action', key: 'key', content: 'content'},
|
|
call: function(req) {
|
|
if (!(req.args?.action in ['decrypt', 'encrypt']))
|
|
return { result: false, error: 'illegal action' };
|
|
|
|
if (!req.args?.key)
|
|
return { result: false, error: 'illegal key' };
|
|
|
|
if (!req.args?.content)
|
|
return { result: '' };
|
|
|
|
let content = req.args?.content;
|
|
let infd = mkstemp();
|
|
|
|
if (content) {
|
|
content = trim(content);
|
|
content = replace(content, /\r\n?/g, '\n');
|
|
}
|
|
infd.write(content);
|
|
|
|
infd.seek();
|
|
const out = executeCommand(infd, '/usr/bin/mihomo age', req.args?.action, shellQuote(req.args?.key), '-', '-');
|
|
infd.close();
|
|
|
|
return out.exitcode === 0 ? { result: out.stdout } : { result: false, error: out.stderr };
|
|
}
|
|
},
|
|
|
|
age_crypto_file: {
|
|
args: { action: 'action', key: 'key', input: 'input', output: 'output'},
|
|
call: function(req) {
|
|
if (!(req.args?.action in ['decrypt', 'encrypt']))
|
|
return { result: false, error: 'illegal action' };
|
|
|
|
if (!req.args?.key)
|
|
return { result: false, error: 'illegal key' };
|
|
|
|
if ((!req.args?.input) || match(req.args?.input, /\.\.\//))
|
|
return { result: false, error: 'illegal input' };
|
|
|
|
if ((!req.args?.output) || match(req.args?.output, /\.\.\//))
|
|
return { result: false, error: 'illegal output' };
|
|
|
|
const out = executeCommand(null, '/usr/bin/mihomo age', req.args?.action, shellQuote(req.args?.key), shellQuote(req.args?.input), shellQuote(req.args?.output));
|
|
|
|
return out.exitcode === 0 ? { result: true } : { result: false, error: out.stderr };
|
|
}
|
|
},
|
|
|
|
get_features: {
|
|
call: function() {
|
|
let features = {};
|
|
|
|
const use_apk = system('command -v apk') == 0 || null; // @less_25_12
|
|
|
|
const fd = popen('/usr/bin/mihomo -v');
|
|
if (fd) {
|
|
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
|
let ver = match(trim(line), /Mihomo Meta (.*)/);
|
|
if (ver)
|
|
features.core_version = split(ver[1], ' ')[0];
|
|
let tags = match(trim(line), /Use tags: (.*)/);
|
|
if (tags)
|
|
for (let k in split(tags[1], ','))
|
|
features[k] = true;
|
|
}
|
|
|
|
fd.close();
|
|
}
|
|
|
|
features.app_version = trim(popen(`${use_apk ? 'apk list -I --manifest' : 'opkg list-installed'} luci-app-fchomo | ` +
|
|
`awk '{print $NF}'`).read('all')) || null; // @less_25_12
|
|
|
|
features.has_dnsmasq_full = system(`[ -n "$(${use_apk ? 'apk list -qI' : 'opkg list-installed'} dnsmasq-full)" ]`) == 0 || null; // @less_25_12
|
|
features.has_ip_full = access('/usr/libexec/ip-full');
|
|
features.has_stunclient = access('/usr/bin/stunclient');
|
|
features.has_tcp_brutal = hasKernelModule('brutal.ko');
|
|
features.has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy');
|
|
features.has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun');
|
|
|
|
return features;
|
|
}
|
|
},
|
|
|
|
get_clash_api: {
|
|
args: { instance: 'instance' },
|
|
call: function(req) {
|
|
if (req.args?.instance) {
|
|
const instance = req.args?.instance;
|
|
|
|
let config = json(trim(yqReadFile('-oj', '.[] |= with(select(type == "!!map"); del(.)) |= with(select(type == "!!seq"); del(.))', `${RUN_DIR}/${instance}.yaml`)) || '{}');
|
|
|
|
return {
|
|
http: config['external-controller'],
|
|
https: config['external-controller-tls'],
|
|
doh: config['external-doh-server'],
|
|
secret: config.secret
|
|
};
|
|
} else
|
|
return {}
|
|
}
|
|
},
|
|
|
|
connection_check: {
|
|
args: { url: 'url' },
|
|
call: function(req) {
|
|
if (!req.args?.url)
|
|
return { httpcode: null, error: 'illegal url' };
|
|
|
|
const httpcode = trim(popen(`wget --spider -t1 -ST3 ${shellQuote(req.args?.url)} 2>&1 | awk '/^\\s*HTTP\\//{print $2}'`).read('all')) || '-1';
|
|
|
|
return { httpcode: httpcode };
|
|
}
|
|
},
|
|
|
|
crond_set: {
|
|
args: { type: 'type', expr: 'expr' },
|
|
call: function(req) {
|
|
if (req.args?.type == 'resources') {
|
|
system(`sed -i "/${replace(EXE_DIR, "/", "\\/")}\\/update_resources.sh/d" /etc/crontabs/root`);
|
|
if ((req.args?.expr && match(req.args?.expr, /^[0-9 ,\*\/\?-]+$/)))
|
|
system(`echo -e "` + req.args?.expr + ` ${EXE_DIR}/update_resources.sh ALL" >> /etc/crontabs/root`);
|
|
else
|
|
return { result: false, error: 'illegal expr' };
|
|
} else
|
|
return { result: false, error: 'illegal type' };
|
|
|
|
system(`/etc/init.d/cron restart`);
|
|
return { result: true };
|
|
}
|
|
},
|
|
|
|
log_clean: {
|
|
args: { type: 'type' },
|
|
call: function(req) {
|
|
if (!(req.args?.type in ['fchomo', 'mihomo-c', 'mihomo-s']))
|
|
return { result: false, error: 'illegal type' };
|
|
|
|
const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`);
|
|
if (filestat)
|
|
writefile(`${RUN_DIR}/${req.args?.type}.log`, '');
|
|
return { result: true };
|
|
}
|
|
},
|
|
|
|
resources_get_version: {
|
|
args: { type: 'type', repo: 'repo' },
|
|
call: function(req) {
|
|
const resources = json(trim(readfile(`${HM_DIR}/resources.json`)) || '{}');
|
|
const versions = resources[req.args?.type];
|
|
if (req.args?.repo) {
|
|
for (let obj in values(versions))
|
|
if (obj.repo === req.args?.repo)
|
|
return { version: obj.version };
|
|
|
|
return { version: null };
|
|
} else
|
|
return { version: versions };
|
|
}
|
|
},
|
|
|
|
resources_update: {
|
|
args: { type: 'type', repo: 'repo' },
|
|
call: function(req) {
|
|
if (req.args?.type) {
|
|
const type = shellQuote(req.args?.type),
|
|
repo = shellQuote(req.args?.repo);
|
|
const exit_code = system(`${EXE_DIR}/update_resources.sh ${type} ${repo}`);
|
|
return { status: exit_code };
|
|
} else
|
|
return { status: 255, error: 'illegal type' };
|
|
}
|
|
},
|
|
|
|
json2yaml: {
|
|
args: { content: 'content', command: 'command' },
|
|
call: function(req) {
|
|
if (!req.args?.content)
|
|
return { result: ''};
|
|
|
|
return { result: trim(yqRead('-oy', req.args?.command ? req.args?.command + ' | ... style=""' : '... style=""', req.args?.content)) || '' };
|
|
}
|
|
},
|
|
|
|
yaml2json: {
|
|
args: { content: 'content', command: 'command' },
|
|
call: function(req) {
|
|
if (!req.args?.content)
|
|
return { result: {}};
|
|
|
|
return { result: json(trim(yqRead('-oj', req.args?.command || '.', req.args?.content)) || '{}') };
|
|
}
|
|
},
|
|
|
|
dir_ls: {
|
|
args: { type: 'type' },
|
|
call: function(req) {
|
|
if (!(req.args?.type in RES_TYPE))
|
|
return { result: null, error: 'illegal type' };
|
|
|
|
const list = lsdir(`${HM_DIR}/${req.args?.type}/`);
|
|
return { result: list };
|
|
}
|
|
},
|
|
|
|
file_read: {
|
|
args: { type: 'type', filename: 'filename', isbinary: 'isbinary' },
|
|
call: function(req) {
|
|
if (!(req.args?.type in RES_TYPE))
|
|
return { content: null, error: 'illegal type' };
|
|
|
|
if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//))
|
|
return { content: null, error: 'illegal filename' };
|
|
|
|
let filecontent = readfile(`${HM_DIR}/${req.args?.type}/${req.args?.filename}`);
|
|
|
|
if (req.args?.isbinary == 1)
|
|
filecontent = b64enc(filecontent);
|
|
|
|
return { content: filecontent };
|
|
}
|
|
},
|
|
|
|
file_write: {
|
|
args: { type: 'type', filename: 'filename', content: 'content', isbinary: 'isbinary' },
|
|
call: function(req) {
|
|
if (!(req.args?.type in RES_TYPE))
|
|
return { result: false, error: 'illegal type' };
|
|
|
|
if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//))
|
|
return { result: false, error: 'illegal filename' };
|
|
|
|
const file = `${HM_DIR}/${req.args?.type}/${req.args?.filename}`;
|
|
let content = req.args?.content;
|
|
|
|
/* Sanitize content */
|
|
if (content) {
|
|
content = trim(content);
|
|
content = replace(content, /\r\n?/g, '\n');
|
|
if (!match(content, /\n$/))
|
|
content += '\n';
|
|
}
|
|
|
|
if (req.args?.isbinary == 1)
|
|
content = b64dec(content);
|
|
|
|
//system(`mkdir -p ${HM_DIR}/${req.args?.type}`);
|
|
writefile(file, content);
|
|
|
|
return { result: true };
|
|
}
|
|
},
|
|
|
|
file_download: {
|
|
args: { type: 'type', filename: 'filename', url: 'url', header: 'header' },
|
|
call: function(req) {
|
|
if (!(req.args?.type in RES_TYPE))
|
|
return { result: false, error: 'illegal type' };
|
|
|
|
if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//))
|
|
return { result: false, error: 'illegal filename' };
|
|
|
|
if (!req.args?.url)
|
|
return { result: false, error: 'illegal url' };
|
|
|
|
const file = `${HM_DIR}/${req.args?.type}/${req.args?.filename}`;
|
|
|
|
//system(`mkdir -p ${HM_DIR}/${req.args?.type}`);
|
|
let exitcode = wGET(req.args?.url, req.args?.header, file);
|
|
|
|
if (exitcode === 0) {
|
|
return { result: true };
|
|
} else
|
|
return { result: false, error: 'wget exitcode: ' + sprintf("%d", exitcode) };
|
|
}
|
|
},
|
|
|
|
file_remove: {
|
|
args: { type: 'type', filename: 'filename' },
|
|
call: function(req) {
|
|
if (!(req.args?.type in RES_TYPE))
|
|
return { result: false, error: 'illegal type' };
|
|
|
|
if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//))
|
|
return { result: false, error: 'illegal filename' };
|
|
|
|
system(`rm -rf ${HM_DIR}/${req.args?.type}/${shellQuote(req.args?.filename)}`);
|
|
|
|
return { result: true };
|
|
}
|
|
},
|
|
|
|
/* thanks to homeproxy */
|
|
certificate_write: {
|
|
args: { filename: 'filename' },
|
|
call: function(req) {
|
|
const writeCertificate = function(filename, priv) {
|
|
const tmpcert = '/tmp/fchomo_certificate.tmp';
|
|
const filestat = lstat(tmpcert);
|
|
|
|
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
|
|
system(`rm -f ${tmpcert}`);
|
|
return { result: false, error: 'empty certificate file' };
|
|
}
|
|
|
|
let filecontent = readfile(tmpcert);
|
|
if (isBinary(filecontent)) {
|
|
system(`rm -f ${tmpcert}`);
|
|
return { result: false, error: 'illegal file type: binary' };
|
|
}
|
|
|
|
/* Kanged from luci-proto-openconnect */
|
|
const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
|
|
end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
|
|
lines = split(trim(filecontent), /[\r\n]/);
|
|
let start = false, i;
|
|
|
|
for (i = 0; i < length(lines); i++) {
|
|
if (match(lines[i], beg))
|
|
start = true;
|
|
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
|
|
break;
|
|
}
|
|
|
|
if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
|
|
system(`rm -f ${tmpcert}`);
|
|
return { result: false, error: 'this does not look like a correct PEM file' };
|
|
}
|
|
|
|
/* Sanitize certificate */
|
|
filecontent = trim(filecontent);
|
|
filecontent = replace(filecontent, /\r\n?/g, '\n');
|
|
if (!match(filecontent, /\n$/))
|
|
filecontent += '\n';
|
|
|
|
system(`mkdir -p ${HM_DIR}/certs`);
|
|
writefile(`${HM_DIR}/certs/${filename}.pem`, filecontent);
|
|
system(`rm -f ${tmpcert}`);
|
|
|
|
return { result: true };
|
|
};
|
|
|
|
const filename = req.args?.filename;
|
|
if (!filename || match(filename, /\.\.\//))
|
|
return { result: false, error: 'illegal cerificate filename' };
|
|
switch (filename) {
|
|
case 'client_ca':
|
|
case 'server_publickey':
|
|
case 'client_publickey':
|
|
return writeCertificate(filename, false);
|
|
break;
|
|
case 'server_privatekey':
|
|
case 'client_privatekey':
|
|
return writeCertificate(filename, true);
|
|
break;
|
|
default:
|
|
return { result: false, error: 'illegal cerificate filename' };
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
initialpack_write: {
|
|
call: function(req) {
|
|
const writeResources = function() {
|
|
const tmpcert = '/tmp/fchomo_initialpack.tmp';
|
|
const filestat = lstat(tmpcert);
|
|
|
|
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
|
|
system(`rm -f ${tmpcert}`);
|
|
return { result: false, error: 'empty initialpack file' };
|
|
}
|
|
|
|
system(`tar -C '${HM_DIR}/' -xzf ${tmpcert} `);
|
|
system(`rm -f ${tmpcert}`);
|
|
|
|
return { result: true };
|
|
};
|
|
|
|
return writeResources();
|
|
}
|
|
}
|
|
};
|
|
|
|
return { 'luci.fchomo': methods };
|