update 2026-10-27 07:31:06

This commit is contained in:
action committed 2026-10-27 07:31:06 +08:00
1 parent 197c60d843
commit a4d6470641
4213 files changed
-1575003

No files matched your search

-108
View File
@@ -1,108 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2026 kenzok8
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-honk
PKG_VERSION:=2026.10.25
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
PKG_SOURCE:=doona-4354c99b-e60ed58ef40e.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-honk/releases/download/staging
PKG_SOURCE_SUBDIR:=doona
PKG_HASH:=e60ed58ef40e849269219de38dddf6974f1dbd2915675390d4b748eb7f78db2b
-include $(CURDIR)/generated-doona-stage.mk
PKG_BUILD_DEPENDS:=luci-base/host
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-honk
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI support for Honk
DEPENDS:=+honk +rpcd +luci-base +cgi-io +jsonfilter +libubox
PKGARCH:=all
URL:=https://github.com/kenzok8/openwrt-honk
MAINTAINER:=kenzok8
endef
define Package/luci-app-honk/description
LuCI web interface for managing Honk.
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)/root/usr/share/doona
$(TAR) --strip-components=1 -C $(PKG_BUILD_DIR)/root/usr/share/doona -xzf $(DL_DIR)/$(PKG_SOURCE)
$(SED) 's#^Build patch: .*#Build patch: ci/patches/doona-openwrt.patch#' $(PKG_BUILD_DIR)/root/usr/share/doona/SOURCE
$(INSTALL_DIR) $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona
$(INSTALL_DATA) $(PKG_BUILD_DIR)/root/usr/share/doona/LICENSE $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona/LICENSE
$(INSTALL_DATA) $(PKG_BUILD_DIR)/root/usr/share/doona/NOTICE $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona/NOTICE
$(INSTALL_DATA) $(PKG_BUILD_DIR)/root/usr/share/doona/THIRD-PARTY-NOTICES.txt $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona/THIRD-PARTY-NOTICES.txt
$(INSTALL_DIR) $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona/LICENSES
$(CP) $(PKG_BUILD_DIR)/root/usr/share/doona/LICENSES/* $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona/LICENSES/
po2lmo ./po/zh-cn/honk.po $(PKG_BUILD_DIR)/honk.zh-cn.lmo
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/luci-app-honk/install
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/honk $(1)/usr/libexec/rpcd/honk
$(INSTALL_DIR) $(1)/usr/share/luci-app-honk
$(INSTALL_BIN) ./root/usr/share/luci-app-honk/pkg-info.sh $(1)/usr/share/luci-app-honk/pkg-info.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-honk/refresh-index.sh $(1)/usr/share/luci-app-honk/refresh-index.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-honk/update-pkg.sh $(1)/usr/share/luci-app-honk/update-pkg.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-honk/update-geo.sh $(1)/usr/share/luci-app-honk/update-geo.sh
$(INSTALL_BIN) ./root/usr/share/luci-app-honk/geo-cron.sh $(1)/usr/share/luci-app-honk/geo-cron.sh
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-honk.json $(1)/usr/share/luci/menu.d/luci-app-honk.json
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-honk.json $(1)/usr/share/rpcd/acl.d/luci-app-honk.json
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/honk
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/honk/*.js $(1)/www/luci-static/resources/view/honk/
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/honk/*.css $(1)/www/luci-static/resources/view/honk/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/honk/vendor
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/honk/vendor/* $(1)/www/luci-static/resources/view/honk/vendor/
$(INSTALL_DIR) $(1)/usr/share/doona
$(CP) $(PKG_BUILD_DIR)/root/usr/share/doona/* $(1)/usr/share/doona/
$(INSTALL_DIR) $(1)/usr/share/licenses/honk-doona
$(CP) $(PKG_BUILD_DIR)/root/usr/share/licenses/honk-doona/* $(1)/usr/share/licenses/honk-doona/
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
$(INSTALL_DATA) $(PKG_BUILD_DIR)/honk.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/honk.zh-cn.lmo
endef
define Package/luci-app-honk/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || { \
rm -f /tmp/luci-indexcache.*
rm -rf /tmp/luci-modulecache/
/etc/init.d/rpcd reload 2>/dev/null
/etc/init.d/uhttpd restart 2>/dev/null
exit 0
}
endef
define Package/luci-app-honk/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || { \
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
exit 0
}
endef
$(eval $(call BuildPackage,luci-app-honk))
@@ -1,235 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require rpc';
'require view';
'require view.honk.rpc as honk';
'require view.honk.converter as converter';
const DAE_UPLOAD_PATH = '/tmp/honk-v2-upload/import.dae';
const DAE_LIMIT = 2 * 1024 * 1024;
const PASTE_LIMIT = 2 * 1024 * 1024;
function utf8Length(value) {
return new TextEncoder().encode(value).length;
}
function isHttpUrl(value) {
try {
const url = new URL(value);
return (url.protocol === 'http:' || url.protocol === 'https:') && !!url.hostname;
}
catch (e) {
return false;
}
}
function loadYamlParser() {
if (window.jsyaml && window.jsyaml.load)
return Promise.resolve(window.jsyaml);
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.src = L.resource('view/honk/vendor/js-yaml.min.js');
script.onload = function() {
if (window.jsyaml && window.jsyaml.load)
resolve(window.jsyaml);
else
reject(new Error(_('YAML parser failed to load')));
};
script.onerror = function() { reject(new Error(_('YAML parser failed to load'))); };
document.head.appendChild(script);
});
}
function parsePasted(content) {
let text = String(content || '').trim();
if (!text)
return Promise.resolve({ links: [], rejected: 1 });
if (!converter.looksLikeNodeList(text)) {
const decoded = converter.tryBase64Decode(text);
if (decoded && (converter.looksLikeNodeList(decoded) ||
converter.looksLikeClashYaml(decoded) || converter.looksLikeSurge(decoded)))
text = decoded;
}
if (converter.looksLikeMihomoJson(text))
return Promise.resolve(converter.parseMihomoJson(text));
if (converter.looksLikeClashYaml(text)) {
return loadYamlParser().then(function(yaml) {
const doc = yaml.load(text);
const proxies = (doc && doc.proxies) || [];
const links = [];
let rejected = 0;
proxies.forEach(function(node) {
if (converter.isMetadataProxy(node)) {
rejected++;
return;
}
const result = converter.convertProxy(node);
if (result.ok) links.push(result.link);
else rejected++;
});
return { links: links, rejected: rejected };
});
}
if (converter.looksLikeSurge(text))
return Promise.resolve(converter.parseSurgeProxies(text));
return Promise.resolve(converter.parseUriList(text));
}
function uploadDae(file) {
const form = new FormData();
form.append('sessionid', rpc.getSessionID());
form.append('filename', DAE_UPLOAD_PATH);
form.append('filedata', file, file.name);
return fetch(L.env.cgi_base + '/cgi-upload', {
method: 'POST',
credentials: 'same-origin',
body: form
}).then(function(response) {
return response.json();
}).then(function(result) {
if (!result || result.failure)
throw new Error('upload_failed');
return result;
});
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
render: function() {
honk.installStyles();
const page = E('div', { 'class': 'cbi-map honk-page' });
page.appendChild(E('div', { 'class': 'honk-header' }, [
E('h2', {}, _('Configuration')),
E('p', { 'class': 'honk-header-sub' }, _('Honk reads its nodes, subscriptions and routing from /etc/honk/config.dae.'))
]));
/* --- Add subscription --- */
const subStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
const nameInput = E('input', {
'class': 'cbi-input-text', 'name': 'sub_name',
'placeholder': _('Name, e.g. MyAirport'), 'maxlength': '256'
});
const urlInput = E('input', {
'class': 'cbi-input-text', 'name': 'sub_url',
'placeholder': 'https://…', 'inputmode': 'url'
});
const subBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Add subscription') ]);
subBtn.addEventListener('click', function(ev) {
ev.preventDefault();
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name) { subStatus.textContent = _('Name is required.'); nameInput.focus(); return; }
if (!isHttpUrl(url)) { subStatus.textContent = _('A valid HTTP or HTTPS URL is required.'); urlInput.focus(); return; }
subBtn.disabled = true;
subStatus.textContent = _('Adding subscription…');
honk.addSubscription(name, url).then(honk.ensureOk).then(function(result) {
subStatus.textContent = honk.resultMessage(result, _('Subscription saved with usable nodes.'));
nameInput.value = '';
urlInput.value = '';
}).catch(function(error) {
subStatus.textContent = honk.errorMessage(error, _('Could not add the subscription.'));
}).finally(function() { subBtn.disabled = false; });
});
const subSection = E('section', { 'class': 'honk-card' }, [
E('h3', { 'class': 'honk-card-title' }, _('Add subscription')),
E('p', { 'class': 'honk-note' }, _('Enter a name and subscription URL. Honk checks for usable nodes before saving.')),
E('div', { 'class': 'honk-subscription-add' }, [ nameInput, urlInput, subBtn ]),
subStatus
]);
/* --- Paste nodes --- */
const pasteStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
const pasteInput = E('textarea', {
'class': 'cbi-input-textarea', 'name': 'paste',
'rows': '10', 'spellcheck': 'false',
'placeholder': _('Paste Clash YAML, Surge INI, Base64 or share links.')
});
const pasteBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Import nodes') ]);
pasteBtn.addEventListener('click', function(ev) {
ev.preventDefault();
const content = pasteInput.value.trim();
if (!content) { pasteStatus.textContent = _('Paste nodes or subscription content first.'); pasteInput.focus(); return; }
if (utf8Length(content) > PASTE_LIMIT) { pasteStatus.textContent = _('The pasted content is too large.'); return; }
pasteBtn.disabled = true;
pasteStatus.textContent = _('Parsing nodes…');
parsePasted(content).then(function(parsed) {
const links = (parsed && parsed.links) || [];
if (!links.length) {
pasteStatus.textContent = _('No nodes could be parsed from the input.');
return;
}
pasteStatus.textContent = _('Importing %d node(s)…').format(links.length);
return honk.addNodes(links.join('\n')).then(honk.ensureOk).then(function(result) {
pasteStatus.textContent = honk.resultMessage(result, _('%d node(s) imported.').format(links.length));
pasteInput.value = '';
});
}).catch(function(error) {
pasteStatus.textContent = honk.errorMessage(error, _('Could not import nodes.'));
}).finally(function() { pasteBtn.disabled = false; });
});
const pasteSection = E('section', { 'class': 'honk-card' }, [
E('h3', { 'class': 'honk-card-title' }, _('Paste nodes')),
E('p', { 'class': 'honk-note' }, _('Paste Clash YAML, Surge INI, Base64 or share links; they are parsed locally into nodes and written to /etc/honk/config.dae.')),
pasteInput,
E('div', { 'class': 'honk-actions' }, [ pasteBtn ]),
pasteStatus
]);
/* --- Import dae config --- */
const daeStatus = E('p', { 'class': 'honk-note', 'role': 'status', 'aria-live': 'polite' }, '');
const daeInput = E('input', { 'type': 'file', 'name': 'dae', 'accept': '.dae,text/plain' });
const daeBtn = E('button', { 'class': 'cbi-button cbi-button-apply', 'type': 'button' }, [ _('Import configuration') ]);
daeBtn.addEventListener('click', function(ev) {
ev.preventDefault();
const file = daeInput.files && daeInput.files[0];
if (!file) { daeStatus.textContent = _('Choose a .dae file first.'); return; }
if (file.size > DAE_LIMIT) { daeStatus.textContent = _('The dae file exceeds the 2 MiB limit.'); return; }
daeBtn.disabled = true;
daeStatus.textContent = _('Uploading configuration…');
uploadDae(file).then(function() {
daeStatus.textContent = _('Importing configuration…');
return honk.importDae().then(honk.ensureOk);
}).then(function(result) {
daeStatus.textContent = honk.resultMessage(result, _('Configuration imported and Honk reloaded.'));
daeInput.value = '';
}).catch(function(error) {
daeStatus.textContent = honk.errorMessage(error, _('Could not import the configuration.'));
}).finally(function() { daeBtn.disabled = false; });
});
const daeSection = E('section', { 'class': 'honk-card' }, [
E('h3', { 'class': 'honk-card-title' }, _('Import configuration')),
E('p', { 'class': 'honk-note' }, _('Import a native dae config.dae (e.g. exported from Tower). Its node block is merged into /etc/honk/config.dae.')),
E('div', { 'class': 'honk-subscription-add' }, [ daeInput, daeBtn ]),
daeStatus
]);
/* --- Direct configuration note --- */
const note = E('section', { 'class': 'honk-card' }, [
E('h3', { 'class': 'honk-card-title' }, _('Direct configuration')),
E('p', { 'class': 'honk-note' }, _('After the first valid subscription or node import, the fresh-install policy sends local and Chinese traffic direct and other traffic through the proxy group. You can adjust it in Doona.'))
]);
page.appendChild(subSection);
page.appendChild(pasteSection);
page.appendChild(daeSection);
page.appendChild(note);
return page;
}
});
@@ -1,501 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'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);
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 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 {
ok: true,
name: String(node.name || 'Node'),
type: type,
link: converters[type](node),
error: ''
};
} 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) : [];
}
// --- Paste-import helpers -------------------------------------------------
const NODE_SCHEMES = /^(ss|ssr|vmess|vless|trojan|tuic|hysteria2?|hy2|anytls|socks5|socks4|juicity|snell):\/\//i;
function looksLikeNodeList(text) {
const lines = String(text || '').split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line && NODE_SCHEMES.test(line))
return true;
}
return false;
}
function looksLikeClashYaml(text) {
return /\bproxies\s*:/.test(String(text || ''));
}
function looksLikeSurge(text) {
return /^\s*\[Proxy\]\s*$/m.test(String(text || ''));
}
function tryBase64Decode(text) {
const compact = String(text || '').replace(/\s+/g, '');
if (compact.length < 4 || !/^[A-Za-z0-9+/_-]+={0,2}$/.test(compact))
return '';
let normalized = compact.replace(/-/g, '+').replace(/_/g, '/');
while (normalized.length % 4)
normalized += '=';
try {
return utf8FromBase64(normalized);
} catch (e) {
return '';
}
}
function parseUriList(text) {
const links = [];
let rejected = 0;
String(text || '').split('\n').forEach(function(raw) {
const line = raw.trim();
if (!line || line[0] === '#' || line[0] === ';' || line.indexOf('//') === 0)
return;
if (NODE_SCHEMES.test(line))
links.push(line);
else
rejected++;
});
return { links: links, rejected: rejected };
}
function splitSurgeFields(value) {
const fields = [];
let current = '';
let quote = '';
for (let i = 0; i < value.length; i++) {
const ch = value[i];
if (ch === '"' || ch === '\'') {
if (quote === ch) quote = '';
else if (quote === '') quote = ch;
current += ch;
} else if (ch === ',' && quote === '') {
fields.push(surgeScalar(current));
current = '';
} else {
current += ch;
}
}
fields.push(surgeScalar(current));
return fields;
}
function surgeScalar(value) {
let t = String(value || '').trim();
if (t.length >= 2 && (t[0] === '"' || t[0] === '\'') && t[0] === t[t.length - 1])
t = t.slice(1, -1);
try { return decodeURIComponent(t); } catch (e) { return t; }
}
function firstNonEmpty() {
for (let i = 0; i < arguments.length; i++)
if (arguments[i] !== undefined && arguments[i] !== null && arguments[i] !== '')
return arguments[i];
return '';
}
// Parse a Surge/Shadowrocket [Proxy] section. Only Shadowsocks lines are
// understood, matching the upstream parser; other protocols are rejected so an
// ordinary URL in the file is never mistaken for a node.
function parseSurgeProxies(text) {
const links = [];
let rejected = 0;
let inside = false;
String(text || '').split('\n').forEach(function(raw) {
const line = raw.trim();
if (!line || line[0] === '#' || line[0] === ';' || line.indexOf('//') === 0)
return;
if (line[0] === '[' && line[line.length - 1] === ']' && line.indexOf('=') < 0) {
inside = line.toLowerCase() === '[proxy]';
return;
}
if (!inside)
return;
const sep = line.indexOf('=');
if (sep < 0)
return;
const name = line.slice(0, sep).trim();
const fields = splitSurgeFields(line.slice(sep + 1));
if (!name || fields.length < 3 || fields[0].toLowerCase() !== 'ss' || !fields[1]) {
rejected++;
return;
}
const port = Number(fields[2]);
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
rejected++;
return;
}
const options = {};
for (let i = 3; i < fields.length; i++) {
const eq = fields[i].indexOf('=');
if (eq < 0)
continue;
options[fields[i].slice(0, eq).trim().toLowerCase()] = fields[i].slice(eq + 1).trim();
}
const node = {
name: name,
type: 'ss',
server: fields[1],
port: port,
cipher: firstNonEmpty(options['encrypt-method'], options.method, options.cipher),
password: options.password,
plugin: options.plugin || options['obfs'],
'plugin-opts': {}
};
if (node.plugin) {
node['plugin-opts'].mode = options['obfs'] || 'http';
if (options['obfs-host'])
node['plugin-opts'].host = options['obfs-host'];
}
if (!node.cipher || !node.password) {
rejected++;
return;
}
const result = convertProxy(node);
if (result.ok) links.push(result.link);
else rejected++;
});
if (!links.length && !rejected)
rejected = 1;
return { links: links, rejected: rejected };
}
// Mihomo/mihomo-format JSON keeps nodes in `outbounds` and names them with
// `tag` instead of `name`, with nested `tls`/`transport` objects. Normalize to
// the Clash shape so the existing convertProxy converters work unchanged.
function looksLikeMihomoJson(text) {
const t = String(text || '').trim();
if (!t || t[0] !== '{')
return false;
try {
const doc = JSON.parse(t);
return !!(doc && Array.isArray(doc.outbounds));
}
catch (e) {
return false;
}
}
function normalizeMihomoNode(node) {
const n = Object.assign({}, node);
if (node.tag !== undefined && node.name === undefined) n.name = node.tag;
if (node.server_port !== undefined && node.port === undefined) n.port = node.server_port;
const tls = node.tls;
if (tls && typeof tls === 'object') {
if (tls.server_name !== undefined && n.servername === undefined) n.servername = tls.server_name;
if (tls.insecure && n['skip-cert-verify'] === undefined) n['skip-cert-verify'] = true;
if (Array.isArray(tls.alpn) && n.alpn === undefined) n.alpn = tls.alpn;
if (tls.utls && tls.utls.fingerprint !== undefined && n['client-fingerprint'] === undefined)
n['client-fingerprint'] = tls.utls.fingerprint;
if (tls.reality && typeof tls.reality === 'object') {
if (!n['reality-opts']) n['reality-opts'] = {};
if (tls.reality.public_key !== undefined) n['reality-opts']['public-key'] = tls.reality.public_key;
if (tls.reality.short_id !== undefined) n['reality-opts']['short-id'] = tls.reality.short_id;
}
}
const transport = node.transport;
if (transport && typeof transport === 'object') {
if (transport.type && n.network === undefined) n.network = transport.type;
if (transport.type === 'ws' && transport.path && !n['ws-opts']) {
n['ws-opts'] = { path: transport.path };
if (transport.headers && transport.headers.Host)
n['ws-opts'].headers = { Host: transport.headers.Host };
}
}
return n;
}
function parseMihomoJson(text) {
let doc;
try {
doc = JSON.parse(String(text || ''));
}
catch (e) {
return { links: [], rejected: 1 };
}
const outbounds = (doc && doc.outbounds) || [];
const links = [];
let rejected = 0;
outbounds.forEach(function(node) {
if (isMetadataProxy(node)) {
rejected++;
return;
}
const result = convertProxy(normalizeMihomoNode(node));
if (result.ok) links.push(result.link);
else rejected++;
});
return { links: links, rejected: rejected };
}
return baseclass.extend({
convertProxy: convertProxy,
convertProxies: convertProxies,
isMetadataProxy: isMetadataProxy,
looksLikeNodeList: looksLikeNodeList,
looksLikeClashYaml: looksLikeClashYaml,
looksLikeSurge: looksLikeSurge,
looksLikeMihomoJson: looksLikeMihomoJson,
tryBase64Decode: tryBase64Decode,
parseUriList: parseUriList,
parseSurgeProxies: parseSurgeProxies,
parseMihomoJson: parseMihomoJson,
normalizeMihomoNode: normalizeMihomoNode
});
@@ -1,133 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require view';
'require view.honk.rpc as honk';
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
load: function() {
return honk.status().then(function(status) {
return { status: status };
}).catch(function() {
return { error: true };
});
},
render: function(data) {
honk.installStyles();
const page = E('div', { 'class': 'cbi-map honk-page' });
function renderInitialized(status) {
const section = E('div', { 'class': 'honk-card' });
const webUi = honk.localWebUi(status);
section.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Doona')));
if (webUi) {
section.appendChild(E('a', {
'class': 'cbi-button cbi-button-action',
'href': webUi,
'target': '_blank',
'rel': 'noopener noreferrer'
}, _('Open Doona')));
}
else if (!status.running) {
section.appendChild(E('p', {}, _('Honk is stopped. Start it from the overview to open Doona.')));
section.appendChild(E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin/services/honk/overview')
}, _('Go to overview')));
}
else {
section.appendChild(E('p', {}, _('Doona is not ready yet. Check the overview for status.')));
section.appendChild(E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin/services/honk/overview')
}, _('Go to overview')));
}
page.appendChild(section);
}
if (data.error) {
page.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read Honk status.')));
return page;
}
if (data.status.initialized === true) {
renderInitialized(data.status);
return page;
}
const form = E('form', { 'class': 'honk-card' });
const username = E('input', {
'type': 'text',
'name': 'username',
'autocomplete': 'username',
'class': 'cbi-input-text'
});
const password = E('input', {
'type': 'password',
'name': 'password',
'autocomplete': 'new-password',
'class': 'cbi-input-password'
});
const message = E('p', { 'role': 'status' }, '');
const submit = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Initialize Honk'));
form.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Initial setup')));
form.appendChild(E('p', {}, _('Create the Honk administrator account. The password is sent only for this request and is not saved by this page.')));
form.appendChild(E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title', 'for': 'honk-init-username' }, _('Username')),
E('div', { 'class': 'cbi-value-field' }, username)
]));
username.id = 'honk-init-username';
form.appendChild(E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title', 'for': 'honk-init-password' }, _('Password')),
E('div', { 'class': 'cbi-value-field' }, password)
]));
password.id = 'honk-init-password';
form.appendChild(E('div', { 'class': 'cbi-page-actions' }, submit));
form.appendChild(message);
form.addEventListener('submit', function(ev) {
ev.preventDefault();
const user = username.value.trim();
const secret = password.value;
if (!user || !secret) {
message.textContent = _('Enter both a username and password.');
return;
}
submit.disabled = true;
message.textContent = _('Initializing…');
honk.initialize(user, secret).then(honk.ensureOk).then(function(result) {
username.value = '';
password.value = '';
message.textContent = honk.resultMessage(result, _('Honk initialized.'));
form.remove();
page.appendChild(E('p', { 'role': 'status' }, _('Refreshing status…')));
return honk.status().then(function(status) {
while (page.firstChild)
page.removeChild(page.firstChild);
renderInitialized(status);
}).catch(function() {
while (page.firstChild)
page.removeChild(page.firstChild);
page.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read Honk status.')));
});
}).catch(function() {
password.value = '';
message.textContent = _('Initialization failed. Check the system status.');
}).finally(function() {
submit.disabled = false;
});
});
page.appendChild(form);
return page;
}
});
@@ -1,915 +0,0 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Honk — Apple-style design system.
Cards, pill badges, iOS segmented controls and soft-tinted buttons on a
translucent surface, so the UI adapts to every LuCI theme (light or dark). */
.honk-page {
/* Accent — Apple system blue */
--hk-accent-rgb: 0, 122, 255;
--hk-accent: rgba(var(--hk-accent-rgb), 0.9);
--hk-accent-strong: rgba(var(--hk-accent-rgb), 1);
--hk-accent-border: rgba(var(--hk-accent-rgb), 0.35);
--hk-accent-soft: rgba(var(--hk-accent-rgb), 0.09);
--hk-accent-soft-hover: rgba(var(--hk-accent-rgb), 0.16);
/* Status — Apple system green / red */
--hk-green: #34c759;
--hk-red: #ff3b30;
--hk-orange: #ff9500;
/* Surfaces (translucent → adapts to any theme) */
--hk-card-bg: rgba(128, 128, 128, 0.04);
--hk-card-border: rgba(128, 128, 128, 0.14);
--hk-field-bg: rgba(128, 128, 128, 0.05);
--hk-field-border: rgba(128, 128, 128, 0.22);
--hk-hover: rgba(128, 128, 128, 0.08);
--hk-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
--hk-shadow-hover: 0 8px 28px rgba(0, 0, 0, 0.08);
--hk-muted: rgba(74, 86, 106, 0.62);
--hk-radius: 14px;
--hk-radius-sm: 9px;
--hk-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Helvetica Neue", "Microsoft YaHei", sans-serif;
--hk-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
font-family: var(--hk-font);
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.honk-page,
.honk-page * {
box-sizing: border-box;
}
/* LuCI themes (Argon/Bootstrap) set #view>div:first-child and .cbi-map to flex;
force block so our card/segmented-control layout behaves identically across
themes (ID selector specificity requires !important). */
.cbi-map.honk-page {
display: block !important;
}
/* ---------- Hidden attribute guard ---------- */
.honk-page [hidden] {
display: none !important;
}
/* ---------- Page header ---------- */
.honk-header {
margin: 4px 0 18px;
}
.honk-header h2 {
margin: 0;
padding: 0;
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
background: transparent !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.honk-header-sub {
margin: 6px 0 0;
font-size: 13px;
line-height: 1.5;
color: var(--hk-muted);
max-width: 68ch;
}
/* ---------- Cards ---------- */
.honk-page .cbi-section,
.honk-page .cbi-section-node {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
margin: 0;
padding: 0;
}
.honk-card {
background: var(--hk-card-bg);
border: 1px solid var(--hk-card-border);
border-radius: var(--hk-radius);
padding: 18px 20px;
margin-bottom: 16px;
box-shadow: var(--hk-shadow);
backdrop-filter: blur(6px);
transition: box-shadow 0.25s ease;
}
.honk-card:hover {
box-shadow: var(--hk-shadow-hover);
}
.honk-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.honk-card-title {
margin: 0 0 6px;
padding: 0 !important;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.55;
background: transparent !important;
box-shadow: none !important;
border-radius: 0 !important;
width: auto !important;
}
.honk-card-head .honk-card-title {
margin-bottom: 4px;
}
/* ---------- Status badge (pill) ---------- */
.honk-badge {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 4px 14px;
border-radius: 999px;
font-size: 12.5px;
font-weight: 600;
line-height: 1.3;
border: 1px solid transparent;
white-space: nowrap;
}
.honk-badge::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex: none;
}
.honk-badge--run {
color: var(--hk-green);
border-color: rgba(52, 199, 89, 0.5);
}
.honk-badge--stop {
color: var(--hk-red);
border-color: rgba(255, 59, 48, 0.55);
}
.honk-badge--idle {
color: var(--hk-muted);
border-color: rgba(128, 128, 128, 0.35);
}
/* ---------- Buttons ---------- */
.honk-page .cbi-button {
font-family: var(--hk-font);
font-size: 13px !important;
font-weight: 500;
line-height: 1.35 !important;
min-height: 0 !important;
height: auto !important;
padding: 7px 16px !important;
border-radius: 10px !important;
border: 1px solid rgba(128, 128, 128, 0.32) !important;
background: transparent !important;
color: inherit !important;
box-shadow: none !important;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.18s ease, border-color 0.18s ease,
opacity 0.18s ease, transform 0.12s ease;
}
.honk-page .cbi-button:hover {
background: var(--hk-hover) !important;
}
.honk-page .cbi-button:active {
transform: scale(0.98);
}
.honk-page .cbi-button:disabled {
opacity: 0.42;
cursor: not-allowed;
}
/* Primary — soft Apple blue */
.honk-page .cbi-button-action {
border-color: var(--hk-accent-border) !important;
color: var(--hk-accent) !important;
background: var(--hk-accent-soft) !important;
}
.honk-page .cbi-button-action:hover {
background: var(--hk-accent-soft-hover) !important;
border-color: var(--hk-accent) !important;
}
/* Positive — soft green */
.honk-page .cbi-button-positive {
border-color: rgba(52, 199, 89, 0.5) !important;
color: var(--hk-green) !important;
background: rgba(52, 199, 89, 0.09) !important;
}
.honk-page .cbi-button-positive:hover {
background: rgba(52, 199, 89, 0.16) !important;
border-color: var(--hk-green) !important;
}
/* Negative — soft red */
.honk-page .cbi-button-negative {
border-color: rgba(255, 59, 48, 0.5) !important;
color: var(--hk-red) !important;
background: rgba(255, 59, 48, 0.08) !important;
}
.honk-page .cbi-button-negative:hover {
background: rgba(255, 59, 48, 0.15) !important;
border-color: var(--hk-red) !important;
}
/* ---------- Button groups ---------- */
.honk-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.honk-subscription-add {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-top: 10px;
}
.honk-subscription-add .cbi-input-text {
flex: 1;
min-width: 160px;
}
/* ---------- Meta line (mono, muted) ---------- */
.honk-meta {
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
font-family: var(--hk-mono);
font-size: 12px;
color: var(--hk-muted);
}
.honk-meta-item {
display: inline-flex;
align-items: baseline;
gap: 5px;
}
.honk-meta-label {
opacity: 0.55;
}
/* ---------- Forms / fields ---------- */
.honk-page .cbi-value {
padding: 9px 0;
margin: 0;
border: 0;
background: transparent;
}
.honk-page .cbi-value-title {
float: none;
display: block;
width: auto;
padding: 0 0 6px;
font-size: 13px;
font-weight: 600;
opacity: 0.85;
}
.honk-page .cbi-value-field {
display: block;
width: auto;
padding: 0;
min-width: 0;
}
.honk-page .cbi-value-description {
font-size: 12px;
color: var(--hk-muted);
line-height: 1.5;
padding-top: 6px;
}
.honk-page .cbi-input-text,
.honk-page .cbi-input-password,
.honk-page .cbi-input-select,
.honk-page .cbi-input-textarea,
.honk-page select,
.honk-page input[type="text"],
.honk-page input[type="password"],
.honk-page input[type="number"],
.honk-page input[type="url"],
.honk-page textarea {
width: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
font-family: var(--hk-font);
font-size: 13.5px;
height: 38px;
padding: 7px 12px;
border: 1px solid var(--hk-field-border);
border-radius: var(--hk-radius-sm);
background: var(--hk-field-bg);
color: inherit;
transition: border-color 0.18s ease, box-shadow 0.18s ease,
background-color 0.18s ease;
}
.honk-page textarea,
.honk-page .cbi-input-textarea {
height: auto;
min-height: 96px;
resize: vertical;
font-family: var(--hk-mono);
line-height: 1.5;
}
.honk-page .cbi-input-text:focus,
.honk-page .cbi-input-password:focus,
.honk-page .cbi-input-select:focus,
.honk-page .cbi-input-textarea:focus,
.honk-page select:focus,
.honk-page input[type="text"]:focus,
.honk-page input[type="password"]:focus,
.honk-page input[type="number"]:focus,
.honk-page input[type="url"]:focus,
.honk-page textarea:focus {
outline: 0;
border-color: var(--hk-accent-border);
box-shadow: 0 0 0 3px var(--hk-accent-soft);
background: transparent;
}
.honk-page .cbi-input-text::placeholder,
.honk-page textarea::placeholder,
.honk-page input::placeholder {
opacity: 0.5;
}
/* Comfortable field widths inside full-width cards */
.honk-page .cbi-value-field .cbi-input-text,
.honk-page .cbi-value-field .cbi-input-password,
.honk-page .cbi-value-field input[type="number"],
.honk-page .cbi-value-field input[type="url"],
.honk-page .cbi-value-field input[type="text"],
.honk-page .cbi-value-field .cbi-input-select,
.honk-page .cbi-value-field select,
.honk-page .cbi-value-field input[type="file"] {
max-width: 400px;
}
.honk-page .cbi-value-field textarea,
.honk-page .cbi-value-field .cbi-input-textarea {
max-width: 640px;
}
/* ---------- iOS toggle switch (boot-enabled checkbox) ---------- */
.honk-page input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
position: relative;
width: 46px;
height: 27px;
margin: 0;
border: 0;
border-radius: 999px;
background: rgba(128, 128, 128, 0.3);
cursor: pointer;
transition: background-color 0.2s ease, opacity 0.2s ease;
flex: none;
}
.honk-page input[type="checkbox"]::before {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 23px;
height: 23px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease;
}
.honk-page input[type="checkbox"]:checked {
background: var(--hk-green);
}
.honk-page input[type="checkbox"]:checked::before {
transform: translateX(19px);
}
.honk-page input[type="checkbox"]:focus-visible {
outline: 2px solid var(--hk-accent);
outline-offset: 3px;
}
.honk-page input[type="checkbox"]:disabled {
opacity: 0.42;
cursor: not-allowed;
}
/* ---------- Segmented control (iOS style) ---------- */
.honk-seg {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: 10px;
background: rgba(128, 128, 128, 0.12);
max-width: 100%;
overflow-x: auto;
}
.honk-seg-btn {
height: 30px;
line-height: 30px;
padding: 0 14px;
border: 0;
border-radius: 8px;
background: transparent;
color: inherit;
font-family: var(--hk-font);
font-size: 12.5px;
font-weight: 500;
opacity: 0.6;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.18s ease, color 0.18s ease,
opacity 0.18s ease;
}
.honk-seg-btn:hover {
opacity: 0.85;
}
.honk-seg-btn[aria-selected="true"],
.honk-seg-btn.is-active {
background: #fff;
color: #1c1c1e;
opacity: 1;
font-weight: 600;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.16), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
}
.honk-seg-btn:focus-visible {
outline: 2px solid var(--hk-accent);
outline-offset: 2px;
}
/* ---------- Notes & alerts ---------- */
.honk-note {
font-size: 12.5px;
line-height: 1.55;
color: var(--hk-muted);
max-width: 72ch;
}
.honk-page .alert-message {
font-size: 12.5px;
line-height: 1.55;
padding: 10px 14px;
border-radius: var(--hk-radius-sm);
background: rgba(255, 59, 48, 0.08);
border: 1px solid rgba(255, 59, 48, 0.25);
color: inherit;
margin: 10px 0;
list-style: none;
}
.honk-page .alert-message li {
margin: 4px 0;
}
.honk-status-msg {
font-size: 12.5px;
color: var(--hk-muted);
min-height: 1.4em;
margin: 10px 0 0;
}
.honk-status-msg:empty {
margin: 0;
}
/* ---------- Disclosure (details/summary) ---------- */
.honk-page details {
margin-top: 12px;
}
.honk-page details > summary {
min-height: 36px;
padding: 8px 0;
font-size: 13px;
font-weight: 600;
cursor: pointer;
opacity: 0.85;
user-select: none;
list-style: none;
display: flex;
align-items: center;
gap: 8px;
}
.honk-page details > summary::-webkit-details-marker {
display: none;
}
.honk-page details > summary::before {
content: "";
width: 7px;
height: 7px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: rotate(-45deg);
transition: transform 0.2s ease;
opacity: 0.6;
flex: none;
margin-top: -3px;
}
.honk-page details[open] > summary::before {
transform: rotate(45deg);
margin-top: 3px;
}
.honk-page details > summary:hover {
opacity: 1;
}
.honk-disclosure-danger > summary {
color: var(--hk-red);
}
/* ---------- Logs ---------- */
.honk-log-toolbar {
display: flex;
align-items: flex-end;
gap: 10px;
flex-wrap: wrap;
margin: 12px 0;
}
.honk-log-toolbar .honk-field {
padding: 0;
}
.honk-log-toolbar label {
display: grid;
gap: 5px;
min-width: min(12em, 100%);
font-size: 12px;
font-weight: 600;
opacity: 0.8;
}
.honk-log-output {
width: 100%;
min-height: 380px;
max-height: 62vh;
margin: 0;
padding: 14px 16px;
border-radius: var(--hk-radius-sm);
border: 1px solid rgba(128, 128, 128, 0.22);
background: #1a1d21;
color: #d8dde6;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
font-family: var(--hk-mono);
font-size: 12px;
line-height: 1.55;
}
.honk-log-output .honk-log-line {
display: block;
padding: 1px 6px;
border-radius: 4px;
}
.honk-log-output .honk-log-ts {
color: #6b7480;
margin-right: 8px;
white-space: nowrap;
}
.honk-log-output .honk-log-lvl {
display: inline-block;
min-width: 40px;
padding: 0 5px;
margin-right: 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.4px;
text-align: center;
vertical-align: 1px;
}
.honk-log-output .honk-log-lvl-info {
color: #7fc7a8;
background: rgba(127, 199, 168, 0.08);
}
.honk-log-output .honk-log-lvl-warn {
color: #e8b95a;
background: rgba(232, 185, 90, 0.10);
}
.honk-log-output .honk-log-lvl-error {
color: #ea7878;
background: rgba(234, 120, 120, 0.10);
}
.honk-log-output .honk-log-lvl-debug {
color: #7a8290;
background: rgba(122, 130, 144, 0.10);
}
.honk-log-output .honk-log-lvl-trace {
color: #68717d;
background: rgba(104, 113, 125, 0.10);
}
.honk-log-output .honk-log-msg {
color: inherit;
}
.honk-log-output .honk-log-error .honk-log-msg {
color: #ea7878;
}
.honk-log-output .honk-log-warn .honk-log-msg {
color: #e8b95a;
}
.honk-log-output .honk-log-info .honk-log-msg {
color: #cfd6df;
}
.honk-log-output .honk-log-debug .honk-log-msg {
color: #7a8290;
}
.honk-log-output .honk-log-trace .honk-log-msg {
color: #68717d;
}
/* ---------- Package update rows ---------- */
.honk-pkg-row {
display: grid;
grid-template-columns: 24px 130px minmax(0, 1fr) auto auto;
gap: 10px;
align-items: center;
font-size: 12.5px;
padding: 9px 0;
border-top: 1px dashed rgba(128, 128, 128, 0.18);
}
.honk-pkg-row:first-child {
border-top: 0;
}
.honk-pkg-icon {
text-align: center;
font-size: 14px;
}
.honk-pkg-ok {
color: var(--hk-green);
}
.honk-pkg-err {
color: var(--hk-red);
}
.honk-pkg-new {
color: var(--hk-accent);
}
.honk-pkg-name {
font-weight: 600;
opacity: 0.85;
}
.honk-pkg-meta {
opacity: 0.7;
font-family: var(--hk-mono);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.honk-up-log {
margin-top: 12px;
padding: 10px 12px;
border: 1px solid var(--hk-field-border);
border-radius: var(--hk-radius-sm);
max-height: 220px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
font-family: var(--hk-mono);
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 640px) {
.honk-pkg-row {
grid-template-columns: 24px minmax(0, 1fr);
row-gap: 4px;
}
.honk-pkg-row .honk-pkg-icon {
grid-column: 1;
grid-row: 1;
}
.honk-pkg-row .honk-pkg-name {
grid-column: 2;
grid-row: 1;
}
.honk-pkg-row .honk-pkg-meta {
grid-column: 2;
grid-row: 2;
white-space: normal;
}
.honk-pkg-row > span:nth-child(4) {
display: none;
}
.honk-pkg-row > button,
.honk-pkg-row > span:last-child {
grid-column: 2;
grid-row: 3;
justify-self: start;
}
}
/* ---------- Page actions footer ---------- */
.honk-page .cbi-page-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
background: transparent;
border: 0;
padding: 12px 0 0;
margin: 0;
}
/* ---------- Responsive ---------- */
@media (max-width: 640px) {
.honk-page {
max-width: 100%;
}
.honk-header h2 {
font-size: 22px;
}
.honk-card {
padding: 15px 16px;
border-radius: 12px;
}
.honk-card-head {
flex-direction: column;
align-items: stretch;
}
.honk-actions {
width: 100%;
}
.honk-actions .cbi-button {
flex: 1 1 auto;
}
.honk-log-output {
min-height: 320px;
}
}
/* ---------- Dark mode ---------- */
body.dark .honk-page,
html.dark .honk-page,
body[data-theme="dark"] .honk-page,
html[data-theme="dark"] .honk-page,
body[data-bs-theme="dark"] .honk-page,
html[data-bs-theme="dark"] .honk-page,
body[data-darkmode="1"] .honk-page,
html[data-darkmode="1"] .honk-page,
body[data-darkmode="true"] .honk-page,
html[data-darkmode="true"] .honk-page {
--hk-green: #30d158;
--hk-red: #ff453a;
--hk-orange: #ff9f0a;
--hk-card-bg: rgba(255, 255, 255, 0.04);
--hk-card-border: rgba(255, 255, 255, 0.1);
--hk-field-bg: rgba(255, 255, 255, 0.05);
--hk-field-border: rgba(255, 255, 255, 0.16);
--hk-hover: rgba(255, 255, 255, 0.08);
--hk-shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
--hk-shadow-hover: 0 10px 32px rgba(0, 0, 0, 0.45);
--hk-muted: rgba(200, 210, 224, 0.6);
}
body.dark .honk-page .honk-seg-btn[aria-selected="true"],
html.dark .honk-page .honk-seg-btn[aria-selected="true"],
body[data-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
html[data-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
body[data-bs-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
html[data-bs-theme="dark"] .honk-page .honk-seg-btn[aria-selected="true"],
body[data-darkmode="1"] .honk-page .honk-seg-btn[aria-selected="true"],
html[data-darkmode="1"] .honk-page .honk-seg-btn[aria-selected="true"],
body[data-darkmode="true"] .honk-page .honk-seg-btn[aria-selected="true"],
html[data-darkmode="true"] .honk-page .honk-seg-btn[aria-selected="true"],
body.dark .honk-page .honk-seg-btn.is-active,
html.dark .honk-page .honk-seg-btn.is-active,
body[data-theme="dark"] .honk-page .honk-seg-btn.is-active,
html[data-theme="dark"] .honk-page .honk-seg-btn.is-active,
body[data-bs-theme="dark"] .honk-page .honk-seg-btn.is-active,
html[data-bs-theme="dark"] .honk-page .honk-seg-btn.is-active,
body[data-darkmode="1"] .honk-page .honk-seg-btn.is-active,
html[data-darkmode="1"] .honk-page .honk-seg-btn.is-active,
body[data-darkmode="true"] .honk-page .honk-seg-btn.is-active,
html[data-darkmode="true"] .honk-page .honk-seg-btn.is-active {
background: rgba(255, 255, 255, 0.22);
color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
body.dark .honk-page .honk-log-output,
html.dark .honk-page .honk-log-output,
body[data-theme="dark"] .honk-page .honk-log-output,
html[data-theme="dark"] .honk-page .honk-log-output,
body[data-bs-theme="dark"] .honk-page .honk-log-output,
html[data-bs-theme="dark"] .honk-page .honk-log-output,
body[data-darkmode="1"] .honk-page .honk-log-output,
html[data-darkmode="1"] .honk-page .honk-log-output,
body[data-darkmode="true"] .honk-page .honk-log-output,
html[data-darkmode="true"] .honk-page .honk-log-output {
background: rgba(0, 0, 0, 0.28);
}
/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
.honk-page .cbi-button,
.honk-page .cbi-button:active,
.honk-card,
.honk-page input[type="checkbox"],
.honk-page input[type="checkbox"]::before,
.honk-page details > summary::before {
transition: none !important;
transform: none !important;
}
}
@@ -1,192 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require view';
'require view.honk.rpc as honk';
function detectLogLevel(line) {
// honk-core tracing output (e.g. " ERROR honk_core::...")
if (/\s(?:ERROR|FATAL|PANIC)\s/.test(line)) return 'error';
if (/\sWARN(?:ING)?\s/.test(line)) return 'warn';
if (/\sDEBUG\s/.test(line)) return 'debug';
if (/\sTRACE\s/.test(line)) return 'trace';
if (/\sINFO\s/.test(line)) return 'info';
// syslog facility.level (e.g. "daemon.err")
if (/daemon\.(?:err|crit|alert|emerg)\b/.test(line)) return 'error';
if (/daemon\.(?:warning|warn|notice)\b/.test(line)) return 'warn';
if (/daemon\.debug\b/.test(line)) return 'debug';
if (/daemon\.info\b/.test(line)) return 'info';
return '';
}
function formatLogTs(raw) {
// logread timestamp: "Wed Oct 7 18:58:09 2026" -> "2026-10-07 18:58:09"
const m = String(raw || '').match(/^[A-Z][a-z]{2}\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+(\d{4})$/);
if (!m) return raw;
const months = { Jan: 1, Feb: 2, Mar: 3, Apr: 4, May: 5, Jun: 6, Jul: 7, Aug: 8, Sep: 9, Oct: 10, Nov: 11, Dec: 12 };
const month = months[m[1]];
if (!month) return raw;
return m[4] + '-' + String(month).padStart(2, '0') + '-' + String(Number(m[2])).padStart(2, '0') + ' ' + m[3];
}
function lvlShort(level) {
const u = (level || '').toUpperCase();
if (u === 'WARNING') return 'WARN';
return u || '-';
}
function lvlClass(level) {
const u = (level || '').toUpperCase();
if (u === 'TRACE') return 'honk-log-lvl-trace';
if (u === 'DEBUG') return 'honk-log-lvl-debug';
if (u === 'INFO') return 'honk-log-lvl-info';
if (u === 'WARN' || u === 'WARNING') return 'honk-log-lvl-warn';
return 'honk-log-lvl-error';
}
function parseLogLine(line) {
// "Wed Oct 7 18:58:09 2026 daemon.info honk-core[31377]: INFO honk_core::..."
const m = line.match(/^([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+\S+\s+[^:]+:\s*(.*)$/);
if (!m) return { ts: '', lvl: '', msg: line };
let body = m[2] || '';
const lvlMatch = body.match(/^\s*(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i);
let lvl = '';
if (lvlMatch) {
lvl = lvlMatch[1].toUpperCase();
body = lvlMatch[2];
}
return { ts: formatLogTs(m[1]), lvl: lvl, msg: body };
}
function buildLogLine(line) {
const level = detectLogLevel(line);
const parsed = parseLogLine(line);
const parts = [];
if (parsed.ts)
parts.push(E('span', { 'class': 'honk-log-ts' }, parsed.ts));
if (parsed.lvl)
parts.push(E('span', { 'class': 'honk-log-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)));
parts.push(E('span', { 'class': 'honk-log-msg' }, parsed.msg));
return E('span', { 'class': 'honk-log-line' + (level ? ' honk-log-' + level : '') }, parts);
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
_timer: null,
handleUnload: function() {
if (this._timer != null)
clearInterval(this._timer);
this._timer = null;
},
render: function() {
honk.installStyles();
const source = E('select', { 'class': 'cbi-input-select' }, [
E('option', { 'value': 'core' }, _('Core')),
E('option', { 'value': 'service' }, _('Service')),
E('option', { 'value': 'maintenance' }, _('Maintenance'))
]);
const level = E('select', { 'class': 'cbi-input-select' }, [
E('option', { 'value': 'all' }, _('All levels')),
E('option', { 'value': 'debug' }, _('Debug and above')),
E('option', { 'value': 'info' }, _('Info and above')),
E('option', { 'value': 'warn' }, _('Warnings and errors')),
E('option', { 'value': 'error' }, _('Errors only'))
]);
const output = E('pre', {
'class': 'honk-log-output',
'aria-live': 'polite'
}, _('Loading logs…'));
const status = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const pause = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Pause'));
const refresh = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Refresh now'));
const download = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Download redacted log'));
let paused = false;
let pendingRequests = 0;
let requestSequence = 0;
let latest = [];
download.disabled = true;
function loadLogs(force) {
if (paused && !force || pendingRequests && !force)
return Promise.resolve();
const requestId = ++requestSequence;
const requestedSource = source.value;
const requestedLevel = level.value;
pendingRequests++;
download.disabled = true;
return honk.logs(requestedSource, requestedLevel, 200).then(honk.ensureOk).then(function(result) {
if (requestId !== requestSequence)
return;
latest = Array.isArray(result.lines) ? result.lines.map(String) : [];
output.replaceChildren();
if (latest.length) {
latest.forEach(function(line) {
output.appendChild(buildLogLine(line));
});
}
else {
output.textContent = _('No matching Honk log entries.');
}
status.textContent = result.capped ? _('Output was capped; showing the latest %d entries.').format(latest.length) : _('Showing the latest %d matching entries.').format(latest.length);
}).catch(function(error) {
if (requestId === requestSequence)
status.textContent = honk.errorMessage(error, _('Could not read Honk logs.'));
}).finally(function() {
pendingRequests--;
download.disabled = pendingRequests > 0;
});
}
pause.addEventListener('click', function(ev) {
ev.preventDefault();
paused = !paused;
pause.textContent = paused ? _('Resume') : _('Pause');
status.textContent = paused ? _('Log polling is paused.') : _('Log polling resumed.');
if (!paused)
loadLogs(true);
});
refresh.addEventListener('click', function(ev) {
ev.preventDefault();
loadLogs(true);
});
source.addEventListener('change', function() { loadLogs(true); });
level.addEventListener('change', function() { loadLogs(true); });
download.addEventListener('click', function(ev) {
ev.preventDefault();
const blob = new Blob([ latest.join('\n') + (latest.length ? '\n' : '') ], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = E('a', { 'href': url, 'download': 'honk-redacted.log' });
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
});
source.id = 'honk-log-source';
level.id = 'honk-log-level';
this._timer = setInterval(function() { loadLogs(false); }, 5000);
loadLogs(true);
return E('div', { 'class': 'cbi-map honk-page' }, [
E('div', { 'class': 'honk-header' }, [
E('h2', {}, _('Logs')),
E('p', { 'class': 'honk-header-sub' }, _('Shows up to 200 Honk-specific lines, capped at 32 KiB, and refreshes every 5 seconds while live. Sensitive URLs and credentials are redacted before display.'))
]),
E('section', { 'class': 'honk-card' }, [
E('h3', { 'class': 'honk-card-title' }, _('Live log')),
E('div', { 'class': 'honk-log-toolbar' }, [
E('label', { 'for': source.id }, [ _('Log source'), source ]),
E('label', { 'for': level.id }, [ _('Minimum level'), level ]),
E('div', { 'class': 'honk-actions' }, [ pause, refresh, download ])
]),
status,
output
])
]);
}
});
@@ -1,535 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require fs';
'require poll';
'require uci';
'require ui';
'require view';
'require view.honk.rpc as honk';
const BACKUP_PATH = '/tmp/honk-backup.tar.gz';
const RESTORE_PATH = '/tmp/honk-maintenance/restore.tar.gz';
const PKG_INFO = '/usr/share/luci-app-honk/pkg-info.sh';
const UPDATE_PKG = '/usr/share/luci-app-honk/update-pkg.sh';
const REFRESH_INDEX = '/usr/share/luci-app-honk/refresh-index.sh';
const UPDATE_GEO = '/usr/share/luci-app-honk/update-geo.sh';
const GEO_CRON = '/usr/share/luci-app-honk/geo-cron.sh';
const PKGS = [ 'honk', 'luci-app-honk' ];
const PKG_NAMES = { honk: 'Honk', 'luci-app-honk': 'luci-app-honk' };
const DATA_PATHS = {
geoip: '/usr/share/v2ray/geoip.dat',
geosite: '/usr/share/v2ray/geosite.dat'
};
// Geo data source presets. `loyalsoldier` keeps both URLs empty so the actual
// default lives in one place — update-geo.sh — avoiding UI/script drift.
const GEO_PRESETS = {
loyalsoldier: { geoip: '', geosite: '' },
v2fly: {
geoip: 'https://github.com/v2fly/geoip/releases/latest/download/geoip.dat',
geosite: 'https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat'
}
};
function fmtBytes(n) {
if (!n && n !== 0) return '-';
if (n < 1024) return n + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
return (n / 1024 / 1024).toFixed(1) + ' MB';
}
function fmtMtime(epoch) {
if (!epoch) return '';
const d = new Date(epoch * 1000);
return d.toISOString().slice(0, 10);
}
// Compare two version strings like `sort -V`. Returns <0 / 0 / >0.
function cmpVer(a, b) {
const ax = String(a).match(/(\d+|\D+)/g) || [];
const bx = String(b).match(/(\d+|\D+)/g) || [];
const n = Math.max(ax.length, bx.length);
for (let i = 0; i < n; i++) {
const as = ax[i], bs = bx[i];
if (as === undefined) return -1;
if (bs === undefined) return 1;
if (/^\d+$/.test(as) && /^\d+$/.test(bs)) {
const d = parseInt(as, 10) - parseInt(bs, 10);
if (d !== 0) return d < 0 ? -1 : 1;
} else if (as !== bs) {
return as < bs ? -1 : 1;
}
}
return 0;
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
load: function() {
return uci.load('honk').catch(function() {});
},
render: function() {
honk.installStyles();
const page = E('div', { 'class': 'cbi-map honk-page' });
const recoveryMessage = E('p', { 'class': 'alert-message', 'role': 'status' }, '');
recoveryMessage.hidden = true;
const writeControls = [];
let recoveryRequired = false;
function phaseMessage(job, fallback) {
const messages = {
preparing: _('Preparing maintenance…'),
stopping: _('Stopping Honk…'),
applying: _('Applying the staged data…'),
validating: _('Validating the new state…'),
starting: _('Starting Honk…'),
rolling_back: _('Restoring the previous state…'),
committing: _('Finishing the transaction…'),
recovery: _('Recovery is required before another write operation.')
};
return messages[job.phase] || fallback;
}
function blockWrites() {
recoveryRequired = true;
writeControls.forEach(function(button) { button.disabled = true; });
}
/* Shared log pane and row helper for both data and package updates. */
const updateLog = E('pre', { 'class': 'honk-up-log', 'hidden': true }, '');
function mkPkgRow(icon, iconCls, name, meta, btn) {
return E('div', { 'class': 'honk-pkg-row' }, [
E('span', { 'class': 'honk-pkg-icon ' + iconCls }, icon),
E('span', { 'class': 'honk-pkg-name' }, name),
E('span', { 'class': 'honk-pkg-meta', 'title': meta }, meta),
E('span', {}, ''),
btn || E('span', {}, '')
]);
}
function probeFile(path) {
return fs.stat(path).then(function(st) {
return { exists: true, size: st.size, mtime: st.mtime };
}).catch(function() {
return { exists: false, size: 0, mtime: 0 };
});
}
/* --- Geo data card --- */
const dataSection = E('section', { 'class': 'honk-card' });
const dataBody = E('div', { 'id': 'honk-geo' }, E('em', {}, _('Probing…')));
function updateGeo(kind, btn) {
const orig = btn.textContent;
btn.disabled = true;
btn.textContent = '…';
let tries = 0;
const pollLog = function() {
return fs.read_direct('/tmp/luci-app-honk.' + kind + '.log', 'text').then(function(c) {
if (c) {
updateLog.textContent = c;
updateLog.hidden = false;
}
if (/[✓✗]/.test(c)) { refreshData(); return; }
if (tries++ > 90) return;
return new Promise(function(r) { setTimeout(r, 2000); }).then(pollLog);
}).catch(function() {});
};
return fs.exec(UPDATE_GEO, [ kind ]).then(function(res) {
if (res.code === 0) return pollLog();
}).catch(function() {}).finally(function() {
btn.disabled = false;
btn.textContent = orig;
});
}
function refreshData() {
return Promise.all([ probeFile(DATA_PATHS.geoip), probeFile(DATA_PATHS.geosite) ]).then(function(r) {
while (dataBody.firstChild)
dataBody.removeChild(dataBody.firstChild);
[
{ k: 'geoip', name: 'GeoIP', r: r[0] },
{ k: 'geosite', name: 'GeoSite', r: r[1] }
].forEach(function(entry) {
const btn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Update'));
btn.addEventListener('click', function(ev) { ev.preventDefault(); updateGeo(entry.k, btn); });
const meta = entry.r.exists
? fmtBytes(entry.r.size) + (entry.r.mtime ? ' · ' + fmtMtime(entry.r.mtime) : '')
: _('missing — click Update to fetch');
dataBody.appendChild(mkPkgRow(
entry.r.exists ? '✓' : '✗',
entry.r.exists ? 'honk-pkg-ok' : 'honk-pkg-err',
entry.name,
meta,
btn
));
});
});
}
function geoField(label, input) {
return E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
E('div', { 'class': 'cbi-value-field' }, input)
]);
}
const geoSettings = (function() {
const gi0 = uci.get('honk', 'main', 'geoip_url') || '';
const gs0 = uci.get('honk', 'main', 'geosite_url') || '';
const auto0 = uci.get('honk', 'main', 'geo_auto') === '1';
const freq0 = uci.get('honk', 'main', 'geo_auto_freq') || 'daily';
const preset0 = (!gi0 && !gs0) ? 'loyalsoldier' :
(gi0 === GEO_PRESETS.v2fly.geoip && gs0 === GEO_PRESETS.v2fly.geosite) ? 'v2fly' : 'custom';
const presetSel = E('select', { 'class': 'cbi-input-select', 'id': 'honk-geo-preset' }, [
E('option', { 'value': 'loyalsoldier' }, 'Loyalsoldier'),
E('option', { 'value': 'v2fly' }, 'v2fly'),
E('option', { 'value': 'custom' }, _('Custom'))
]);
presetSel.value = preset0;
const giInput = E('input', { 'class': 'cbi-input-text', 'type': 'text', 'id': 'honk-geo-ip', 'placeholder': 'https://…/geoip.dat' });
const gsInput = E('input', { 'class': 'cbi-input-text', 'type': 'text', 'id': 'honk-geo-site', 'placeholder': 'https://…/geosite.dat' });
giInput.value = preset0 === 'custom' ? gi0 : '';
gsInput.value = preset0 === 'custom' ? gs0 : '';
const customRows = E('div', {}, [
geoField(_('GeoIP URL'), giInput),
geoField(_('GeoSite URL'), gsInput)
]);
const syncCustom = function() { customRows.style.display = presetSel.value === 'custom' ? '' : 'none'; };
presetSel.addEventListener('change', syncCustom);
syncCustom();
const autoSel = E('select', { 'class': 'cbi-input-select', 'id': 'honk-geo-auto' }, [
E('option', { 'value': 'off' }, _('Disabled')),
E('option', { 'value': 'daily' }, _('Daily')),
E('option', { 'value': 'weekly' }, _('Weekly'))
]);
autoSel.value = auto0 ? freq0 : 'off';
const saveBtn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Save'));
saveBtn.addEventListener('click', function() {
const p = presetSel.value;
let gi = '', gs = '';
const geoAuto = autoSel.value !== 'off';
const geoFreq = autoSel.value === 'weekly' ? 'weekly' : 'daily';
if (p === 'v2fly') { gi = GEO_PRESETS.v2fly.geoip; gs = GEO_PRESETS.v2fly.geosite; }
else if (p === 'custom') {
gi = giInput.value.trim(); gs = gsInput.value.trim();
for (const [label, url] of [['GeoIP URL', gi], ['GeoSite URL', gs]]) {
if (url) {
try {
const u = new URL(url);
if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad protocol');
} catch (e) {
document.getElementById('honk-geo-msg').textContent = _('Invalid %s: must be an http(s) URL.').format(label);
return;
}
}
}
}
uci.set('honk', 'main', 'geoip_url', gi);
uci.set('honk', 'main', 'geosite_url', gs);
uci.set('honk', 'main', 'geo_auto', geoAuto ? '1' : '0');
uci.set('honk', 'main', 'geo_auto_freq', geoFreq);
const orig = saveBtn.textContent;
saveBtn.disabled = true; saveBtn.textContent = '…';
uci.save().then(function() {
return uci.changes();
}).then(function(changes) {
if (changes && Object.keys(changes).length)
return uci.apply();
}).then(function() {
return fs.exec(GEO_CRON, [ geoAuto ? 'enable' : 'disable' ]);
}).then(function() {
updateLog.textContent = _('Geo data source saved.');
updateLog.hidden = false;
ui.changes.init();
}).catch(function(e) {
updateLog.textContent = _('Save failed') + ': ' + (e && e.message ? e.message : e);
updateLog.hidden = false;
}).finally(function() {
saveBtn.disabled = false; saveBtn.textContent = orig;
});
});
return E('details', {}, [
E('summary', {}, _('Data source and auto-update')),
E('div', {}, [
geoField(_('Source'), presetSel),
customRows,
geoField(_('Auto-update'), autoSel),
E('div', { 'class': 'honk-actions' }, [ saveBtn ]),
E('p', { 'class': 'honk-note', 'id': 'honk-geo-msg' })
])
]);
})();
dataSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Geo data')));
dataSection.appendChild(E('p', { 'class': 'honk-note' }, _('GeoIP and GeoSite routing data can be updated here. Updates run in the background and reload Honk when done.')));
dataSection.appendChild(dataBody);
dataSection.appendChild(geoSettings);
/* --- Software package updates card --- */
const updateSection = E('section', { 'class': 'honk-card' });
const pkgBody = E('div', { 'id': 'honk-pkg' }, E('em', {}, _('Probing…')));
function probePkg(pkg) {
return fs.exec(PKG_INFO, [ pkg ]).then(function(res) {
const out = (res.stdout || '').trim().split('\t');
return { installed: out[0] || '', latest: out[1] || '' };
}).catch(function() {
return { installed: '', latest: '' };
});
}
function upgradePkg(pkg, btn) {
const orig = btn.textContent;
btn.disabled = true;
btn.textContent = '…';
let tries = 0;
const pollLog = function() {
return fs.read_direct('/tmp/luci-app-honk.pkg-' + pkg + '.log', 'text').then(function(c) {
if (c) {
updateLog.textContent = c;
updateLog.hidden = false;
}
if (/[✓✗]/.test(c)) { refreshPkgs(); return; }
if (tries++ > 90) return;
return new Promise(function(r) { setTimeout(r, 2000); }).then(pollLog);
}).catch(function() {});
};
return fs.exec(UPDATE_PKG, [ pkg ]).then(function(res) {
if (res.code === 0) return pollLog();
}).catch(function() {}).finally(function() {
btn.disabled = false;
btn.textContent = orig;
});
}
function refreshPkgs() {
const probes = PKGS.map(probePkg);
return Promise.all(probes).then(function(infos) {
while (pkgBody.firstChild)
pkgBody.removeChild(pkgBody.firstChild);
PKGS.forEach(function(pkg, i) {
const r = infos[i];
const btn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Upgrade'));
btn.addEventListener('click', function(ev) { ev.preventDefault(); upgradePkg(pkg, btn); });
const cmp = (r.installed && r.latest) ? cmpVer(r.latest, r.installed) : null;
const updatable = cmp !== null && cmp > 0;
let meta;
if (!r.installed) {
meta = _('not installed via package manager');
btn.disabled = true;
btn.textContent = _('Unavailable');
} else if (!r.latest) {
meta = _('installed') + ': ' + r.installed + ' · ' + _('latest version unknown');
btn.disabled = true;
} else if (updatable) {
meta = _('installed') + ': ' + r.installed + ' → ' + _('latest') + ': ' + r.latest;
} else {
meta = _('installed') + ': ' + r.installed + ' · ' + _('up to date');
btn.disabled = true;
}
pkgBody.appendChild(mkPkgRow(
updatable ? '↑' : (r.installed ? '✓' : '✗'),
updatable ? 'honk-pkg-new' : (r.installed ? 'honk-pkg-ok' : 'honk-pkg-err'),
PKG_NAMES[pkg],
meta,
btn
));
});
});
}
updateSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Package updates')));
updateSection.appendChild(E('p', { 'class': 'honk-note' }, _('Honk core and LuCI can be upgraded here without leaving the management page. The package index refreshes in the background.')));
updateSection.appendChild(pkgBody);
/* --- Config backup card --- */
const backupSection = E('section', { 'class': 'honk-card' });
const backupMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const exportBtn = E('button', { 'class': 'cbi-button cbi-button-action', 'type': 'button' }, _('Export'));
const importBtn = E('button', { 'class': 'cbi-button', 'type': 'button' }, _('Import'));
const restoreCfgBtn = E('button', { 'class': 'cbi-button cbi-button-negative', 'type': 'button' }, _('Restore config'));
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
let backupBusy = false;
writeControls.push(exportBtn, importBtn, restoreCfgBtn);
function setBackupBusy(busy) {
backupBusy = busy;
[exportBtn, importBtn, restoreCfgBtn].forEach(function(b) {
b.disabled = busy || recoveryRequired;
});
}
exportBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (backupBusy) return;
setBackupBusy(true);
backupMessage.textContent = _('Preparing backup…');
honk.backup().then(honk.ensureOk).then(function(result) {
return honk.waitJob(result.job_id, function(job) {
backupMessage.textContent = job.state === 'running' ? _('Creating backup…') : _('Preparing download…');
});
}).then(function() {
return fs.read_direct(BACKUP_PATH, 'blob');
}).then(function(blob) {
const url = URL.createObjectURL(blob);
const link = E('a', { 'href': url, 'download': 'honk-backup.tar.gz' });
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
backupMessage.textContent = _('Backup downloaded.');
}).catch(function(error) {
if (error && error.recoveryRequired)
blockWrites();
backupMessage.textContent = honk.errorMessage(error, _('Backup failed.'));
}).finally(function() { setBackupBusy(false); });
});
importBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (backupBusy) return;
fileInput.click();
});
fileInput.addEventListener('change', function(ev) {
const file = ev.target.files && ev.target.files[0];
if (!file) return;
if (!confirm(_('Stop Honk before restoring. Restoring replaces Honk user data. Continue?'))) {
fileInput.value = '';
return;
}
setBackupBusy(true);
backupMessage.textContent = _('Restoring…');
honk.restorePrepare().then(honk.ensureOk).then(function() {
return ui.uploadFile(RESTORE_PATH);
}).then(function() {
return honk.restore();
}).then(honk.ensureOk).then(function(result) {
return honk.waitJob(result.job_id, function(job) {
backupMessage.textContent = phaseMessage(job, _('Restoring Honk data…'));
});
}).then(function(result) {
backupMessage.textContent = honk.resultMessage(result, _('Backup restored.'));
}).catch(function(error) {
if (error && error.recoveryRequired)
blockWrites();
backupMessage.textContent = honk.errorMessage(error, _('Restore failed.'));
}).finally(function() {
setBackupBusy(false);
fileInput.value = '';
});
});
restoreCfgBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (!confirm(_('This permanently clears Honk subscriptions, nodes, policies, DNS data, history, and cache, then restores the default configuration. The administrator account is kept, and Honk will be stopped. Continue?')))
return;
setBackupBusy(true);
backupMessage.textContent = _('Restoring default config…');
honk.reset().then(honk.ensureOk).then(function(result) {
return honk.waitJob(result.job_id, function(job) {
backupMessage.textContent = phaseMessage(job, _('Restoring default config…'));
});
}).then(function(result) {
backupMessage.textContent = honk.resultMessage(result, _('Honk data reset. The service is stopped.'));
}).catch(function(error) {
if (error && error.recoveryRequired)
blockWrites();
backupMessage.textContent = honk.errorMessage(error, _('Reset failed.'));
}).finally(function() { setBackupBusy(false); });
});
backupSection.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Config backup')));
backupSection.appendChild(E('p', { 'class': 'honk-note' }, _('Export a backup archive, import one from your computer, or restore Honk to its default configuration.')));
backupSection.appendChild(E('div', { 'class': 'honk-actions' }, [ exportBtn, importBtn, restoreCfgBtn ]));
backupSection.appendChild(fileInput);
backupSection.appendChild(backupMessage);
/* --- System check and repair card --- */
const health = E('section', { 'class': 'honk-card' });
const healthMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const check = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Run system check'));
check.addEventListener('click', function(ev) {
ev.preventDefault();
check.disabled = true;
healthMessage.textContent = _('Checking…');
honk.check().then(function(result) {
if (result && result.ok === true) {
healthMessage.textContent = _('System check passed.');
}
else if (Array.isArray(result && result.errors) && result.errors.length) {
healthMessage.textContent = result.errors.map(honk.statusIssue).join(';');
}
else {
healthMessage.textContent = _('The backend did not confirm the check.');
}
}).catch(function(error) {
healthMessage.textContent = _('System check failed.');
}).finally(function() { check.disabled = false; });
});
const repairMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const repair = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Repair'));
writeControls.push(repair);
repair.addEventListener('click', function(ev) {
ev.preventDefault();
repair.disabled = true;
repairMessage.textContent = _('Repairing…');
honk.repair().then(honk.ensureOk).then(function(result) {
repairMessage.textContent = honk.resultMessage(result, _('Repair completed.'));
}).catch(function(error) {
if (error && error.recoveryRequired)
blockWrites();
repairMessage.textContent = honk.errorMessage(error, _('Repair failed.'));
}).finally(function() { repair.disabled = recoveryRequired; });
});
health.appendChild(E('h3', { 'class': 'honk-card-title' }, _('System check and repair')));
health.appendChild(E('p', { 'class': 'honk-note' }, _('Checks Honk system components and reports detected issues.')));
health.appendChild(E('div', { 'class': 'honk-actions' }, [ check, repair ]));
health.appendChild(healthMessage);
health.appendChild(repairMessage);
health.appendChild(E('p', { 'class': 'honk-note' }, _('Repair replaces missing or damaged Honk system files and does not overwrite user configuration.')));
page.appendChild(E('div', { 'class': 'honk-header' }, [
E('h2', {}, _('Maintenance')),
E('p', { 'class': 'honk-header-sub' }, _('Package updates, config backup and system recovery for the Honk installation.'))
]));
page.appendChild(recoveryMessage);
page.appendChild(dataSection);
page.appendChild(updateSection);
page.appendChild(backupSection);
page.appendChild(health);
page.appendChild(updateLog);
// Background index refresh so new versions show on the next poll.
fs.exec(REFRESH_INDEX, []).catch(function() {});
refreshData();
refreshPkgs();
poll.add(refreshData, 15);
poll.add(refreshPkgs, 15);
honk.status().then(function(result) {
const errors = Array.isArray(result && result.errors) ? result.errors : [];
const recoveryToken = errors.indexOf('update_recovery_required') >= 0 ? 'update_recovery_required' :
errors.indexOf('maintenance_recovery_required') >= 0 ? 'maintenance_recovery_required' : null;
if (recoveryToken) {
blockWrites();
recoveryMessage.textContent = honk.statusIssue(recoveryToken);
recoveryMessage.hidden = false;
}
}).catch(function() {});
return page;
}
});
@@ -1,391 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require network';
'require poll';
'require view';
'require view.honk.rpc as honk';
'require uci';
function getNetworkNames(networks) {
if (!networks)
return [];
const list = Array.isArray(networks) ? networks : Object.keys(networks).map(function(key) { return networks[key]; });
return list.map(function(net) {
if (net && typeof net.getName === 'function')
return net.getName();
if (net && typeof net.name === 'string')
return net.name;
return typeof net === 'string' ? net : '';
}).filter(function(name, index, names) {
return !!name && names.indexOf(name) === index;
}).sort();
}
function metaItem(label, value) {
return E('span', { 'class': 'honk-meta-item' }, [
E('span', { 'class': 'honk-meta-label' }, label),
value == null || value === '' ? '—' : String(value)
]);
}
function field(label, input, help) {
const row = E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
E('div', { 'class': 'cbi-value-field' }, input)
]);
if (help)
row.appendChild(E('div', { 'class': 'cbi-value-description honk-note' }, help));
return row;
}
function captureFields(root) {
const values = {};
Array.prototype.forEach.call(root.querySelectorAll('input, select, textarea'), function(input) {
const key = input.id || input.name;
if (!key)
return;
values[key] = {
value: input.value,
checked: input.checked,
focused: document.activeElement === input,
selectionStart: typeof input.selectionStart === 'number' ? input.selectionStart : null,
selectionEnd: typeof input.selectionEnd === 'number' ? input.selectionEnd : null
};
});
return values;
}
function restoreFields(root, values) {
Object.keys(values).forEach(function(key) {
const input = root.querySelector('#%s'.format(key));
if (!input || input.type === 'file')
return;
input.value = values[key].value;
input.checked = values[key].checked;
if (values[key].focused) {
input.focus();
if (values[key].selectionStart !== null && typeof input.setSelectionRange === 'function')
input.setSelectionRange(values[key].selectionStart, values[key].selectionEnd);
}
});
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
load: function() {
return Promise.all([
honk.status().then(function(status) { return { status: status }; }).catch(function(error) { return { error: error.message || String(error) }; }),
network.getNetworks().then(getNetworkNames).then(function(networks) { return { networks: networks }; }).catch(function() { return { networks: [] }; }),
uci.load('honk').then(function() { return { clash_secret: uci.get('honk', 'main', 'clash_api_secret') || '' }; }).catch(function() { return { clash_secret: '' }; })
]).then(function(results) {
return Object.assign({}, results[0], results[1], results[2]);
});
},
render: function(data) {
honk.installStyles();
const page = E('div', { 'class': 'cbi-map honk-page' });
const statusArea = E('div');
const actionMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const setupArea = E('div');
let currentStatus = data.status;
const clashSecret = data.clash_secret || '';
let busy = false;
let setupBusy = false;
let settingsBusy = false;
let setupState = null;
function renderStatus(status, error) {
const fields = captureFields(statusArea);
const focusedAction = statusArea.contains(document.activeElement) && document.activeElement.dataset.action;
while (statusArea.firstChild)
statusArea.removeChild(statusArea.firstChild);
if (error) {
statusArea.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read Honk status.')));
restoreFields(statusArea, fields);
return;
}
currentStatus = status;
const running = status.running === true;
/* Status card */
const badge = E('span', {
'class': 'honk-badge %s'.format(running ? 'honk-badge--run' : 'honk-badge--stop')
}, running ? _('Running') : _('Stopped'));
const webUi = honk.localWebUi(status);
const openDoona = E(webUi ? 'a' : 'button', {
'class': 'cbi-button cbi-button-action',
'href': webUi || null,
'target': webUi ? '_blank' : null,
'rel': webUi ? 'noopener noreferrer' : null,
'type': webUi ? null : 'button',
'disabled': webUi ? null : true
}, _('Open Doona'));
const clashUi = honk.localClashUi(status, clashSecret);
const openClash = E(clashUi ? 'a' : 'button', {
'class': 'cbi-button cbi-button-action',
'href': clashUi || null,
'target': clashUi ? '_blank' : null,
'rel': clashUi ? 'noopener noreferrer' : null,
'type': clashUi ? null : 'button',
'disabled': clashUi ? null : true
}, _('Open Clash panel'));
const toggle = E('button', {
'class': 'cbi-button cbi-button-action',
'type': 'button',
'data-action': running ? 'stop' : 'start'
}, running ? _('Stop') : _('Start'));
toggle.disabled = busy || !status.initialized;
toggle.addEventListener('click', function(ev) {
ev.preventDefault();
runAction(running ? 'stop' : 'start');
});
const actions = [ openDoona, openClash, toggle ];
if (running) {
const restart = E('button', { 'class': 'cbi-button', 'type': 'button', 'data-action': 'restart' }, _('Restart'));
restart.disabled = busy;
restart.addEventListener('click', function(ev) {
ev.preventDefault();
runAction('restart');
});
actions.push(restart);
}
const statusCard = E('section', { 'class': 'honk-card' }, [
E('div', { 'class': 'honk-card-head' }, [
E('div', {}, [
E('h3', { 'class': 'honk-card-title' }, _('Status')),
badge
]),
E('div', { 'class': 'honk-actions' }, actions)
]),
E('div', { 'class': 'honk-meta' }, [
metaItem(_('Core version'), status.core_version),
metaItem(_('Doona version'), status.doona_version),
metaItem(_('Kernel'), status.kernel),
metaItem(_('Clash API'), status.clash_api_enabled ? status.clash_api_url : _('Off'))
])
]);
if (!webUi) {
const reason = !status.initialized ? _('Complete initial setup before opening Doona.') :
!running ? _('Start Honk to open Doona.') : _('Doona is not ready yet. Check the service status.');
statusCard.appendChild(E('p', { 'class': 'honk-note', 'role': 'status' }, reason));
}
if (status.clash_api_enabled && running && !clashUi)
statusCard.appendChild(E('p', { 'class': 'honk-note', 'role': 'status' }, status.clash_api_ready ? _('Zashboard is still downloading or unavailable.') : _('The Clash API listener is unavailable. Check the service logs.')));
const errors = (Array.isArray(status.errors) ? status.errors : []).filter(function(token) {
return token !== 'disabled' && token !== 'not_initialized' && token !== 'service_not_running';
});
if (errors.length)
statusCard.appendChild(E('ul', { 'class': 'alert-message' }, errors.map(function(token) {
return E('li', {}, honk.statusIssue(token));
})));
statusArea.appendChild(statusCard);
statusArea.appendChild(renderServiceSettings(status));
if (focusedAction) {
const replacement = statusArea.querySelector('[data-action="%s"]'.format(focusedAction));
if (replacement)
replacement.focus();
}
restoreFields(statusArea, fields);
}
function renderServiceSettings(status) {
const card = E('section', { 'class': 'honk-card' });
const form = E('form');
const networkSelect = E('select', { 'class': 'cbi-input-select', 'name': 'lan_network', 'id': 'honk-lan-network' });
const portInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'listen_port', 'id': 'honk-listen-port', 'min': '1024', 'max': '65535', 'step': '1' });
const bootEnabledInput = E('input', { 'type': 'checkbox', 'name': 'boot_enabled', 'id': 'honk-boot-enabled' });
const clashEnabledInput = E('input', { 'type': 'checkbox', 'name': 'clash_api_enabled', 'id': 'honk-clash-enabled' });
const clashPortInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'clash_api_port', 'id': 'honk-clash-port', 'min': '1024', 'max': '65535', 'step': '1' });
const clashSecretInput = E('input', { 'class': 'cbi-input-password', 'type': 'password', 'name': 'clash_api_secret', 'id': 'honk-clash-secret', 'autocomplete': 'new-password', 'readonly': 'readonly' });
clashSecretInput.addEventListener('focus', function() { clashSecretInput.removeAttribute('readonly'); });
const message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const save = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Save service settings'));
(data.networks || []).forEach(function(name) {
networkSelect.appendChild(E('option', { 'value': name }, name));
});
if (status.lan_network && !(data.networks || []).includes(status.lan_network))
networkSelect.appendChild(E('option', { 'value': status.lan_network }, status.lan_network));
if (status.lan_network)
networkSelect.value = status.lan_network;
portInput.value = status.listen_port ? String(status.listen_port) : '9527';
bootEnabledInput.checked = status.boot_enabled === true;
clashEnabledInput.checked = status.clash_api_enabled === true;
clashPortInput.value = status.clash_api_port ? String(status.clash_api_port) : '9090';
card.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Service settings')));
if (!(data.networks || []).length)
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
form.appendChild(field(_('LAN network'), networkSelect));
form.appendChild(field(_('Listen port'), portInput));
form.appendChild(field(_('Start Honk at boot'), bootEnabledInput));
form.appendChild(field(_('Enable Clash API and Zashboard'), clashEnabledInput));
form.appendChild(field(_('Clash API port'), clashPortInput));
form.appendChild(field(_('Clash API secret'), clashSecretInput, status.clash_api_secret_set ? _('A secret is set. Leave blank to keep it.') : _('Required when enabling. Use 6–128 letters, numbers, dots, underscores or hyphens.')));
form.appendChild(E('p', { 'class': 'honk-note' }, _('Use your existing HTTPS reverse proxy for external access. Enter the secret in Zashboard when connecting.')));
form.appendChild(E('div', { 'class': 'cbi-page-actions' }, save));
form.appendChild(message);
form.addEventListener('submit', function(ev) {
ev.preventDefault();
const port = Number(portInput.value);
const clashPort = Number(clashPortInput.value);
const clashSecret = clashSecretInput.value;
if (!networkSelect.value || !Number.isInteger(port) || port < 1024 || port > 65535) {
message.textContent = _('Choose a LAN network and a port from 1024 to 65535.');
return;
}
if (!Number.isInteger(clashPort) || clashPort < 1024 || clashPort > 65535) {
message.textContent = _('Clash API port must be between 1024 and 65535.');
return;
}
if (clashEnabledInput.checked && clashPort === port) {
message.textContent = _('The Clash API port must differ from the listen port.');
return;
}
if (clashEnabledInput.checked && !status.clash_api_secret_set && !clashSecret) {
message.textContent = _('A secret is required when enabling Clash API.');
return;
}
if (clashEnabledInput.checked && clashSecret && !/^[A-Za-z0-9._-]{6,128}$/.test(clashSecret)) {
message.textContent = _('The secret must be 6-128 letters, numbers, dots, underscores or hyphens.');
return;
}
save.disabled = true;
settingsBusy = true;
message.textContent = _('Saving…');
honk.settings(networkSelect.value, port, bootEnabledInput.checked, clashEnabledInput.checked, clashPort, clashSecret).then(honk.ensureOk).then(function(result) {
clashSecretInput.value = '';
message.textContent = honk.resultMessage(result, _('Service settings saved.'));
}).catch(function(error) {
message.textContent = honk.errorMessage(error, _('Settings could not be applied.'));
}).finally(function() {
settingsBusy = false;
save.disabled = false;
refresh();
});
});
card.appendChild(form);
return card;
}
function renderSetup(status) {
if (setupBusy)
return;
if (!status)
return;
const initialized = status.initialized === true;
if (setupState === initialized)
return;
setupState = initialized;
const fields = captureFields(setupArea);
while (setupArea.firstChild)
setupArea.removeChild(setupArea.firstChild);
if (initialized)
return;
const form = E('form', { 'class': 'honk-card' });
const username = E('input', { 'type': 'text', 'name': 'username', 'id': 'honk-init-username', 'autocomplete': 'username', 'required': true, 'class': 'cbi-input-text' });
const password = E('input', { 'type': 'password', 'name': 'password', 'id': 'honk-init-password', 'autocomplete': 'new-password', 'required': true, 'class': 'cbi-input-password' });
const message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const submit = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Initialize Honk'));
form.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Initial setup')));
form.appendChild(E('p', { 'class': 'honk-note' }, _('Create the Honk administrator account. The password is sent only for this request and is not saved by this page.')));
form.appendChild(field(_('Username'), username));
form.appendChild(field(_('Password'), password));
form.appendChild(E('div', { 'class': 'cbi-page-actions' }, submit));
form.appendChild(message);
form.addEventListener('submit', function(ev) {
ev.preventDefault();
const user = username.value.trim();
const secret = password.value;
if (!user || !secret) {
message.textContent = _('Enter both a username and password.');
return;
}
submit.disabled = true;
setupBusy = true;
message.textContent = _('Initializing…');
honk.initialize(user, secret).then(honk.ensureOk).then(function(result) {
username.value = '';
password.value = '';
message.textContent = honk.resultMessage(result, _('Honk initialized.'));
return honk.status();
}).then(function(updatedStatus) {
setupBusy = false;
currentStatus = updatedStatus;
renderSetup(updatedStatus);
renderStatus(updatedStatus, null);
}).catch(function() {
password.value = '';
message.textContent = _('Initialization failed. Check the system status.');
}).finally(function() {
setupBusy = false;
submit.disabled = false;
});
});
setupArea.appendChild(form);
restoreFields(setupArea, fields);
}
function refresh() {
if (busy || setupBusy || settingsBusy)
return Promise.resolve();
return honk.status().then(function(status) {
renderStatus(status, null);
renderSetup(status);
}).catch(function() {
renderStatus(null, true);
});
}
function runAction(method) {
if (busy)
return;
busy = true;
actionMessage.textContent = _('Working…');
renderStatus(currentStatus, null);
honk[method]().then(honk.ensureOk).then(function(result) {
actionMessage.textContent = honk.resultMessage(result, _('Operation completed.'));
}).catch(function(error) {
actionMessage.textContent = honk.errorMessage(error, _('Operation failed. Check Honk status.'));
}).finally(function() {
busy = false;
refresh();
});
}
page.appendChild(E('div', { 'class': 'honk-header' }, [
E('h2', {}, _('Honk')),
E('p', { 'class': 'honk-header-sub' }, _('Transparent proxy core for OpenWrt. Start, stop and manage the Honk service.'))
]));
page.appendChild(statusArea);
page.appendChild(actionMessage);
page.appendChild(setupArea);
renderStatus(data.status, data.error);
if (data.status)
renderSetup(data.status);
poll.add(refresh, 5);
return page;
}
});
@@ -1,338 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require baseclass';
'require rpc';
const callStatus = rpc.declare({
object: 'honk',
method: 'status',
expect: { '': {} }
});
const callCheck = rpc.declare({
object: 'honk',
method: 'check',
expect: { '': {} }
});
const callInitialize = rpc.declare({
object: 'honk',
method: 'initialize',
params: [ 'username', 'password' ],
expect: { '': {} }
});
const callStart = rpc.declare({ object: 'honk', method: 'start', expect: { '': {} } });
const callStop = rpc.declare({ object: 'honk', method: 'stop', expect: { '': {} } });
const callRestart = rpc.declare({ object: 'honk', method: 'restart', expect: { '': {} } });
const callSettings = rpc.declare({
object: 'honk',
method: 'settings',
params: [ 'lan_network', 'listen_port', 'boot_enabled', 'clash_api_enabled', 'clash_api_port', 'clash_api_secret' ],
expect: { '': {} }
});
const callAddSubscription = rpc.declare({
object: 'honk',
method: 'add_subscription',
params: [ 'name', 'url' ],
expect: { '': {} }
});
const callAddNodes = rpc.declare({
object: 'honk',
method: 'add_nodes',
params: [ 'links' ],
expect: { '': {} }
});
const callImportDae = rpc.declare({
object: 'honk',
method: 'import_dae',
expect: { '': {} }
});
const callRepair = rpc.declare({ object: 'honk', method: 'repair', expect: { '': {} } });
const callBackup = rpc.declare({ object: 'honk', method: 'backup', expect: { '': {} } });
const callRestore = rpc.declare({ object: 'honk', method: 'restore', expect: { '': {} } });
const callRestorePrepare = rpc.declare({ object: 'honk', method: 'restore_prepare', expect: { '': {} } });
const callReset = rpc.declare({ object: 'honk', method: 'reset', expect: { '': {} } });
const callJobStatus = rpc.declare({
object: 'honk',
method: 'job_status',
params: [ 'job_id' ],
expect: { '': {} }
});
const callLogs = rpc.declare({
object: 'honk',
method: 'logs',
params: [ 'source', 'level', 'limit' ],
expect: { '': {} }
});
const callUpdateCheck = rpc.declare({ object: 'honk', method: 'update_check', expect: { '': {} } });
const callUpdateApply = rpc.declare({ object: 'honk', method: 'update_apply', expect: { '': {} } });
let pageStylesInstalled = false;
function installStyles() {
if (pageStylesInstalled || document.getElementById('honk-page-styles'))
return;
const stylesheet = document.createElement('link');
stylesheet.id = 'honk-page-styles';
stylesheet.rel = 'stylesheet';
stylesheet.href = L.resource('view/honk/honk.css');
document.head.appendChild(stylesheet);
pageStylesInstalled = true;
}
function localWebUi(status) {
if (!status || !status.initialized || !status.running || !status.api_ready || !status.api_url)
return null;
try {
const raw = String(status.api_url);
const authority = raw.match(/^https?:\/\/([^/?#]+)/i);
if (!authority || authority[1].indexOf('@') >= 0)
return null;
const portMatch = authority[1].match(/:(\d+)$/);
const url = new URL(raw);
const hostname = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
const port = portMatch ? Number(portMatch[1]) : 0;
if (!portMatch || !Number.isInteger(port) || port < 1 || port > 65535 ||
url.protocol !== 'http:' && url.protocol !== 'https:' ||
url.username || url.password || url.search || url.hash)
return null;
const octets = hostname.split('.');
let localIPv4 = false;
if (octets.length === 4 && octets.every(function(part) { return /^\d{1,3}$/.test(part) && Number(part) <= 255; })) {
const ip = octets.map(Number);
localIPv4 = ip[0] === 10 || ip[0] === 127 || ip[0] === 192 && ip[1] === 168 ||
ip[0] === 172 && ip[1] >= 16 && ip[1] <= 31 || ip[0] === 169 && ip[1] === 254;
}
const localIPv6 = hostname === '::1' || /^f[cd][0-9a-f:]*$/.test(hostname) || /^fe[89ab][0-9a-f:]*$/.test(hostname);
if (!localIPv4 && !localIPv6)
return null;
return url.origin.replace(/\/$/, '') + '/ui/';
}
catch (e) {
return null;
}
}
function localClashUi(status, secret) {
if (!status || !status.clash_api_enabled || !status.clash_api_ready || !status.clash_ui_ready)
return null;
const base = localWebUi(Object.assign({}, status, { api_url: status.clash_api_url, api_ready: true }));
if (!base)
return null;
try {
const url = new URL(status.clash_api_url);
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
const port = status.clash_api_port || url.port;
const query = [
'host=' + encodeURIComponent(host),
'hostname=' + encodeURIComponent(host),
'port=' + encodeURIComponent(port),
'secret=' + encodeURIComponent(secret || '')
].join('&');
return base + '?' + query + '#/proxies';
}
catch (e) {
return null;
}
}
function tokenMessage(token) {
const messages = {
disabled: _('Honk is disabled.'),
not_initialized: _('Complete initial setup before starting Honk.'),
service_not_running: _('Honk is enabled but currently stopped.'),
api_unavailable: _('Honk is running, but its management interface is not ready.'),
operation_in_progress: _('Another Honk operation is in progress.'),
stale_operation_lock: _('A previous operation did not finish cleanly.'),
started: _('Honk started.'),
already_running: _('Honk is already running.'),
stopped: _('Honk stopped.'),
restarted: _('Honk restarted.'),
settings_saved: _('Settings saved and applied.'),
initialized: _('Honk initialized.'),
administrator_initialized: _('Honk initialized.'),
system_config_healthy: _('System check passed.'),
system_config_repaired: _('Repair completed.'),
backup_created: _('Backup created.'),
restored: _('Backup restored.'),
data_reset: _('Honk data reset. The service is stopped.'),
worker_lost: _('The maintenance worker stopped unexpectedly; check system recovery status before retrying.'),
operation_failed: _('The maintenance operation failed.'),
job_timeout: _('The maintenance operation timed out.'),
maintenance_recovery_required: _('Recovery is required. Run system recovery before starting another maintenance operation.'),
update_recovery_required: _('The signed update did not finish. Keep Honk stopped and reinstall a complete verified package set before retrying.'),
rollback_tuple_unavailable: _('A compatible update is available, but a complete verified rollback package set is not available yet.'),
trusted_feed_unavailable: _('No trusted update source is configured.'),
manifest_incompatible: _('The signed update manifest does not match this device.'),
signed_manifest_unavailable: _('The signed update manifest could not be fetched or verified.'),
update_available: _('A compatible update is available.'),
no_update: _('No compatible update is available.'),
already_current: _('Honk is already up to date.'),
update_apply_failed: _('The update could not be installed.'),
update_check_failed: _('The signed update could not be verified for this device.'),
signed_manifest_missing: _('Check for updates again to refresh the signed manifest.'),
signed_manifest_expired: _('The signed manifest expired. Check for updates again.'),
reset_failed: _('Reset failed; the previous service state was restored.'),
reset_failed_rollback_failed: _('Reset failed and automatic recovery did not finish. Run system recovery before retrying.'),
restore_failed_rollback_failed: _('Restore failed and automatic recovery did not finish. Run system recovery before retrying.'),
logs_unavailable: _('Honk logs are unavailable.'),
unsupported_core: _('This installed core does not support the requested import operation.'),
invalid_request: _('The import request is incomplete or invalid.'),
import_failed: _('The core could not prepare this import preview.'),
share_links_too_large: _('Share links exceed the 16 KiB per request limit.'),
share_links_invalid_or_conflicting: _('A share link is invalid or conflicts with an existing node. Review the input and try again.'),
subscription_invalid: _('The subscription name or URL is invalid.'),
subscription_invalid_or_conflicting: _('The subscription is invalid or its name already exists.'),
subscription_no_nodes: _('The subscription returned no usable nodes. The configuration was not changed.'),
subscription_added: _('Subscription saved with usable nodes.'),
nodes_added: _('Nodes saved.'),
reload_failed_rolled_back: _('Honk could not restart. The previous configuration and service were restored.'),
reload_and_rollback_failed: _('Honk could not restart or restore the previous service. Check its status and backup.'),
dae_imported: _('Configuration imported and Honk reloaded.'),
config_missing: _('The configuration file is missing.'),
config_write_failed: _('Could not write the configuration file.'),
dae_too_large: _('The dae file exceeds the 2 MiB limit.'),
dae_invalid_or_unsupported: _('The dae file contains invalid sections or unsupported includes.'),
candidate_invalid: _('Honk could not load the proposed configuration.'),
candidate_rejected: _('The imported configuration failed core validation.'),
source_changed: _('Honk or Doona changed the configuration during this operation. Try again.'),
preview_expired: _('The preview expired. Create a new preview.'),
preview_corrupt: _('The saved preview is damaged. Create a new preview.'),
dae_upload_unavailable: _('The dae upload is missing or unavailable. Upload the file again.'),
dae_upload_invalid: _('The uploaded dae file is invalid.'),
dae_upload_changed: _('The uploaded dae file changed. Upload it again.'),
upload_unavailable: _('A private upload location could not be prepared.'),
settings_write_failed: _('Could not save Honk settings.'),
curl_missing: _('The curl command is missing. Install the curl package and try again.'),
start_failed: _('Honk could not start.'),
stop_failed: _('Honk could not stop.'),
restart_failed: _('Honk could not restart.'),
managed_service_unhealthy: _('The managed Honk service is not healthy.'),
unmanaged_core_running: _('An unmanaged Honk core process is running.'),
invalid_lan_network: _('The selected LAN network is invalid or unavailable.'),
lan_network_unavailable: _('The selected LAN network is unavailable.'),
listen_port_occupied: _('The listen port is already in use.'),
clash_api_port_occupied: _('The Clash API port is already in use.'),
invalid_clash_api_settings: _('Enter a different Clash API port and a secret of 6–128 letters, numbers, dots, underscores or hyphens.'),
invalid_listen_port: _('The listen port is invalid.'),
initialize_before_enabling_service: _('Complete initial setup before enabling Honk.'),
system_config_unavailable: _('Honk system configuration is unavailable.'),
package_config_unavailable: _('Honk configuration is unavailable.'),
service_apply_failed_rolled_back: _('Settings could not be applied. The previous settings were restored.')
};
return messages[token] || null;
}
function statusIssue(token) {
return tokenMessage(token) || _('Honk reported a system issue.');
}
function resultMessage(result, fallback) {
return result && tokenMessage(result.message) || fallback;
}
function errorMessage(error, fallback) {
return error && tokenMessage(error.message) || fallback;
}
function jobPhaseMessage(phase) {
const messages = {
queued: _('Waiting for the operation to start…'),
checking_feed: _('Checking the signed update source…'),
downloading: _('Downloading the signed package set…'),
preparing: _('Preparing configuration apply…'),
validating_candidate: _('Validating the candidate in an isolated runtime…'),
stopping: _('Stopping Honk safely…'),
applying: _('Applying the reviewed configuration…'),
validating: _('Checking the updated configuration…'),
starting: _('Restoring the previous service state…'),
rolling_back: _('Restoring the previous configuration…'),
committing: _('Finishing the transaction…'),
recovery: _('Recovery is required before another operation.')
};
return messages[phase] || _('Applying configuration…');
}
function ensureOk(result) {
if (!result || result.ok !== true)
throw new Error(result && result.message ? result.message : _('The backend did not confirm the operation.'));
return result;
}
function waitJob(jobId, onProgress) {
if (!/^[0-9a-f]{32}$/.test(jobId || ''))
return Promise.reject(new Error('invalid_job_id'));
let attempts = 0;
return new Promise(function(resolve, reject) {
function poll() {
callJobStatus(jobId).then(function(result) {
if (!result || typeof result.state !== 'string')
throw new Error('invalid_job_status');
if (onProgress)
onProgress(result);
if (result.state === 'succeeded') {
resolve(result);
return;
}
if (result.state === 'failed' || result.state === 'rollback_required') {
const error = new Error(result.state === 'rollback_required' ? 'maintenance_recovery_required' : result.message || 'operation_failed');
error.recoveryRequired = result.state === 'rollback_required';
reject(error);
return;
}
if (result.state !== 'queued' && result.state !== 'running') {
reject(new Error('invalid_job_status'));
return;
}
if (++attempts >= 1800) {
reject(new Error('job_timeout'));
return;
}
setTimeout(poll, 1000);
}).catch(reject);
}
poll();
});
}
return baseclass.extend({
status: callStatus,
check: callCheck,
initialize: callInitialize,
start: callStart,
stop: callStop,
restart: callRestart,
settings: callSettings,
addSubscription: callAddSubscription,
addNodes: callAddNodes,
importDae: callImportDae,
repair: callRepair,
backup: callBackup,
restore: callRestore,
restorePrepare: callRestorePrepare,
reset: callReset,
logs: callLogs,
updateCheck: callUpdateCheck,
updateApply: callUpdateApply,
waitJob: waitJob,
ensureOk: ensureOk,
localWebUi: localWebUi,
localClashUi: localClashUi,
statusIssue: statusIssue,
resultMessage: resultMessage,
errorMessage: errorMessage,
jobPhaseMessage: jobPhaseMessage,
installStyles: installStyles
});
@@ -1,133 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require network';
'require view';
'require view.honk.rpc as honk';
function getNetworkNames(networks) {
if (!networks)
return [];
const list = Array.isArray(networks) ? networks : Object.keys(networks).map(function(key) { return networks[key]; });
return list.map(function(net) {
if (net && typeof net.getName === 'function')
return net.getName();
if (net && typeof net.name === 'string')
return net.name;
return typeof net === 'string' ? net : '';
}).filter(function(name, index, names) {
return !!name && names.indexOf(name) === index;
}).sort();
}
function field(label, input) {
return E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title', 'for': input.id }, label),
E('div', { 'class': 'cbi-value-field' }, input)
]);
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
load: function() {
return Promise.all([
honk.status().then(function(status) { return { status: status }; }).catch(function(error) { return { statusError: error.message || String(error) }; }),
network.getNetworks().then(getNetworkNames).then(function(names) { return { networks: names }; }).catch(function(error) { return { networkError: error.message || String(error), networks: [] }; })
]).then(function(results) {
return Object.assign({}, results[0], results[1]);
});
},
render: function(data) {
honk.installStyles();
const page = E('div', { 'class': 'cbi-map honk-page' });
const section = E('form', { 'class': 'honk-card' });
const networkSelect = E('select', { 'class': 'cbi-input-select', 'name': 'lan_network', 'id': 'honk-lan-network' });
const portInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'listen_port', 'id': 'honk-listen-port', 'min': '1024', 'max': '65535', 'step': '1' });
const bootEnabledInput = E('input', { 'type': 'checkbox', 'name': 'boot_enabled', 'id': 'honk-boot-enabled' });
const clashEnabledInput = E('input', { 'type': 'checkbox', 'name': 'clash_api_enabled', 'id': 'honk-clash-enabled' });
const clashPortInput = E('input', { 'class': 'cbi-input-text', 'type': 'number', 'name': 'clash_api_port', 'id': 'honk-clash-port', 'min': '1024', 'max': '65535', 'step': '1' });
const clashSecretInput = E('input', { 'class': 'cbi-input-password', 'type': 'password', 'name': 'clash_api_secret', 'id': 'honk-clash-secret', 'autocomplete': 'new-password', 'readonly': 'readonly' });
clashSecretInput.addEventListener('focus', function() { clashSecretInput.removeAttribute('readonly'); });
const message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const save = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Save settings'));
save.disabled = !!data.statusError;
(data.networks || []).forEach(function(name) {
networkSelect.appendChild(E('option', { 'value': name }, name));
});
if (data.status && data.status.lan_network && !(data.networks || []).includes(data.status.lan_network))
networkSelect.appendChild(E('option', { 'value': data.status.lan_network }, data.status.lan_network));
if (data.status && data.status.lan_network)
networkSelect.value = data.status.lan_network;
portInput.value = data.status && data.status.listen_port ? String(data.status.listen_port) : '9527';
bootEnabledInput.checked = !!(data.status && data.status.boot_enabled === true);
clashEnabledInput.checked = !!(data.status && data.status.clash_api_enabled === true);
clashPortInput.value = data.status && data.status.clash_api_port ? String(data.status.clash_api_port) : '9090';
section.appendChild(E('h3', { 'class': 'honk-card-title' }, _('Honk settings')));
if (data.statusError)
section.appendChild(E('div', { 'class': 'alert-message' }, _('Could not read current settings: %s').format(data.statusError)));
if (data.networkError)
section.appendChild(E('div', { 'class': 'alert-message' }, _('Could not load LAN networks: %s').format(data.networkError)));
if (!(data.networks || []).length)
networkSelect.appendChild(E('option', { 'value': '' }, _('No network interfaces found')));
section.appendChild(field(_('LAN network'), networkSelect));
section.appendChild(field(_('Listen port'), portInput));
section.appendChild(field(_('Start Honk at boot'), bootEnabledInput));
section.appendChild(field(_('Enable Clash API and Zashboard'), clashEnabledInput));
section.appendChild(field(_('Clash API port'), clashPortInput));
section.appendChild(field(_('Clash API secret'), clashSecretInput));
section.appendChild(E('p', { 'class': 'honk-note' }, data.status && data.status.clash_api_secret_set ? _('A secret is set. Leave blank to keep it. Use your existing HTTPS reverse proxy for external access.') : _('A secret is required when enabling Clash API. Use 6–128 letters, numbers, dots, underscores or hyphens.')));
section.appendChild(E('div', { 'class': 'cbi-page-actions' }, save));
section.appendChild(message);
section.addEventListener('submit', function(ev) {
ev.preventDefault();
const port = Number(portInput.value);
const clashPort = Number(clashPortInput.value);
const clashSecret = clashSecretInput.value;
if (!networkSelect.value || !Number.isInteger(port) || port < 1024 || port > 65535) {
message.textContent = _('Choose a LAN network and a port from 1024 to 65535.');
return;
}
if (!Number.isInteger(clashPort) || clashPort < 1024 || clashPort > 65535) {
message.textContent = _('Clash API port must be between 1024 and 65535.');
return;
}
if (clashEnabledInput.checked && clashPort === port) {
message.textContent = _('The Clash API port must differ from the listen port.');
return;
}
if (clashEnabledInput.checked && !(data.status && data.status.clash_api_secret_set) && !clashSecret) {
message.textContent = _('A secret is required when enabling Clash API.');
return;
}
if (clashEnabledInput.checked && clashSecret && !/^[A-Za-z0-9._-]{6,128}$/.test(clashSecret)) {
message.textContent = _('The secret must be 6-128 letters, numbers, dots, underscores or hyphens.');
return;
}
save.disabled = true;
message.textContent = _('Saving…');
honk.settings(networkSelect.value, port, bootEnabledInput.checked, clashEnabledInput.checked, clashPort, clashSecret).then(honk.ensureOk).then(function(result) {
clashSecretInput.value = '';
message.textContent = honk.resultMessage(result, _('Settings saved and applied.'));
}).catch(function(error) {
message.textContent = honk.errorMessage(error, _('Save failed. Check the settings and system status.'));
}).finally(function() {
save.disabled = false;
});
});
page.appendChild(E('div', { 'class': 'honk-header' }, [
E('h2', {}, _('Settings')),
E('p', { 'class': 'honk-header-sub' }, _('Network interface, listen port and boot behaviour for the Honk service.'))
]));
page.appendChild(section);
return page;
}
});
@@ -1,72 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require baseclass';
function sha256(input) {
const bytes = new Uint8Array(input);
const bitLength = bytes.length * 8;
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
const padded = new Uint8Array(paddedLength);
padded.set(bytes);
padded[bytes.length] = 0x80;
const lowBits = bitLength >>> 0;
const highBits = Math.floor(bitLength / 0x100000000) >>> 0;
for (let i = 0; i < 4; i++) {
padded[paddedLength - 8 + i] = highBits >>> (24 - i * 8);
padded[paddedLength - 4 + i] = lowBits >>> (24 - i * 8);
}
const constants = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
const state = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ];
const words = new Uint32Array(64);
const rotateRight = function(value, count) { return (value >>> count) | (value << (32 - count)); };
for (let offset = 0; offset < paddedLength; offset += 64) {
for (let i = 0; i < 16; i++) {
const at = offset + i * 4;
words[i] = ((padded[at] << 24) | (padded[at + 1] << 16) | (padded[at + 2] << 8) | padded[at + 3]) >>> 0;
}
for (let i = 16; i < 64; i++) {
const x = words[i - 15];
const y = words[i - 2];
const sigma0 = rotateRight(x, 7) ^ rotateRight(x, 18) ^ (x >>> 3);
const sigma1 = rotateRight(y, 17) ^ rotateRight(y, 19) ^ (y >>> 10);
words[i] = (words[i - 16] + sigma0 + words[i - 7] + sigma1) >>> 0;
}
let a = state[0], b = state[1], c = state[2], d = state[3];
let e = state[4], f = state[5], g = state[6], h = state[7];
for (let i = 0; i < 64; i++) {
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
const choice = (e & f) ^ (~e & g);
const temp1 = (h + sum1 + choice + constants[i] + words[i]) >>> 0;
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
const majority = (a & b) ^ (a & c) ^ (b & c);
const temp2 = (sum0 + majority) >>> 0;
h = g; g = f; f = e; e = (d + temp1) >>> 0;
d = c; c = b; b = a; a = (temp1 + temp2) >>> 0;
}
state[0] = (state[0] + a) >>> 0;
state[1] = (state[1] + b) >>> 0;
state[2] = (state[2] + c) >>> 0;
state[3] = (state[3] + d) >>> 0;
state[4] = (state[4] + e) >>> 0;
state[5] = (state[5] + f) >>> 0;
state[6] = (state[6] + g) >>> 0;
state[7] = (state[7] + h) >>> 0;
}
return state.map(function(value) { return value.toString(16).padStart(8, '0'); }).join('');
}
return baseclass.extend({ sha256: sha256 });
@@ -1,21 +0,0 @@
(The MIT License)
Copyright (C) 2011-2015 by Vitaly Puzrin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -1,33 +0,0 @@
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.jsyaml={}))})(this,function(e){Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var t=Object.create,n=Object.defineProperty,r=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,a=Object.getPrototypeOf,o=Object.prototype.hasOwnProperty,s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,t,a,s)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var c=i(t),l=0,u=c.length,d;l<u;l++)d=c[l],!o.call(e,d)&&d!==a&&n(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(s=r(t,d))||s.enumerable});return e},l=(e,r,i)=>(i=e==null?{}:t(a(e)),c(r||!e||!e.__esModule?n(i,`default`,{value:e,enumerable:!0}):i,e)),u=s(((e,t)=>{function n(e){return e==null}function r(e){return typeof e==`object`&&!!e}function i(e){return Array.isArray(e)?e:n(e)?[]:[e]}function a(e,t){if(t){let n=Object.keys(t);for(let r=0,i=n.length;r<i;r+=1){let i=n[r];e[i]=t[i]}}return e}function o(e,t){let n=``;for(let r=0;r<t;r+=1)n+=e;return n}function s(e){return e===0&&1/e==-1/0}t.exports.isNothing=n,t.exports.isObject=r,t.exports.toArray=i,t.exports.repeat=o,t.exports.isNegativeZero=s,t.exports.extend=a})),d=s(((e,t)=>{function n(e,t){let n=``,r=e.reason||`(unknown reason)`;return e.mark?(e.mark.name&&(n+=`in "`+e.mark.name+`" `),n+=`(`+(e.mark.line+1)+`:`+(e.mark.column+1)+`)`,!t&&e.mark.snippet&&(n+=`
`+e.mark.snippet),r+` `+n):r}function r(e,t){Error.call(this),this.name=`YAMLException`,this.reason=e,this.mark=t,this.message=n(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=Error().stack||``}r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r.prototype.toString=function(e){return this.name+`: `+n(this,e)},t.exports=r})),f=s(((e,t)=>{var n=u();function r(e,t,n,r,i){let a=``,o=``,s=Math.floor(i/2)-1;return r-t>s&&(a=` ... `,t=r-s+a.length),n-r>s&&(o=` ...`,n=r+s-o.length),{str:a+e.slice(t,n).replace(/\t/g,`→`)+o,pos:r-t+a.length}}function i(e,t){return n.repeat(` `,t-e.length)+e}function a(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),typeof t.indent!=`number`&&(t.indent=1),typeof t.linesBefore!=`number`&&(t.linesBefore=3),typeof t.linesAfter!=`number`&&(t.linesAfter=2);let a=/\r?\n|\r|\0/g,o=[0],s=[],c,l=-1;for(;c=a.exec(e.buffer);)s.push(c.index),o.push(c.index+c[0].length),e.position<=c.index&&l<0&&(l=o.length-2);l<0&&(l=o.length-1);let u=``,d=Math.min(e.line+t.linesAfter,s.length).toString().length,f=t.maxLength-(t.indent+d+3);for(let a=1;a<=t.linesBefore&&!(l-a<0);a++){let c=r(e.buffer,o[l-a],s[l-a],e.position-(o[l]-o[l-a]),f);u=n.repeat(` `,t.indent)+i((e.line-a+1).toString(),d)+` | `+c.str+`
`+u}let p=r(e.buffer,o[l],s[l],e.position,f);u+=n.repeat(` `,t.indent)+i((e.line+1).toString(),d)+` | `+p.str+`
`,u+=n.repeat(`-`,t.indent+d+3+p.pos)+`^
`;for(let a=1;a<=t.linesAfter&&!(l+a>=s.length);a++){let c=r(e.buffer,o[l+a],s[l+a],e.position-(o[l]-o[l+a]),f);u+=n.repeat(` `,t.indent)+i((e.line+a+1).toString(),d)+` | `+c.str+`
`}return u.replace(/\n$/,``)}t.exports=a})),p=s(((e,t)=>{var n=d(),r=[`kind`,`multi`,`resolve`,`construct`,`instanceOf`,`predicate`,`represent`,`representName`,`defaultStyle`,`styleAliases`],i=[`scalar`,`sequence`,`mapping`];function a(e){let t={};return e!==null&&Object.keys(e).forEach(function(n){e[n].forEach(function(e){t[String(e)]=n})}),t}function o(e,t){if(t=t||{},Object.keys(t).forEach(function(t){if(r.indexOf(t)===-1)throw new n(`Unknown option "`+t+`" is met in definition of "`+e+`" YAML type.`)}),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=a(t.styleAliases||null),i.indexOf(this.kind)===-1)throw new n(`Unknown kind "`+this.kind+`" is specified for "`+e+`" YAML type.`)}t.exports=o})),m=s(((e,t)=>{var n=d(),r=p();function i(e,t){let n=[];return e[t].forEach(function(e){let t=n.length;n.forEach(function(n,r){n.tag===e.tag&&n.kind===e.kind&&n.multi===e.multi&&(t=r)}),n[t]=e}),n}function a(){let e={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function t(t){t.multi?(e.multi[t.kind].push(t),e.multi.fallback.push(t)):e[t.kind][t.tag]=e.fallback[t.tag]=t}for(let e=0,n=arguments.length;e<n;e+=1)arguments[e].forEach(t);return e}function o(e){return this.extend(e)}o.prototype.extend=function(e){let t=[],s=[];if(e instanceof r)s.push(e);else if(Array.isArray(e))s=s.concat(e);else if(e&&(Array.isArray(e.implicit)||Array.isArray(e.explicit)))e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(s=s.concat(e.explicit));else throw new n(`Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })`);t.forEach(function(e){if(!(e instanceof r))throw new n(`Specified list of YAML types (or a single Type object) contains a non-Type object.`);if(e.loadKind&&e.loadKind!==`scalar`)throw new n(`There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.`);if(e.multi)throw new n(`There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.`)}),s.forEach(function(e){if(!(e instanceof r))throw new n(`Specified list of YAML types (or a single Type object) contains a non-Type object.`)});let c=Object.create(o.prototype);return c.implicit=(this.implicit||[]).concat(t),c.explicit=(this.explicit||[]).concat(s),c.compiledImplicit=i(c,`implicit`),c.compiledExplicit=i(c,`explicit`),c.compiledTypeMap=a(c.compiledImplicit,c.compiledExplicit),c},t.exports=o})),h=s(((e,t)=>{t.exports=new(p())(`tag:yaml.org,2002:str`,{kind:`scalar`,construct:function(e){return e===null?``:e}})})),g=s(((e,t)=>{t.exports=new(p())(`tag:yaml.org,2002:seq`,{kind:`sequence`,construct:function(e){return e===null?[]:e}})})),_=s(((e,t)=>{t.exports=new(p())(`tag:yaml.org,2002:map`,{kind:`mapping`,construct:function(e){return e===null?{}:e}})})),v=s(((e,t)=>{t.exports=new(m())({explicit:[h(),g(),_()]})})),y=s(((e,t)=>{var n=p();function r(e){if(e===null)return!0;let t=e.length;return t===1&&e===`~`||t===4&&(e===`null`||e===`Null`||e===`NULL`)}function i(){return null}function a(e){return e===null}t.exports=new n(`tag:yaml.org,2002:null`,{kind:`scalar`,resolve:r,construct:i,predicate:a,represent:{canonical:function(){return`~`},lowercase:function(){return`null`},uppercase:function(){return`NULL`},camelcase:function(){return`Null`},empty:function(){return``}},defaultStyle:`lowercase`})})),b=s(((e,t)=>{var n=p();function r(e){if(e===null)return!1;let t=e.length;return t===4&&(e===`true`||e===`True`||e===`TRUE`)||t===5&&(e===`false`||e===`False`||e===`FALSE`)}function i(e){return e===`true`||e===`True`||e===`TRUE`}function a(e){return Object.prototype.toString.call(e)===`[object Boolean]`}t.exports=new n(`tag:yaml.org,2002:bool`,{kind:`scalar`,resolve:r,construct:i,predicate:a,represent:{lowercase:function(e){return e?`true`:`false`},uppercase:function(e){return e?`TRUE`:`FALSE`},camelcase:function(e){return e?`True`:`False`}},defaultStyle:`lowercase`})})),x=s(((e,t)=>{var n=u(),r=p();function i(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function a(e){return e>=48&&e<=55}function o(e){return e>=48&&e<=57}function s(e){if(e===null)return!1;let t=e.length,n=0,r=!1;if(!t)return!1;let s=e[n];if((s===`-`||s===`+`)&&(s=e[++n]),s===`0`){if(n+1===t)return!0;if(s=e[++n],s===`b`){for(n++;n<t;n++){if(s=e[n],s!==`0`&&s!==`1`)return!1;r=!0}return r&&Number.isFinite(c(e))}if(s===`x`){for(n++;n<t;n++){if(!i(e.charCodeAt(n)))return!1;r=!0}return r&&Number.isFinite(c(e))}if(s===`o`){for(n++;n<t;n++){if(!a(e.charCodeAt(n)))return!1;r=!0}return r&&Number.isFinite(c(e))}}for(;n<t;n++){if(!o(e.charCodeAt(n)))return!1;r=!0}return r?Number.isFinite(c(e)):!1}function c(e){let t=e Line truncated
\r`;function i(e){if(e===null)return!1;let t=0,n=e.length,i=r;for(let r=0;r<n;r++){let n=i.indexOf(e.charAt(r));if(!(n>64)){if(n<0)return!1;t+=6}}return t%8==0}function a(e){let t=e.replace(/[\r\n=]/g,``),n=t.length,i=r,a=0,o=[];for(let e=0;e<n;e++)e%4==0&&e&&(o.push(a>>16&255),o.push(a>>8&255),o.push(a&255)),a=a<<6|i.indexOf(t.charAt(e));let s=n%4*6;return s===0?(o.push(a>>16&255),o.push(a>>8&255),o.push(a&255)):s===18?(o.push(a>>10&255),o.push(a>>2&255)):s===12&&o.push(a>>4&255),new Uint8Array(o)}function o(e){let t=``,n=0,i=e.length,a=r;for(let r=0;r<i;r++)r%3==0&&r&&(t+=a[n>>18&63],t+=a[n>>12&63],t+=a[n>>6&63],t+=a[n&63]),n=(n<<8)+e[r];let o=i%3;return o===0?(t+=a[n>>18&63],t+=a[n>>12&63],t+=a[n>>6&63],t+=a[n&63]):o===2?(t+=a[n>>10&63],t+=a[n>>4&63],t+=a[n<<2&63],t+=a[64]):o===1&&(t+=a[n>>2&63],t+=a[n<<4&63],t+=a[64],t+=a[64]),t}function s(e){return Object.prototype.toString.call(e)===`[object Uint8Array]`}t.exports=new n(`tag:yaml.org,2002:binary`,{kind:`scalar`,resolve:i,construct:a,predicate:s,represent:o})})),O=s(((e,t)=>{var n=p(),r=Object.prototype.hasOwnProperty,i=Object.prototype.toString;function a(e){if(e===null)return!0;let t=[],n=e;for(let e=0,a=n.length;e<a;e+=1){let a=n[e],o=!1;if(i.call(a)!==`[object Object]`)return!1;let s;for(s in a)if(r.call(a,s))if(!o)o=!0;else return!1;if(!o)return!1;if(t.indexOf(s)===-1)t.push(s);else return!1}return!0}function o(e){return e===null?[]:e}t.exports=new n(`tag:yaml.org,2002:omap`,{kind:`sequence`,resolve:a,construct:o})})),k=s(((e,t)=>{var n=p(),r=Object.prototype.toString;function i(e){if(e===null)return!0;let t=e,n=Array(t.length);for(let e=0,i=t.length;e<i;e+=1){let i=t[e];if(r.call(i)!==`[object Object]`)return!1;let a=Object.keys(i);if(a.length!==1)return!1;n[e]=[a[0],i[a[0]]]}return!0}function a(e){if(e===null)return[];let t=e,n=Array(t.length);for(let e=0,r=t.length;e<r;e+=1){let r=t[e],i=Object.keys(r);n[e]=[i[0],r[i[0]]]}return n}t.exports=new n(`tag:yaml.org,2002:pairs`,{kind:`sequence`,resolve:i,construct:a})})),A=s(((e,t)=>{var n=p(),r=Object.prototype.hasOwnProperty;function i(e){if(e===null)return!0;let t=e;for(let e in t)if(r.call(t,e)&&t[e]!==null)return!1;return!0}function a(e){return e===null?{}:e}t.exports=new n(`tag:yaml.org,2002:set`,{kind:`mapping`,resolve:i,construct:a})})),ee=s(((e,t)=>{t.exports=w().extend({implicit:[T(),E()],explicit:[D(),O(),k(),A()]})})),j=s(((e,t)=>{var n=u(),r=d(),i=f(),a=ee(),o=Object.prototype.hasOwnProperty,s=1,c=2,l=3,p=4,m=1,h=2,g=3,_=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,v=/[\x85\u2028\u2029]/,y=/[,\[\]{}]/,b=/^(?:!|!!|![0-9A-Za-z-]+!)$/,x=/^(?:!|[^,\[\]{}])(?:%[0-9a-f]{2}|[0-9a-z\-#;/?:@&=+$,_.!~*'()\[\]])*$/i;function S(e){return Object.prototype.toString.call(e)}function C(e){return e===10||e===13}function w(e){return e===9||e===32}function T(e){return e===9||e===32||e===10||e===13}function E(e){return e===44||e===91||e===93||e===123||e===125}function D(e){if(e>=48&&e<=57)return e-48;let t=e|32;return t>=97&&t<=102?t-97+10:-1}function O(e){return e===120?2:e===117?4:e===85?8:0}function k(e){return e>=48&&e<=57?e-48:-1}function A(e){switch(e){case 48:return`\0`;case 97:return`\x07`;case 98:return`\b`;case 116:return` `;case 9:return` `;case 110:return`
`;case 118:return`\v`;case 102:return`\f`;case 114:return`\r`;case 101:return`\x1B`;case 32:return` `;case 34:return`"`;case 47:return`/`;case 92:return`\\`;case 78:return`…`;case 95:return`\xA0`;case 76:return`\u2028`;case 80:return`\u2029`;default:return``}}function j(e){return e<=65535?String.fromCharCode(e):String.fromCharCode((e-65536>>10)+55296,(e-65536&1023)+56320)}function te(e,t,n){t===`__proto__`?Object.defineProperty(e,t,{configurable:!0,enumerable:!0,writable:!0,value:n}):e[t]=n}var M=Array(256),N=Array(256);for(let e=0;e<256;e++)M[e]=+!!A(e),N[e]=A(e);function ne(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||a,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.maxDepth=typeof t.maxDepth==`number`?t.maxDepth:100,this.maxMergeSeqLength=typeof t.maxMergeSeqLength==`number`?t.maxMergeSeqLength:20,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.depth=0,this.firstTabInLine=-1,this.documents=[],this.anchorMapTransactions=[]}function P(e,t){let n={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return n.snippet=i(n),new r(t,n)}function F(e,t){throw P(e,t)}function I(e,t){e.onWarning&&e.onWarning.call(null,P(e,t))}function L(e,t,n){let r=e.anchorMapTransactions;if(r.length!==0){let n=r[r.length-1];o.call(n,t)||(n[t]={existed:o.call(e.anchorMap,t),value:e.anchorMap[t]})}e.anchorMap[t]=n}function R(e){e.anchorMapTransactions.push(Object.create(null))}function re(e){let t=e.anchorMapTransactions.pop(),n=e.anchorMapTransactions;if(n.length===0)return;let r=n[n.length-1],i=Object.keys(t);for(let e=0,n=i.length;e<n;e+=1){let n=i[e];o.call(r,n)||(r[n]=t[n])}}function z(e){let t=e.anchorMapTransactions.pop(),n=Object.keys(t);for(let r=n.length-1;r>=0;--r){let i=t[n[r]];i.existed?e.anchorMap[n[r]]=i.value:delete e.anchorMap[n[r]]}}function B(e){return{position:e.position,line:e.line,lineStart:e.lineStart,lineIndent:e.lineIndent,firstTabInLine:e.firstTabInLine,tag:e.tag,anchor:e.anchor,kind:e.kind,result:e.result}}function V(e,t){e.position=t.position,e.line=t.line,e.lineStart=t.lineStart,e.lineIndent=t.lineIndent,e.firstTabInLine=t.firstTabInLine,e.tag=t.tag,e.anchor=t.anchor,e.kind=t.kind,e.result=t.result}var H={YAML:function(e,t,n){e.version!==null&&F(e,`duplication of %YAML directive`),n.length!==1&&F(e,`YAML directive accepts exactly one argument`);let r=/^([0-9]+)\.([0-9]+)$/.exec(n[0]);r===null&&F(e,`ill-formed argument of the YAML directive`);let i=parseInt(r[1],10),a=parseInt(r[2],10);i!==1&&F(e,`unacceptable YAML version of the document`),e.version=n[0],e.checkLineBreaks=a<2,a!==1&&a!==2&&I(e,`unsupported YAML version of the document`)},TAG:function(e,t,n){let r;n.length!==2&&F(e,`TAG directive accepts exactly two arguments`);let i=n[0];r=n[1],b.test(i)||F(e,`ill-formed tag handle (first argument) of the TAG directive`),o.call(e.tagMap,i)&&F(e,`there is a previously declared suffix for "`+i+`" tag handle`),x.test(r)||F(e,`ill-formed tag prefix (second argument) of the TAG directive`);try{r=decodeURIComponent(r)}catch(t){F(e,`tag prefix is malformed: `+r)}e.tagMap[i]=r}};function U(e,t,n,r){if(t<n){let i=e.input.slice(t,n);if(r)for(let t=0,n=i.length;t<n;t+=1){let n=i.charCodeAt(t);n===9||n>=32&&n<=1114111||F(e,`expected valid JSON character`)}else _.test(i)&&F(e,`the stream contains non-printable characters`);e.result+=i}}function W(e,t,r,i){n.isObject(r)||F(e,`cannot merge mappings; the provided source object is unacceptable`);let a=Object.keys(r);for(let e=0,n=a.length;e<n;e+=1){let n=a[e];o.call(t,n)||(te(t,n,r[n]),i[n]=!0)}}function G(e,t,n,r,i,a,s,c,l){if(Array.isArray(i)){i=Array.prototype.slice.call(i);for(let t=0,n=i.length;t<n;t+=1)Array.isArray(i[t])&&F(e,`nested arrays are not supported inside keys`),typeof i==`object`&&S(i[t])===`[object Object]`&&(i[t]=`[object Object]`)}if(typeof i==`object`&&S(i)===`[object Object]`&&(i=`[object Object]`),i=String(i),t===null&&(t={}),r===`tag:yaml.org,2002:merge`)if(Array.isArray(a)){a.length>e.maxMergeSeqLength&&F(e,`merge sequence length exceeded maxMergeSeqLength (`+e.maxMergeSeqLength+`)`);let r=new Set;for(let i=0,o=a.length;i<o;i+=1){let o=a[i];r.has(o)||(r.add(o),W(e,t,o,n))}}else W(e,t,a,n);else !e.json&&!o.call(n,i)&&o.call(t,i)&&(e.line=s||e.line,e.lineStart=c||e.lineStart,e.position=l||e.position,F(e,`duplicated mapping key`)),te(t,i,a),delete n[i];return t}function K(e){let t=e.input.charCodeAt(e.position);t===10?e.position++:t===13?(e.position++,e.input.charCodeAt(e.position)===10&&e.position++):F(e,`a line break is expected`),e.line+=1,e.lineStart=e.position,e.firstTabInLine=-1}function q(e,t,n){let r=0,i=e.input.charCodeAt(e.position);for(;i!==0;){for(;w(i);)i===9&&e.firstTabInLine===-1&&(e.firstTabInLine=e.position),i=e.input.charCodeAt(++e.position);if(t&&i===35)d Line truncated
`,t-1))}function ie(e,t,n){let r,i,a,o,s,c,l=e.kind,u=e.result,d=e.input.charCodeAt(e.position);if(T(d)||E(d)||d===35||d===38||d===42||d===33||d===124||d===62||d===39||d===34||d===37||d===64||d===96)return!1;if(d===63||d===45){let t=e.input.charCodeAt(e.position+1);if(T(t)||n&&E(t))return!1}for(e.kind=`scalar`,e.result=``,r=i=e.position,a=!1;d!==0;){if(d===58){let t=e.input.charCodeAt(e.position+1);if(T(t)||n&&E(t))break}else if(d===35){if(T(e.input.charCodeAt(e.position-1)))break}else if(e.position===e.lineStart&&J(e)||n&&E(d))break;else if(C(d))if(o=e.line,s=e.lineStart,c=e.lineIndent,q(e,!1,-1),e.lineIndent>=t){a=!0,d=e.input.charCodeAt(e.position);continue}else{e.position=i,e.line=o,e.lineStart=s,e.lineIndent=c;break}a&&(U(e,r,i,!1),Y(e,e.line-o),r=i=e.position,a=!1),w(d)||(i=e.position+1),d=e.input.charCodeAt(++e.position)}return U(e,r,i,!1),e.result?!0:(e.kind=l,e.result=u,!1)}function X(e,t){let n,r,i=e.input.charCodeAt(e.position);if(i!==39)return!1;for(e.kind=`scalar`,e.result=``,e.position++,n=r=e.position;(i=e.input.charCodeAt(e.position))!==0;)if(i===39)if(U(e,n,e.position,!0),i=e.input.charCodeAt(++e.position),i===39)n=e.position,e.position++,r=e.position;else return!0;else C(i)?(U(e,n,r,!0),Y(e,q(e,!1,t)),n=r=e.position):e.position===e.lineStart&&J(e)?F(e,`unexpected end of the document within a single quoted scalar`):(e.position++,w(i)||(r=e.position));F(e,`unexpected end of the stream within a single quoted scalar`)}function ae(e,t){let n,r,i,a=e.input.charCodeAt(e.position);if(a!==34)return!1;for(e.kind=`scalar`,e.result=``,e.position++,n=r=e.position;(a=e.input.charCodeAt(e.position))!==0;)if(a===34)return U(e,n,e.position,!0),e.position++,!0;else if(a===92){if(U(e,n,e.position,!0),a=e.input.charCodeAt(++e.position),C(a))q(e,!1,t);else if(a<256&&M[a])e.result+=N[a],e.position++;else if((i=O(a))>0){let t=i,n=0;for(;t>0;t--)a=e.input.charCodeAt(++e.position),(i=D(a))>=0?n=(n<<4)+i:F(e,`expected hexadecimal character`);e.result+=j(n),e.position++}else F(e,`unknown escape sequence`);n=r=e.position}else C(a)?(U(e,n,r,!0),Y(e,q(e,!1,t)),n=r=e.position):e.position===e.lineStart&&J(e)?F(e,`unexpected end of the document within a double quoted scalar`):(e.position++,w(a)||(r=e.position));F(e,`unexpected end of the stream within a double quoted scalar`)}function oe(e,t){let n=!0,r,i,a,o=e.tag,c,l=e.anchor,u,d,f,p,m=Object.create(null),h,g,_,v=e.input.charCodeAt(e.position);if(v===91)u=93,p=!1,c=[];else if(v===123)u=125,p=!0,c={};else return!1;for(e.anchor!==null&&L(e,e.anchor,c),v=e.input.charCodeAt(++e.position);v!==0;){if(q(e,!0,t),v=e.input.charCodeAt(e.position),v===u)return e.position++,e.tag=o,e.anchor=l,e.kind=p?`mapping`:`sequence`,e.result=c,!0;n?v===44&&F(e,`expected the node content, but found ','`):F(e,`missed comma between flow collection entries`),g=h=_=null,d=f=!1,v===63&&T(e.input.charCodeAt(e.position+1))&&(d=f=!0,e.position++,q(e,!0,t)),r=e.line,i=e.lineStart,a=e.position,Q(e,t,s,!1,!0),g=e.tag,h=e.result,q(e,!0,t),v=e.input.charCodeAt(e.position),(f||e.line===r)&&v===58&&(d=!0,v=e.input.charCodeAt(++e.position),q(e,!0,t),Q(e,t,s,!1,!0),_=e.result),p?G(e,c,m,g,h,_,r,i,a):d?c.push(G(e,null,m,g,h,_,r,i,a)):c.push(h),q(e,!0,t),v=e.input.charCodeAt(e.position),v===44?(n=!0,v=e.input.charCodeAt(++e.position)):n=!1}F(e,`unexpected end of the stream within a flow collection`)}function Z(e,t){let r,i=m,a=!1,o=!1,s=t,c=0,l=!1,u,d=e.input.charCodeAt(e.position);if(d===124)r=!1;else if(d===62)r=!0;else return!1;for(e.kind=`scalar`,e.result=``;d!==0;)if(d=e.input.charCodeAt(++e.position),d===43||d===45)m===i?i=d===43?g:h:F(e,`repeat of a chomping mode identifier`);else if((u=k(d))>=0)u===0?F(e,`bad explicit indentation width of a block scalar; it cannot be less than one`):o?F(e,`repeat of an indentation width identifier`):(s=t+u-1,o=!0);else break;if(w(d)){do d=e.input.charCodeAt(++e.position);while(w(d));if(d===35)do d=e.input.charCodeAt(++e.position);while(!C(d)&&d!==0)}for(;d!==0;){for(K(e),e.lineIndent=0,d=e.input.charCodeAt(e.position);(!o||e.lineIndent<s)&&d===32;)e.lineIndent++,d=e.input.charCodeAt(++e.position);if(!o&&e.lineIndent>s&&(s=e.lineIndent),C(d)){c++;continue}if(!o&&s===0&&F(e,`missing indentation for block scalar`),e.lineIndent<s){i===g?e.result+=n.repeat(`
`,a?1+c:c):i===m&&a&&(e.result+=`
`);break}r?w(d)?(l=!0,e.result+=n.repeat(`
`,a?1+c:c)):l?(l=!1,e.result+=n.repeat(`
`,c+1)):c===0?a&&(e.result+=` `):e.result+=n.repeat(`
`,c):e.result+=n.repeat(`
`,a?1+c:c),a=!0,o=!0,c=0;let t=e.position;for(;!C(d)&&d!==0;)d=e.input.charCodeAt(++e.position);U(e,t,e.position,!1)}return!0}function se(e,t){let n=e.tag,r=e.anchor,i=[],a=!1;if(e.firstTabInLine!==-1)return!1;e.anchor!==null&&L(e,e.anchor,i);let o=e.input.charCodeAt(e.position);for(;o!==0&&(e.firstTabInLine!==-1&&(e.position=e.firstTabInLine,F(e,`tab characters must not be used in indentation`)),!(o!==45||!T(e.input.charCodeAt(e.position+1))));){if(a=!0,e.position++,q(e,!0,-1)&&e.lineIndent<=t){i.push(null),o=e.input.charCodeAt(e.position);continue}let n=e.line;if(Q(e,t,l,!1,!0),i.push(e.result),q(e,!0,-1),o=e.input.charCodeAt(e.position),(e.line===n||e.lineIndent>t)&&o!==0)F(e,`bad indentation of a sequence entry`);else if(e.lineIndent<t)break}return a?(e.tag=n,e.anchor=r,e.kind=`sequence`,e.result=i,!0):!1}function ce(e,t,n){let r,i,a,o,s=e.tag,l=e.anchor,u={},d=Object.create(null),f=null,m=null,h=null,g=!1,_=!1;if(e.firstTabInLine!==-1)return!1;e.anchor!==null&&L(e,e.anchor,u);let v=e.input.charCodeAt(e.position);for(;v!==0;){!g&&e.firstTabInLine!==-1&&(e.position=e.firstTabInLine,F(e,`tab characters must not be used in indentation`));let y=e.input.charCodeAt(e.position+1),b=e.line;if((v===63||v===58)&&T(y))v===63?(g&&(G(e,u,d,f,m,null,i,a,o),f=m=h=null),_=!0,g=!0,r=!0):g?(g=!1,r=!0):F(e,`incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line`),e.position+=1,v=y;else{if(i=e.line,a=e.lineStart,o=e.position,!Q(e,n,c,!1,!0))break;if(e.line===b){for(v=e.input.charCodeAt(e.position);w(v);)v=e.input.charCodeAt(++e.position);if(v===58)v=e.input.charCodeAt(++e.position),T(v)||F(e,`a whitespace character is expected after the key-value separator within a block mapping`),g&&(G(e,u,d,f,m,null,i,a,o),f=m=h=null),_=!0,g=!1,r=!1,f=e.tag,m=e.result;else if(_)F(e,`can not read an implicit mapping pair; a colon is missed`);else return e.tag=s,e.anchor=l,!0}else if(_)F(e,`can not read a block mapping entry; a multiline key may not be an implicit key`);else return e.tag=s,e.anchor=l,!0}if((e.line===b||e.lineIndent>t)&&(g&&(i=e.line,a=e.lineStart,o=e.position),Q(e,t,p,!0,r)&&(g?m=e.result:h=e.result),g||(G(e,u,d,f,m,h,i,a,o),f=m=h=null),q(e,!0,-1),v=e.input.charCodeAt(e.position)),(e.line===b||e.lineIndent>t)&&v!==0)F(e,`bad indentation of a mapping entry`);else if(e.lineIndent<t)break}return g&&G(e,u,d,f,m,null,i,a,o),_&&(e.tag=s,e.anchor=l,e.kind=`mapping`,e.result=u),_}function le(e){let t=!1,n=!1,r,i,a=e.input.charCodeAt(e.position);if(a!==33)return!1;e.tag!==null&&F(e,`duplication of a tag property`),a=e.input.charCodeAt(++e.position),a===60?(t=!0,a=e.input.charCodeAt(++e.position)):a===33?(n=!0,r=`!!`,a=e.input.charCodeAt(++e.position)):r=`!`;let s=e.position;if(t){do a=e.input.charCodeAt(++e.position);while(a!==0&&a!==62);e.position<e.length?(i=e.input.slice(s,e.position),a=e.input.charCodeAt(++e.position)):F(e,`unexpected end of the stream within a verbatim tag`)}else{for(;a!==0&&!T(a);)a===33&&(n?F(e,`tag suffix cannot contain exclamation marks`):(r=e.input.slice(s-1,e.position+1),b.test(r)||F(e,`named tag handle cannot contain such characters`),n=!0,s=e.position+1)),a=e.input.charCodeAt(++e.position);i=e.input.slice(s,e.position),y.test(i)&&F(e,`tag suffix cannot contain flow indicator characters`)}i&&!x.test(i)&&F(e,`tag name cannot contain such characters: `+i);try{i=decodeURIComponent(i)}catch(t){F(e,`tag name is malformed: `+i)}return t?e.tag=i:o.call(e.tagMap,r)?e.tag=e.tagMap[r]+i:r===`!`?e.tag=`!`+i:r===`!!`?e.tag=`tag:yaml.org,2002:`+i:F(e,`undeclared tag handle "`+r+`"`),!0}function ue(e){let t=e.input.charCodeAt(e.position);if(t!==38)return!1;e.anchor!==null&&F(e,`duplication of an anchor property`),t=e.input.charCodeAt(++e.position);let n=e.position;for(;t!==0&&!T(t)&&!E(t);)t=e.input.charCodeAt(++e.position);return e.position===n&&F(e,`name of an anchor node must contain at least one character`),e.anchor=e.input.slice(n,e.position),!0}function de(e){let t=e.input.charCodeAt(e.position);if(t!==42)return!1;t=e.input.charCodeAt(++e.position);let n=e.position;for(;t!==0&&!T(t)&&!E(t);)t=e.input.charCodeAt(++e.position);e.position===n&&F(e,`name of an alias node must contain at least one character`);let r=e.input.slice(n,e.position);return o.call(e.anchorMap,r)||F(e,`unidentified alias "`+r+`"`),e.result=e.anchorMap[r],q(e,!0,-1),!0}function fe(e,t,n,r){let i=B(e);return R(e),V(e,t),e.tag=null,e.anchor=null,e.kind=null,e.result=null,ce(e,n,r)&&e.kind===`mapping`?(re(e),!0):(z(e),V(e,i),!1)}function Q(e,t,n,r,i){let a,u,d=1,f=!1,m=!1,h=null,g,_,v;e.depth>=e.maxDepth&&F(e,`nesting exceeded maxDepth (`+e.maxDepth+`)`),e.depth+=1,e.listener!==null&&e.listener(`open`,e),e.tag=null,e.anchor=null,e.kind=null,e.result=null;let y=a=u=p===n||l===n;if(r&&q(e,!0,-1)&&(f=!0,e.lineIndent>t?d=1:e.lineIndent===t?d=0:e.lineIndent<t&&(d=-1)),d===1)for(;;){let n=e.input.charCodeAt(e.position),r=B(e);if(f&&(n===33&&e.tag!==null||n===38&&e.anchor!==null)||!le(e)&&!ue(e))break;h===nul Line truncated
`),e.charCodeAt(0)===65279&&(e=e.slice(1)));let n=new ne(e,t),r=e.indexOf(`\0`);for(r!==-1&&(n.position=r,F(n,`null byte is not allowed in input`)),n.input+=`\0`;n.input.charCodeAt(n.position)===32;)n.lineIndent+=1,n.position+=1;for(;n.position<n.length-1;)pe(n);return n.documents}function he(e,t,n){typeof t==`object`&&t&&n===void 0&&(n=t,t=null);let r=me(e,n);if(typeof t!=`function`)return r;for(let e=0,n=r.length;e<n;e+=1)t(r[e])}function ge(e,t){let n=me(e,t);if(n.length!==0){if(n.length===1)return n[0];throw new r(`expected a single document in the stream, but found more`)}}t.exports.loadAll=he,t.exports.load=ge})),te=s(((e,t)=>{var n=u(),r=d(),i=ee(),a=Object.prototype.toString,o=Object.prototype.hasOwnProperty,s=65279,c=9,l=10,f=13,p=32,m=33,h=34,g=35,_=37,v=38,y=39,b=42,x=44,S=45,C=58,w=61,T=62,E=63,D=64,O=91,k=93,A=96,j=123,te=124,M=125,N={};N[0]=`\\0`,N[7]=`\\a`,N[8]=`\\b`,N[9]=`\\t`,N[10]=`\\n`,N[11]=`\\v`,N[12]=`\\f`,N[13]=`\\r`,N[27]=`\\e`,N[34]=`\\"`,N[92]=`\\\\`,N[133]=`\\N`,N[160]=`\\_`,N[8232]=`\\L`,N[8233]=`\\P`;var ne=[`y`,`Y`,`yes`,`Yes`,`YES`,`on`,`On`,`ON`,`n`,`N`,`no`,`No`,`NO`,`off`,`Off`,`OFF`],P=/^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/;function F(e,t){if(t===null)return{};let n={},r=Object.keys(t);for(let i=0,a=r.length;i<a;i+=1){let a=r[i],s=String(t[a]);a.slice(0,2)===`!!`&&(a=`tag:yaml.org,2002:`+a.slice(2));let c=e.compiledTypeMap.fallback[a];c&&o.call(c.styleAliases,s)&&(s=c.styleAliases[s]),n[a]=s}return n}function I(e){let t,i,a=e.toString(16).toUpperCase();if(e<=255)t=`x`,i=2;else if(e<=65535)t=`u`,i=4;else if(e<=4294967295)t=`U`,i=8;else throw new r(`code point within a string may not be greater than 0xFFFFFFFF`);return`\\`+t+n.repeat(`0`,i-a.length)+a}var L=1,R=2;function re(e){this.schema=e.schema||i,this.indent=Math.max(1,e.indent||2),this.noArrayIndent=e.noArrayIndent||!1,this.skipInvalid=e.skipInvalid||!1,this.flowLevel=n.isNothing(e.flowLevel)?-1:e.flowLevel,this.styleMap=F(this.schema,e.styles||null),this.sortKeys=e.sortKeys||!1,this.lineWidth=e.lineWidth||80,this.noRefs=e.noRefs||!1,this.noCompatMode=e.noCompatMode||!1,this.condenseFlow=e.condenseFlow||!1,this.quotingType=e.quotingType===`"`?R:L,this.forceQuotes=e.forceQuotes||!1,this.replacer=typeof e.replacer==`function`?e.replacer:null,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result=``,this.duplicates=[],this.usedDuplicates=null}function z(e,t){let r=n.repeat(` `,t),i=0,a=``,o=e.length;for(;i<o;){let t,n=e.indexOf(`
`,i);n===-1?(t=e.slice(i),i=o):(t=e.slice(i,n+1),i=n+1),t.length&&t!==`
`&&(a+=r),a+=t}return a}function B(e,t){return`
`+n.repeat(` `,e.indent*t)}function V(e,t){for(let n=0,r=e.implicitTypes.length;n<r;n+=1)if(e.implicitTypes[n].resolve(t))return!0;return!1}function H(e){return e===p||e===c}function U(e){return e>=32&&e<=126||e>=161&&e<=55295&&e!==8232&&e!==8233||e>=57344&&e<=65533&&e!==s||e>=65536&&e<=1114111}function W(e){return U(e)&&e!==s&&e!==f&&e!==l}function G(e,t,n){let r=W(e),i=r&&!H(e);return(n?r:r&&e!==x&&e!==O&&e!==k&&e!==j&&e!==M)&&e!==g&&!(t===C&&!i)||W(t)&&!H(t)&&e===g||t===C&&i}function K(e){return U(e)&&e!==s&&!H(e)&&e!==S&&e!==E&&e!==C&&e!==x&&e!==O&&e!==k&&e!==j&&e!==M&&e!==g&&e!==v&&e!==b&&e!==m&&e!==te&&e!==w&&e!==T&&e!==y&&e!==h&&e!==_&&e!==D&&e!==A}function q(e){return!H(e)&&e!==C}function J(e,t){let n=e.charCodeAt(t),r;return n>=55296&&n<=56319&&t+1<e.length&&(r=e.charCodeAt(t+1),r>=56320&&r<=57343)?(n-55296)*1024+r-56320+65536:n}function Y(e){return/^\n* /.test(e)}var ie=1,X=2,ae=3,oe=4,Z=5;function se(e,t,n,r,i,a,o,s){let c,u=0,d=null,f=!1,p=!1,m=r!==-1,h=-1,g=K(J(e,0))&&q(J(e,e.length-1));if(t||o)for(c=0;c<e.length;u>=65536?c+=2:c++){if(u=J(e,c),!U(u))return Z;g=g&&G(u,d,s),d=u}else{for(c=0;c<e.length;u>=65536?c+=2:c++){if(u=J(e,c),u===l)f=!0,m&&(p=p||c-h-1>r&&e[h+1]!==` `,h=c);else if(!U(u))return Z;g=g&&G(u,d,s),d=u}p=p||m&&c-h-1>r&&e[h+1]!==` `}return!f&&!p?g&&!o&&!i(e)?ie:a===R?Z:X:n>9&&Y(e)?Z:o?a===R?Z:X:p?oe:ae}function ce(e,t,n,i,a){e.dump=function(){if(t.length===0)return e.quotingType===R?`""`:`''`;if(!e.noCompatMode&&(ne.indexOf(t)!==-1||P.test(t)))return e.quotingType===R?`"`+t+`"`:`'`+t+`'`;let o=e.indent*Math.max(1,n),s=e.lineWidth===-1?-1:Math.max(Math.min(e.lineWidth,40),e.lineWidth-o),c=i||e.flowLevel>-1&&n>=e.flowLevel;function l(t){return V(e,t)}switch(se(t,c,e.indent,s,l,e.quotingType,e.forceQuotes&&!i,a)){case ie:return t;case X:return`'`+t.replace(/'/g,`''`)+`'`;case ae:return`|`+le(t,e.indent)+ue(z(t,o));case oe:return`>`+le(t,e.indent)+ue(z(de(t,s),o));case Z:return`"`+Q(t,s)+`"`;default:throw new r(`impossible error: invalid scalar style`)}}()}function le(e,t){let n=Y(e)?String(t):``,r=e[e.length-1]===`
`;return n+(r&&(e[e.length-2]===`
`||e===`
`)?`+`:r?``:`-`)+`
`}function ue(e){return e[e.length-1]===`
`?e.slice(0,-1):e}function de(e,t){let n=/(\n+)([^\n]*)/g,r=function(){let r=e.indexOf(`
`);return r=r===-1?e.length:r,n.lastIndex=r,fe(e.slice(0,r),t)}(),i=e[0]===`
`||e[0]===` `,a,o;for(;o=n.exec(e);){let e=o[1],n=o[2];a=n[0]===` `,r+=e+(!i&&!a&&n!==``?`
`:``)+fe(n,t),i=a}return r}function fe(e,t){if(e===``||e[0]===` `)return e;let n=/ [^ ]/g,r,i=0,a,o=0,s=0,c=``;for(;r=n.exec(e);)s=r.index,s-i>t&&(a=o>i?o:s,c+=`
`+e.slice(i,a),i=a+1),o=s;return c+=`
`,e.length-i>t&&o>i?c+=e.slice(i,o)+`
`+e.slice(o+1):c+=e.slice(i),c.slice(1)}function Q(e){let t=``,n=0;for(let r=0;r<e.length;n>=65536?r+=2:r++){n=J(e,r);let i=N[n];!i&&U(n)?(t+=e[r],n>=65536&&(t+=e[r+1])):t+=i||I(n)}return t}function pe(e,t,n){let r=``,i=e.tag;for(let i=0,a=n.length;i<a;i+=1){let a=n[i];e.replacer&&(a=e.replacer.call(n,String(i),a)),($(e,t,a,!1,!1)||a===void 0&&$(e,t,null,!1,!1))&&(r!==``&&(r+=`,`+(e.condenseFlow?``:` `)),r+=e.dump)}e.tag=i,e.dump=`[`+r+`]`}function me(e,t,n,r){let i=``,a=e.tag;for(let a=0,o=n.length;a<o;a+=1){let o=n[a];e.replacer&&(o=e.replacer.call(n,String(a),o)),($(e,t+1,o,!0,!0,!1,!0)||o===void 0&&$(e,t+1,null,!0,!0,!1,!0))&&((!r||i!==``)&&(i+=B(e,t)),e.dump&&l===e.dump.charCodeAt(0)?i+=`-`:i+=`- `,i+=e.dump)}e.tag=a,e.dump=i||`[]`}function he(e,t,n){let r=``,i=e.tag,a=Object.keys(n);for(let i=0,o=a.length;i<o;i+=1){let o=``;r!==``&&(o+=`, `),e.condenseFlow&&(o+=`"`);let s=a[i],c=n[s];e.replacer&&(c=e.replacer.call(n,s,c)),$(e,t,s,!1,!1)&&(e.dump.length>1024&&(o+=`? `),o+=e.dump+(e.condenseFlow?`"`:``)+`:`+(e.condenseFlow?``:` `),$(e,t,c,!1,!1)&&(o+=e.dump,r+=o))}e.tag=i,e.dump=`{`+r+`}`}function ge(e,t,n,i){let a=``,o=e.tag,s=Object.keys(n);if(e.sortKeys===!0)s.sort();else if(typeof e.sortKeys==`function`)s.sort(e.sortKeys);else if(e.sortKeys)throw new r(`sortKeys must be a boolean or a function`);for(let r=0,o=s.length;r<o;r+=1){let o=``;(!i||a!==``)&&(o+=B(e,t));let c=s[r],u=n[c];if(e.replacer&&(u=e.replacer.call(n,c,u)),!$(e,t+1,c,!0,!0,!0))continue;let d=e.tag!==null&&e.tag!==`?`||e.dump&&e.dump.length>1024;d&&(e.dump&&l===e.dump.charCodeAt(0)?o+=`?`:o+=`? `),o+=e.dump,d&&(o+=B(e,t)),$(e,t+1,u,!0,d)&&(e.dump&&l===e.dump.charCodeAt(0)?o+=`:`:o+=`: `,o+=e.dump,a+=o)}e.tag=o,e.dump=a||`{}`}function _e(e,t,n){let i=n?e.explicitTypes:e.implicitTypes;for(let s=0,c=i.length;s<c;s+=1){let c=i[s];if((c.instanceOf||c.predicate)&&(!c.instanceOf||typeof t==`object`&&t instanceof c.instanceOf)&&(!c.predicate||c.predicate(t))){if(n?c.multi&&c.representName?e.tag=c.representName(t):e.tag=c.tag:e.tag=`?`,c.represent){let n=e.styleMap[c.tag]||c.defaultStyle,i;if(a.call(c.represent)===`[object Function]`)i=c.represent(t,n);else if(o.call(c.represent,n))i=c.represent[n](t,n);else throw new r(`!<`+c.tag+`> tag resolver accepts not "`+n+`" style`);e.dump=i}return!0}}return!1}function $(e,t,n,i,o,s,c){e.tag=null,e.dump=n,_e(e,n,!1)||_e(e,n,!0);let l=a.call(e.dump),u=i;i&&(i=e.flowLevel<0||e.flowLevel>t);let d=l===`[object Object]`||l===`[object Array]`,f,p;if(d&&(f=e.duplicates.indexOf(n),p=f!==-1),(e.tag!==null&&e.tag!==`?`||p||e.indent!==2&&t>0)&&(o=!1),p&&e.usedDuplicates[f])e.dump=`*ref_`+f;else{if(d&&p&&!e.usedDuplicates[f]&&(e.usedDuplicates[f]=!0),l===`[object Object]`)i&&Object.keys(e.dump).length!==0?(ge(e,t,e.dump,o),p&&(e.dump=`&ref_`+f+e.dump)):(he(e,t,e.dump),p&&(e.dump=`&ref_`+f+` `+e.dump));else if(l===`[object Array]`)i&&e.dump.length!==0?(e.noArrayIndent&&!c&&t>0?me(e,t-1,e.dump,o):me(e,t,e.dump,o),p&&(e.dump=`&ref_`+f+e.dump)):(pe(e,t,e.dump),p&&(e.dump=`&ref_`+f+` `+e.dump));else if(l===`[object String]`)e.tag!==`?`&&ce(e,e.dump,t,s,u);else if(l===`[object Undefined]`)return!1;else{if(e.skipInvalid)return!1;throw new r(`unacceptable kind of an object to dump `+l)}if(e.tag!==null&&e.tag!==`?`){let t=encodeURI(e.tag[0]===`!`?e.tag.slice(1):e.tag).replace(/!/g,`%21`);t=e.tag[0]===`!`?`!`+t:t.slice(0,18)===`tag:yaml.org,2002:`?`!!`+t.slice(18):`!<`+t+`>`,e.dump=t+` `+e.dump}}return!0}function ve(e,t){let n=[],r=[];ye(e,n,r);let i=r.length;for(let e=0;e<i;e+=1)t.duplicates.push(n[r[e]]);t.usedDuplicates=Array(i)}function ye(e,t,n){if(typeof e==`object`&&e){let r=t.indexOf(e);if(r!==-1)n.indexOf(r)===-1&&n.push(r);else if(t.push(e),Array.isArray(e))for(let r=0,i=e.length;r<i;r+=1)ye(e[r],t,n);else{let r=Object.keys(e);for(let i=0,a=r.length;i<a;i+=1)ye(e[r[i]],t,n)}}}function be(e,t){t=t||{};let n=new re(t);n.noRefs||ve(e,n);let r=e;return n.replacer&&(r=n.replacer.call({"":r},``,r)),$(n,0,r,!0,!0)?n.dump+`
`:``}t.exports.dump=be})),M=l(s(((e,t)=>{var n=j(),r=te();function i(e,t){return function(){throw Error(`Function yaml.`+e+` is removed in js-yaml 4. Use yaml.`+t+` instead, which is now safe by default.`)}}t.exports.Type=p(),t.exports.Schema=m(),t.exports.FAILSAFE_SCHEMA=v(),t.exports.JSON_SCHEMA=C(),t.exports.CORE_SCHEMA=w(),t.exports.DEFAULT_SCHEMA=ee(),t.exports.load=n.load,t.exports.loadAll=n.loadAll,t.exports.dump=r.dump,t.exports.YAMLException=d(),t.exports.types={binary:D(),float:S(),map:_(),null:y(),pairs:k(),set:A(),timestamp:T(),bool:b(),int:x(),merge:E(),omap:O(),seq:g(),str:h()},t.exports.safeLoad=i(`safeLoad`,`load`),t.exports.safeLoadAll=i(`safeLoadAll`,`loadAll`),t.exports.safeDump=i(`safeDump`,`dump`)}))(),1),{Type:N,Schema:ne,FAILSAFE_SCHEMA:P,JSON_SCHEMA:F,CORE_SCHEMA:I,DEFAULT_SCHEMA:L,load:R,loadAll:re,dump:z,YAMLException:B,types:V,safeLoad:H,safeLoadAll:U,safeDump:W}=M.default,G=M.default;e.CORE_SCHEMA=I,e.DEFAULT_SCHEMA=L,e.FAILSAFE_SCHEMA=P,e.JSON_SCHEMA=F,e.Schema=ne,e.Type=N,e.YAMLException=B,e.default=G,e.dump=z,e.load=R,e.loadAll=re,e.safeDump=W,e.safeLoad=H,e.safeLoadAll=U,e.types=V});
//# sourceMappingURL=js-yaml.min.js.map
File diff suppressed because it is too large. Load diff
-750
View File
@@ -1,750 +0,0 @@
#!/bin/sh
. /usr/share/libubox/jshn.sh
. /lib/functions.sh
. /usr/share/honk/lock.sh
. /usr/share/honk/lifecycle.sh
. /usr/share/honk/log-lines.sh
CONTROL=/usr/share/honk/control.sh
CORE=/usr/bin/honk-core
JOBS=/tmp/honk-jobs
JOB_RUNNER=/usr/share/honk/job.sh
UPDATE=/usr/share/honk/update.sh
MAINT=/usr/share/honk/maintenance.sh
REDACT=/usr/share/honk/redact-log.sh
LOG_LEVEL_FILTER=/usr/share/honk/log-level-filter.sh
RESTORE_ROOT=/tmp/honk-maintenance
RESTORE_PATH=$RESTORE_ROOT/restore.tar.gz
emit_result() {
ok=$1
message=$2
[ "$ok" = true ] && ok=1 || ok=0
json_init
json_add_boolean ok "$ok"
json_add_string message "$message"
json_dump
}
emit_errors() {
ok=$1
[ "$ok" = true ] && ok=1 || ok=0
json_init
json_add_boolean ok "$ok"
json_add_array errors
for error in $2; do
json_add_string "" "$error"
done
json_close_array
json_dump
}
lock_owner_invalid() {
[ -d "$HONK_LOCK_DIR" ] || return 1
lock_owner=$(cat "$HONK_LOCK_DIR/pid" 2>/dev/null)
case "$lock_owner" in ''|*[!0-9]*) return 0 ;; esac
return 1
}
status() {
local enabled initialized boot_enabled running api_url api_ready core_version doona_version kernel lan_network listen_port errors listen clash_api_enabled clash_api_port clash_api_secret clash_api_url clash_api_ready clash_ui_ready clash_secret_set
config_load honk
config_get_bool enabled main enabled 0
config_get_bool initialized main initialized 0
config_get_bool boot_enabled main boot_enabled 0
config_get listen_port main listen_port 9527
config_get lan_network main lan_network lan
config_get_bool clash_api_enabled main clash_api_enabled 0
config_get clash_api_port main clash_api_port 9090
config_get clash_api_secret main clash_api_secret ''
managed_pid=$(procd_core_pid)
if is_managed_core "$managed_pid"; then running=1; else running=0; fi
listen=$(sed -n "s/^[[:space:]]*listen:[[:space:]]*'\\([^']*\\)'.*/\\1/p" /etc/honk/system.dae | head -n 1)
case "$listen" in
''|*[!0-9.:]*) listen="127.0.0.1:$listen_port" ;;
esac
api_url="http://$listen"
case "$clash_api_port" in
''|*[!0-9]*) clash_api_port=0 ;;
esac
clash_api_url="http://${listen%:*}:$clash_api_port"
clash_api_ready=0
if [ "$running" = 1 ] && [ "$clash_api_enabled" = 1 ] && [ "$clash_api_port" -gt 0 ] && honk_lifecycle_listener_owned "$managed_pid" "$clash_api_port"; then clash_api_ready=1; fi
clash_ui_ready=0
[ -s /usr/share/honk/zashboard/index.html ] && clash_ui_ready=1
clash_secret_set=0
[ -n "$clash_api_secret" ] && clash_secret_set=1
if [ "$running" = 1 ] && honk_lifecycle_api_owned "$managed_pid"; then api_ready=1; else api_ready=0; fi
core_version=$($CORE --version 2>/dev/null | sed -n '1p')
[ -n "$core_version" ] || core_version=unknown
doona_version=$(sed -n 's/^Version: //p' /usr/share/doona/SOURCE 2>/dev/null | head -n 1)
[ -n "$doona_version" ] || doona_version=unknown
kernel=$(uname -r)
errors=""
if [ "$enabled" != 1 ]; then errors="$errors disabled"; fi
if [ "$initialized" != 1 ]; then errors="$errors not_initialized"; fi
if [ "$enabled" = 1 ] && [ "$initialized" = 1 ] && [ "$running" != 1 ]; then errors="$errors service_not_running"; fi
if [ "$running" = 1 ] && [ "$api_ready" != 1 ]; then errors="$errors api_unavailable"; fi
if [ -d "$HONK_LOCK_DIR" ]; then
if lock_owner_invalid || honk_lock_is_stale; then errors="$errors stale_operation_lock maintenance_recovery_required"; else errors="$errors operation_in_progress"; fi
fi
{ [ -e /etc/.honk-maintenance/journal ] || [ -L /etc/.honk-maintenance/journal ]; } && errors="$errors maintenance_recovery_required"
{ [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; } && errors="$errors update_recovery_required"
json_init
json_add_boolean enabled "$enabled"
json_add_boolean initialized "$initialized"
json_add_boolean boot_enabled "$boot_enabled"
json_add_boolean running "$running"
json_add_boolean api_ready "$api_ready"
json_add_string api_url "$api_url"
json_add_boolean clash_api_enabled "$clash_api_enabled"
json_add_int clash_api_port "$clash_api_port"
json_add_boolean clash_api_secret_set "$clash_secret_set"
json_add_boolean clash_api_ready "$clash_api_ready"
json_add_boolean clash_ui_ready "$clash_ui_ready"
json_add_string clash_api_url "$clash_api_url"
json_add_string core_version "$core_version"
json_add_string doona_version "$doona_version"
json_add_string kernel "$kernel"
json_add_string lan_network "$lan_network"
case "$listen_port" in
''|*[!0-9]*) listen_port=0 ;;
esac
json_add_int listen_port "$listen_port"
json_add_array errors
for error in $errors; do json_add_string "" "$error"; done
json_close_array
json_dump
}
service_ready_config() {
local enabled initialized
config_load honk
config_get_bool enabled main enabled 0
config_get_bool initialized main initialized 0
[ "$initialized" = 1 ]
}
start_job() {
job_operation=$1
job_request=
case "$job_operation" in backup|restore|reset|update_check|update_apply) ;; *) emit_result false invalid_operation; return 1 ;; esac
if [ "$job_operation" = update_apply ]; then
update_gate_result=$("$UPDATE" gate 2>/dev/null) || { emit_result false update_check_failed; return 1; }
json_load "$update_gate_result" 2>/dev/null || { emit_result false update_check_failed; return 1; }
json_get_var update_apply_enabled apply_enabled
if [ "$update_apply_enabled" != 1 ]; then
json_get_var update_gate_reason reason
case "$update_gate_reason" in rollback_tuple_unavailable|trusted_feed_unavailable|signed_manifest_missing|signed_manifest_expired|no_update) emit_result false "$update_gate_reason" ;; *) emit_result false update_check_failed ;; esac
return 1
fi
fi
if [ ! -e "$JOBS" ] && [ ! -L "$JOBS" ]; then
(umask 077; mkdir "$JOBS") || { emit_result false job_storage_unavailable; return 1; }
fi
[ -d "$JOBS" ] && [ ! -L "$JOBS" ] && [ "$(stat -c '%u:%a' "$JOBS" 2>/dev/null)" = 0:700 ] || { emit_result false job_storage_unavailable; return 1; }
job_id=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-')
case "$job_id" in [0-9a-f][0-9a-f]*) [ "${#job_id}" -eq 32 ] || { emit_result false job_id_unavailable; return 1; } ;; *) emit_result false job_id_unavailable; return 1 ;; esac
job_dir=$JOBS/$job_id
mkdir "$job_dir" || { emit_result false job_storage_unavailable; return 1; }
chmod 0700 "$job_dir" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
json_init
json_add_string state running
json_add_string phase queued
json_add_boolean ok 1
json_add_string message job_started
json_dump > "$job_dir/status" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
chmod 0600 "$job_dir/status" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
nohup "$JOB_RUNNER" "$job_id" "$job_operation" >/dev/null 2>&1 </dev/null &
job_pid=$!
printf '%s\n' "$job_pid" > "$job_dir/worker.pid" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
chmod 0600 "$job_dir/worker.pid" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
json_init
json_add_boolean ok 1
json_add_string message job_started
json_add_string job_id "$job_id"
json_dump
}
job_status() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var job_id job_id
case "$job_id" in ''|*[!0-9a-f]*) emit_result false invalid_job_id; return 1 ;; esac
[ "${#job_id}" -eq 32 ] || { emit_result false invalid_job_id; return 1; }
[ -d "$JOBS" ] && [ ! -L "$JOBS" ] && [ "$(stat -c '%u:%a' "$JOBS" 2>/dev/null)" = 0:700 ] || { emit_result false job_not_found; return 1; }
job_dir=$JOBS/$job_id
[ -d "$job_dir" ] && [ ! -L "$job_dir" ] && [ "$(stat -c '%u:%a' "$job_dir" 2>/dev/null)" = 0:700 ] || { emit_result false job_not_found; return 1; }
job_status_file=$JOBS/$job_id/status
[ -f "$job_status_file" ] && [ ! -L "$job_status_file" ] && [ "$(stat -c '%u:%a' "$job_status_file" 2>/dev/null)" = 0:600 ] || { emit_result false job_not_found; return 1; }
job_pid_file=$job_dir/worker.pid
[ -f "$job_pid_file" ] && [ ! -L "$job_pid_file" ] && [ "$(stat -c '%u:%a' "$job_pid_file" 2>/dev/null)" = 0:600 ] || { emit_result false job_not_found; return 1; }
job_state=$(jsonfilter -i "$job_status_file" -e '@.state' 2>/dev/null)
job_pid=$(cat "$job_pid_file" 2>/dev/null)
case "$job_pid" in ''|*[!0-9]*) job_pid= ;; esac
if [ "$job_state" = running ] && { [ -z "$job_pid" ] || ! kill -0 "$job_pid" 2>/dev/null; }; then
json_init
if [ -e /etc/.honk-maintenance/journal ] || [ -L /etc/.honk-maintenance/journal ] || [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ] || [ -d "$HONK_LOCK_DIR" ]; then
json_add_string state rollback_required
json_add_string phase recovery
job_lost_message=maintenance_recovery_required
else
json_add_string state failed
json_add_string phase complete
job_lost_message worker_lost
fi
json_add_boolean ok 0
json_add_string message "$job_lost_message"
json_dump > "$job_dir/status.new" && chmod 0600 "$job_dir/status.new" && mv -f "$job_dir/status.new" "$job_status_file" || { emit_result false job_status_unavailable; return 1; }
fi
cat "$job_status_file"
}
logs() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var log_source source
json_get_var log_level level
json_get_var log_limit limit
case "$log_source" in service) log_pattern='honk.init|procd.*honk|honk service|honk\[[0-9]+\]' ;; core) log_pattern='honk-core|honk::' ;; maintenance) log_pattern='honk-maintenance|honk maintenance' ;; *) emit_result false invalid_log_source; return 1 ;; esac
case "$log_level" in all|debug|info|warn|error) ;; *) emit_result false invalid_log_level; return 1 ;; esac
case "$log_limit" in ''|*[!0-9]*) log_limit=200 ;; esac
[ "$log_limit" -ge 1 ] && [ "$log_limit" -le 200 ] || { emit_result false invalid_log_limit; return 1; }
command -v logread >/dev/null 2>&1 || { emit_result false logs_unavailable; return 1; }
[ -x "$REDACT" ] && [ -x "$LOG_LEVEL_FILTER" ] || { emit_result false logs_unavailable; return 1; }
log_raw_file=$(mktemp /tmp/honk-log.XXXXXX) || { emit_result false logs_unavailable; return 1; }
log_file=$(mktemp /tmp/honk-log.XXXXXX) || { rm -f "$log_raw_file"; emit_result false logs_unavailable; return 1; }
chmod 0600 "$log_raw_file" "$log_file" || { rm -f "$log_raw_file" "$log_file"; emit_result false logs_unavailable; return 1; }
log_capped=0
logread -l 200 -e 'honk' 2>/dev/null | head -c 262144 > "$log_raw_file"
log_raw_bytes=$(wc -c < "$log_raw_file" | tr -d ' ')
if [ "$log_raw_bytes" -ge 262144 ]; then
log_capped=1
log_raw_complete=$(mktemp /tmp/honk-log.XXXXXX) || { rm -f "$log_raw_file" "$log_file"; emit_result false logs_unavailable; return 1; }
chmod 0600 "$log_raw_complete" || { rm -f "$log_raw_file" "$log_file" "$log_raw_complete"; emit_result false logs_unavailable; return 1; }
sed '$d' "$log_raw_file" > "$log_raw_complete"
mv -f "$log_raw_complete" "$log_raw_file"
fi
grep -E "$log_pattern" "$log_raw_file" | "$LOG_LEVEL_FILTER" "$log_level" | "$REDACT" > "$log_file"
rm -f "$log_raw_file"
line_count=$(wc -l < "$log_file" | tr -d ' ')
if [ "$line_count" -gt "$log_limit" ]; then
log_capped=1
tail -n "$log_limit" "$log_file" > "$log_file.latest" && mv -f "$log_file.latest" "$log_file"
fi
log_bytes=$(wc -c < "$log_file" | tr -d ' ')
if [ "$log_bytes" -gt 32768 ]; then
log_capped=1
log_capped_file=$(mktemp /tmp/honk-log.XXXXXX) || { rm -f "$log_file"; emit_result false logs_unavailable; return 1; }
chmod 0600 "$log_capped_file" || { rm -f "$log_file" "$log_capped_file"; emit_result false logs_unavailable; return 1; }
LC_ALL=C awk 'BEGIN { first=1; bytes=0 } { lines[NR]=$0; sizes[NR]=length($0)+1; bytes+=sizes[NR]; while (bytes>32768 && first<=NR) { bytes-=sizes[first]; first++ } } END { for (i=first; i<=NR; i++) print lines[i] }' "$log_file" > "$log_capped_file"
mv -f "$log_capped_file" "$log_file"
fi
json_init
json_add_boolean ok 1
json_add_string message logs_read
[ "$log_capped" = 1 ] && json_add_boolean capped 1 || json_add_boolean capped 0
json_add_array lines
honk_add_log_lines "$log_file"
json_close_array
json_dump
rm -f "$log_file"
}
promote_default_route() {
local current=$1 candidate=$2 dns_block
if cmp -s "$current" /usr/share/honk/default-config.dae; then
sed 's/^ fallback: direct$/ fallback: proxy/' "$candidate" > "$candidate.route" || return 1
elif [ "$(sha256sum "$current" | awk '{print $1}')" = 83d1cc32bfe5b44ecae4b05c2c82c81ce9a36ab29f391d1e1d1d6b4993e17d2b ]; then
# The r1 package had only an include and a direct fallback. Preserve the
# newly added node or subscription while upgrading that exact default.
dns_block=$(sed -n '/^dns {$/,/^}$/p' /usr/share/honk/default-config.dae) || return 1
[ -n "$dns_block" ] || return 1
awk -v dns_block="$dns_block" '
/^routing[[:space:]]*\{$/ {
print "group {\n proxy {\n policy: min_moving_avg\n }\n}\n"
print dns_block "\n"
print
print " dip(224.0.0.0/3, \047ff00::/8\047) -> direct\n dip(geoip: private) && !dport(53) -> direct(must)\n dip(geoip: cn) && !dport(53) -> direct(must)\n domain(geosite: cn) -> direct"
next
}
/^ fallback: direct$/ { print " fallback: proxy"; next }
{ print }
' "$candidate" > "$candidate.route" || return 1
else
return 0
fi
mv "$candidate.route" "$candidate" || return 1
}
validate_proxy_nodes() (
local candidate=$1 provider_name=${2:-} node_prefix=${3:-} expected_nodes=${4:-0} temp port core_pid= attempt ready node_count group_count result provider
temp=$(mktemp -d /tmp/honk-onboard.XXXXXX) || return 1
validator_core_owned() {
[ -n "$core_pid" ] && [ -r "/proc/$core_pid/cmdline" ] || return 1
case "$(tr '\000' ' ' < "/proc/$core_pid/cmdline" 2>/dev/null)" in
*"$temp/config.dae"*--mock-ebpf*) [ "$(readlink -f "/proc/$core_pid/exe" 2>/dev/null)" = "$CORE" ] ;;
*) return 1 ;;
esac
}
validator_cleanup() {
if validator_core_owned; then
kill -TERM "$core_pid" 2>/dev/null || :
attempt=0
while validator_core_owned && [ "$attempt" -lt 3 ]; do sleep 1; attempt=$((attempt + 1)); done
if validator_core_owned; then
kill -KILL "$core_pid" 2>/dev/null || :
attempt=0
while validator_core_owned && [ "$attempt" -lt 2 ]; do sleep 1; attempt=$((attempt + 1)); done
fi
validator_core_owned && { logger -t honk "candidate core did not exit; retained $temp"; return 1; }
fi
[ -z "$core_pid" ] || wait "$core_pid" 2>/dev/null || :
rm -rf "$temp" || return 1
core_pid=
temp=
}
trap 'validator_cleanup' EXIT
trap 'exit 1' HUP INT TERM
port=$((20000 + $(hexdump -n 2 -e '1/2 "%u"' /dev/urandom) % 20000))
if ! cp -p /etc/honk/*.dae "$temp/" || ! cp "$candidate" "$temp/config.dae"; then
return 1
fi
cat > "$temp/system.dae" <<EOF
global {
wan_interface: auto
lan_interface: ''
data_dir: '$temp'
tproxy_port: $((port + 20000))
nfqueue_enable: false
store_subscribe: true
}
experimental {
native_api {
enabled: true
listen: '127.0.0.1:$port'
password_auth: false
allow_anonymous_loopback: true
config_write: false
}
}
EOF
DAE_LOCATION_ASSET=/usr/share/v2ray "$CORE" --config "$temp/config.dae" --mock-ebpf > "$temp/core.log" 2>&1 &
core_pid=$!
ready=0
result=2
attempt=0
while [ "$attempt" -lt 25 ]; do
if ! kill -0 "$core_pid" 2>/dev/null; then result=1; break; fi
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$port/api/v1/nodes" -o "$temp/nodes.json" 2>/dev/null; then
ready=1
node_count=$(jsonfilter -i "$temp/nodes.json" -e '@.nodes[*].protocol' 2>/dev/null | awk '$1 != "direct" && $1 != "block" { count++ } END { print count+0 }')
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$port/api/v1/groups" -o "$temp/groups.json" 2>/dev/null; then
group_count=$(jsonfilter -i "$temp/groups.json" -e '@[*].member_count' 2>/dev/null | awk '{ count += $1 } END { print count+0 }')
if [ "$node_count" -gt 0 ] && [ "$group_count" -gt 0 ]; then
if [ -n "$node_prefix" ]; then
node_count=$(jsonfilter -i "$temp/nodes.json" -e '@.nodes[*].name' 2>/dev/null | awk -v prefix="$node_prefix" '$0 ~ "^" prefix "[0-9]+$" { count++ } END { print count+0 }')
if [ "$node_count" -eq "$expected_nodes" ]; then result=0; break; fi
elif [ -z "$provider_name" ]; then result=0; break; fi
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$port/api/v1/providers" -o "$temp/providers.json" 2>/dev/null &&
jsonfilter -i "$temp/providers.json" -e '@.providers[*]' > "$temp/providers.rows" 2>/dev/null; then
while IFS= read -r provider; do
[ "$(jsonfilter -s "$provider" -e '@.name' 2>/dev/null)" = "$provider_name" ] || continue
[ "$(jsonfilter -s "$provider" -e '@.kind' 2>/dev/null)" = subscription ] || continue
node_count=$(jsonfilter -s "$provider" -e '@.node_count' 2>/dev/null)
case "$node_count" in ''|*[!0-9]*) break ;; esac
if [ "$node_count" -gt 0 ]; then result=0; fi
break
done < "$temp/providers.rows"
[ "$result" = 0 ] && break
fi
fi
fi
fi
sleep 1
attempt=$((attempt + 1))
done
[ "$ready" = 1 ] || result=1
validator_cleanup || return 1
trap - EXIT HUP INT TERM
return "$result"
)
commit_validated_candidate() {
local cfg=$1 candidate=$2 was_running=0 restore
[ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ] && was_running=1
cp -p "$cfg" "$cfg.bak" || return 1
chmod 0600 "$candidate" && mv "$candidate" "$cfg" || return 1
[ "$was_running" = 1 ] || return 0
honk_lifecycle_restart 30 && return 0
restore=$(mktemp /etc/honk/.config.rollback.XXXXXX) || return 3
cp -p "$cfg.bak" "$restore" && mv "$restore" "$cfg" || { rm -f "$restore"; return 3; }
honk_lifecycle_stop 30 && honk_lifecycle_start 30 && return 2
logger -t honk "candidate restart and previous service restoration failed; inspect $cfg.bak"
return 3
}
add_subscription() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var sub_name name
json_get_var sub_url url
case "$sub_name" in
''|*[!A-Za-z0-9_-]*) emit_result false subscription_invalid; return 1 ;;
esac
case "$sub_url" in
http://*|https://*) ;;
*) emit_result false subscription_invalid; return 1 ;;
esac
cfg=/etc/honk/config.dae
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
begin_mutation
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
if grep -qE "^[[:space:]]*${sub_name}:[[:space:]]*['\"]" "$cfg"; then
emit_result false subscription_invalid_or_conflicting
return 1
fi
candidate=$(mktemp /etc/honk/.config.dae.XXXXXX) || { emit_result false config_write_failed; return 1; }
if ! awk -v name="$sub_name" -v url="$sub_url" '
/^subscription[[:space:]]*\{/ {
print
printf " %s: \047%s\047 {\n interval: 86400s\n route: direct\n }\n", name, url
found = 1
next
}
{ print }
END {
if (!found) {
printf "\nsubscription {\n %s: \047%s\047 {\n interval: 86400s\n route: direct\n }\n}\n", name, url
}
}
' "$cfg" > "$candidate" 2>/dev/null; then
rm -f "$candidate"
emit_result false config_write_failed
return 1
fi
promote_default_route "$cfg" "$candidate" || { rm -f "$candidate" "$candidate.route"; emit_result false config_write_failed; return 1; }
validate_proxy_nodes "$candidate" "$sub_name"
case "$?" in
0) ;;
2) rm -f "$candidate"; emit_result false subscription_no_nodes; return 1 ;;
*) rm -f "$candidate"; emit_result false candidate_invalid; return 1 ;;
esac
[ "$(sha256sum "$cfg" | awk '{print $1}')" = "$original_hash" ] || { rm -f "$candidate"; emit_result false source_changed; return 1; }
commit_validated_candidate "$cfg" "$candidate"
case "$?" in
0) ;;
2) emit_result false reload_failed_rolled_back; return 1 ;;
3) emit_result false reload_and_rollback_failed; return 1 ;;
*) rm -f "$candidate"; emit_result false config_write_failed; return 1 ;;
esac
emit_result true subscription_added
}
add_nodes() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var node_links links
[ -n "$node_links" ] || { emit_result false invalid_request; return 1; }
cfg=/etc/honk/config.dae
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
begin_mutation
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
node_prefix="node_$(hexdump -n 4 -e '4/1 "%02x"' /dev/urandom)_"
node_lines=$(printf '%s\n' "$node_links" | awk -v prefix="$node_prefix" '
BEGIN { n = 0 }
{
if ($0 !~ /^(vless|vmess|ss|ssr|tuic|trojan|hysteria2|hy2|socks5|http|https):\/\//) next
n++
printf " %s%03d: \047%s\047\n", prefix, n, $0
}
END { if (n == 0) exit 1 }
') || { emit_result false share_links_invalid_or_conflicting; return 1; }
added_nodes=$(printf '%s\n' "$node_lines" | awk 'END { print NR }')
candidate=$(mktemp /etc/honk/.config.dae.XXXXXX) || { emit_result false config_write_failed; return 1; }
if ! awk -v lines="$node_lines" '
/^[[:space:]]*node[[:space:]]*\{/ {
print
printf "%s\n", lines
found = 1
next
}
{ print }
END {
if (!found) printf "\nnode {\n%s\n}\n", lines
}
' "$cfg" > "$candidate" 2>/dev/null; then
rm -f "$candidate"
emit_result false config_write_failed
return 1
fi
promote_default_route "$cfg" "$candidate" || { rm -f "$candidate" "$candidate.route"; emit_result false config_write_failed; return 1; }
validate_proxy_nodes "$candidate" "" "$node_prefix" "$added_nodes"
case "$?" in
0) ;;
2) rm -f "$candidate"; emit_result false share_links_invalid_or_conflicting; return 1 ;;
*) rm -f "$candidate"; emit_result false candidate_invalid; return 1 ;;
esac
[ "$(sha256sum "$cfg" | awk '{print $1}')" = "$original_hash" ] || { rm -f "$candidate"; emit_result false source_changed; return 1; }
commit_validated_candidate "$cfg" "$candidate"
case "$?" in
0) ;;
2) emit_result false reload_failed_rolled_back; return 1 ;;
3) emit_result false reload_and_rollback_failed; return 1 ;;
*) rm -f "$candidate"; emit_result false config_write_failed; return 1 ;;
esac
emit_result true nodes_added
}
extract_dae_block() {
# $1 = 块名, $2 = 文件;输出该顶级块(含大括号)。只匹配行首(无缩进)的块,
# 避免把 dns 里的 routing/request 子块误当成顶级 routing。
awk -v name="$1" '
$0 ~ "^" name "[[:space:]]*\\{" {
in_block = 1
depth = 0
}
in_block {
print
depth += gsub(/\{/, "{")
depth -= gsub(/\}/, "}")
if (depth <= 0 && $0 ~ /\}/) in_block = 0
}
' "$2"
}
import_dae() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
src=/tmp/honk-v2-upload/import.dae
[ -f "$src" ] || { emit_result false dae_upload_unavailable; return 1; }
cfg=/etc/honk/config.dae
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
# 提取 node 块(过滤 honk-core 不支持的 ssr/snell 协议)
node_block=$(awk '
$0 ~ /^node[[:space:]]*\{/ {
in_block = 1
depth = 0
print
next
}
in_block {
if ($0 ~ /node_[A-Za-z0-9_]+:[[:space:]]*\047(ssr|snell):\/\//) next
print
depth += gsub(/\{/, "{")
depth -= gsub(/\}/, "}")
if (depth <= 0 && $0 ~ /\}/) in_block = 0
}
' "$src")
group_block=$(extract_dae_block group "$src")
routing_block=$(extract_dae_block routing "$src")
dns_block=$(extract_dae_block dns "$src")
[ -n "$node_block" ] || [ -n "$group_block" ] || [ -n "$routing_block" ] || [ -n "$dns_block" ] || { emit_result false dae_invalid_or_unsupported; return 1; }
cp -p "$cfg" "$cfg.bak" 2>/dev/null || true
# 整体替换 node/group/routing/dns 四个业务块,保留 include(system.dae)与 subscription。
if ! awk -v node_block="$node_block" -v group_block="$group_block" -v routing_block="$routing_block" -v dns_block="$dns_block" '
function skip_block() {
depth = 1
while (depth > 0 && (getline line) > 0) {
depth += gsub(/\{/, "{", line)
depth -= gsub(/\}/, "}", line)
}
}
/^[[:space:]]*node[[:space:]]*\{/ {
if (node_block != "") { printf "%s\n", node_block; node_done = 1 }
skip_block()
next
}
/^[[:space:]]*group[[:space:]]*\{/ {
if (group_block != "") { printf "%s\n", group_block; group_done = 1 }
skip_block()
next
}
/^[[:space:]]*routing[[:space:]]*\{/ {
if (routing_block != "") { printf "%s\n", routing_block; routing_done = 1 }
skip_block()
next
}
/^[[:space:]]*dns[[:space:]]*\{/ {
if (dns_block != "") { printf "%s\n", dns_block; dns_done = 1 }
skip_block()
next
}
{ print }
END {
if (node_block != "" && !node_done) printf "\n%s\n", node_block
if (group_block != "" && !group_done) printf "\n%s\n", group_block
if (routing_block != "" && !routing_done) printf "\n%s\n", routing_block
if (dns_block != "" && !dns_done) printf "\n%s\n", dns_block
}
' "$cfg" > "$cfg.tmp" 2>/dev/null; then
rm -f "$cfg.tmp"
emit_result false config_write_failed
return 1
fi
mv "$cfg.tmp" "$cfg" || { rm -f "$cfg.tmp"; emit_result false config_write_failed; return 1; }
chmod 0600 "$cfg" 2>/dev/null || true
rm -f "$src" 2>/dev/null || true
if [ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ]; then
/etc/init.d/honk reload >/dev/null 2>&1 || :
fi
emit_result true dae_imported
}
set_enabled() {
uci set "honk.main.enabled=$1" && uci commit honk
}
procd_core_pid() {
honk_lifecycle_procd_pid
}
is_managed_core() {
[ "$1" = "$(honk_lifecycle_managed_pid 2>/dev/null)" ] && [ -n "$1" ]
}
begin_mutation() {
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then emit_result false update_recovery_required; exit 1; fi
honk_lock_acquire || { emit_result false operation_in_progress; exit 1; }
trap 'honk_lock_release >/dev/null 2>&1' 0
}
restore_prepare() {
# Stage the upload directory for a backup restore. The frontend calls
# this before uploading the archive to $RESTORE_PATH.
if ! mkdir -p "$RESTORE_ROOT" 2>/dev/null; then
emit_result false mkdir_failed
return 1
fi
rm -f "$RESTORE_PATH"
emit_result true prepared
}
case "$1" in
list)
echo '{"status":{},"check":{},"initialize":{"username":"String","password":"String"},"start":{},"stop":{},"restart":{},"settings":{"lan_network":"String","listen_port":"Integer","boot_enabled":"Boolean","clash_api_enabled":"Boolean","clash_api_port":"Integer","clash_api_secret":"String"},"add_subscription":{"name":"String","url":"String"},"add_nodes":{"links":"String"},"import_dae":{},"repair":{},"backup":{},"restore":{},"restore_prepare":{},"reset":{},"job_status":{"job_id":"String"},"logs":{"source":"String","level":"String","limit":"Integer"},"update_check":{},"update_apply":{}}'
;;
call)
case "$2" in
status)
status
;;
check)
if errors=$("$CONTROL" diagnose 2>/dev/null); then emit_errors true ""; else emit_errors false "$errors"; fi
;;
initialize)
exec "$CONTROL" initialize
;;
settings)
exec "$CONTROL" settings
;;
add_subscription)
add_subscription
;;
add_nodes)
add_nodes
;;
import_dae)
import_dae
;;
repair)
exec "$CONTROL" repair
;;
backup)
start_job backup
;;
restore)
start_job restore
;;
restore_prepare)
restore_prepare
;;
reset)
start_job reset
;;
job_status)
job_status
;;
logs)
logs
;;
update_check)
start_job update_check
;;
update_apply)
start_job update_apply
;;
start)
begin_mutation
if ! service_ready_config; then emit_result false not_initialized; exit 1; fi
old_enabled=$(uci -q get honk.main.enabled || echo 0)
if ! set_enabled 1; then emit_result false settings_write_failed; exit 1; fi
managed_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
if [ -n "$managed_pid" ]; then
if honk_lifecycle_api_owned "$managed_pid"; then emit_result true already_running; else set_enabled "$old_enabled" >/dev/null 2>&1; emit_result false managed_service_unhealthy; exit 1; fi
elif honk_lifecycle_start 30; then
emit_result true started
else
honk_lifecycle_stop 30 >/dev/null 2>&1 || :
set_enabled "$old_enabled" >/dev/null 2>&1
emit_result false start_failed
exit 1
fi
;;
stop)
begin_mutation
old_enabled=$(uci -q get honk.main.enabled || echo 0)
if ! set_enabled 0; then emit_result false settings_write_failed; exit 1; fi
if honk_lifecycle_stop 30; then emit_result true stopped; else set_enabled "$old_enabled" >/dev/null 2>&1; emit_result false stop_failed; exit 1; fi
;;
restart)
begin_mutation
if ! service_ready_config; then emit_result false not_initialized; exit 1; fi
old_enabled=$(uci -q get honk.main.enabled || echo 0)
if ! set_enabled 1; then emit_result false settings_write_failed; exit 1; fi
if [ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ]; then
:
elif pidof honk-core >/dev/null 2>&1; then
set_enabled "$old_enabled" >/dev/null 2>&1
emit_result false unmanaged_core_running
exit 1
fi
if honk_lifecycle_restart 30; then emit_result true restarted; else set_enabled "$old_enabled" >/dev/null 2>&1; emit_result false restart_failed; exit 1; fi
;;
*)
emit_result false unknown_method
exit 1
;;
esac
;;
*)
exit 2
;;
esac
@@ -1,38 +0,0 @@
#!/bin/sh
# geo-cron.sh enable|disable
# Manage the geo auto-update crontab entry. enable reads the schedule from
# honk.main.geo_auto_freq (daily|weekly); both actions are idempotent.
ACTION="$1"
case "$ACTION" in
enable|disable) ;;
*) echo "usage: $0 enable|disable" >&2; exit 64 ;;
esac
CRONTAB="/etc/crontabs/root"
TAG="# luci-app-honk geo-update"
SCRIPT="/usr/share/luci-app-honk/update-geo.sh"
# Drop any previous honk geo entry (tag line + its command line).
clean() {
[ -f "$CRONTAB" ] || return 0
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
mv "$CRONTAB.tmp" "$CRONTAB"
}
clean
if [ "$ACTION" = "enable" ]; then
case "$(uci -q get honk.main.geo_auto_freq)" in
weekly) sched="17 4 * * 0" ;; # Sunday 04:17
*) sched="17 4 * * *" ;; # every day 04:17
esac
mkdir -p /etc/crontabs
{
echo "$TAG"
echo "$sched $SCRIPT geoip; $SCRIPT geosite"
} >> "$CRONTAB"
/etc/init.d/cron enable >/dev/null 2>&1
fi
/etc/init.d/cron restart >/dev/null 2>&1
exit 0
@@ -1,34 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# pkg-info.sh <honk|luci-app-honk>
# Prints "<installed>\t<latest>" for the named package, either field empty when
# unknown. Used by the Maintenance view to decide whether to enable [升级].
PKG="$1"
case "$PKG" in
honk|luci-app-honk) ;;
*) printf '\t\n'; exit 64 ;;
esac
installed=""
latest=""
if command -v apk >/dev/null 2>&1; then
if apk info -e "$PKG" >/dev/null 2>&1; then
installed=$(apk list -I "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-" {
sub("^" p "-", "", $1);
print $1;
exit
}
')
fi
latest=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-[0-9]" {
v = $1; sub("^" p "-", "", v);
print v
}
' | sort -V | tail -1)
fi
printf '%s\t%s\n' "$installed" "$latest"
@@ -1,19 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# Background apk index refresh for the Maintenance view. Returns at once.
LOCK=/tmp/luci-app-honk.idx.lock
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
[ "$(( $(date +%s) - mtime ))" -lt 90 ] && exit 0
fi
: > "$LOCK"
(
if command -v apk >/dev/null 2>&1; then
flock -n /tmp/luci-app-honk.apk.lock apk update
fi
) >/dev/null 2>&1 </dev/null &
exit 0
@@ -1,96 +0,0 @@
#!/bin/sh
# update-geo.sh <geoip|geosite>
# Fork-and-detach so the LuCI RPC call returns immediately. The maintenance
# card polls the .dat file's mtime/size to detect completion. A lock file
# in /tmp prevents concurrent runs.
TYPE="$1"
# Default source: Loyalsoldier/v2ray-rules-dat — daily-built, China-optimized,
# ships both geoip.dat and geosite.dat. Users can override per-type via
# honk.main.geoip_url / geosite_url (empty falls back to the default below).
case "$TYPE" in
geoip)
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
URL="$(uci -q get honk.main.geoip_url)"
DEST="/usr/share/v2ray/geoip.dat"
;;
geosite)
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
URL="$(uci -q get honk.main.geosite_url)"
DEST="/usr/share/v2ray/geosite.dat"
;;
*)
echo "usage: $0 <geoip|geosite>" >&2
exit 64
;;
esac
[ -n "$URL" ] || URL="$DEF_URL"
LOCK="/tmp/luci-app-honk.${TYPE}.lock"
LOG="/tmp/luci-app-honk.${TYPE}.log"
schedule_backend_reload() {
if /bin/pidof honk-core >/dev/null 2>&1; then
(
sleep 25
if /usr/bin/honk-core reload >/dev/null 2>&1; then
echo "$(date '+%F %T') reloaded honk-core"
else
echo "$(date '+%F %T') honk-core reload failed"
fi
) </dev/null >>"$LOG" 2>&1 &
fi
}
if [ -f "$LOCK" ]; then
# Lock is fresh (< 5 min)? Refuse. Stale? Remove and proceed.
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${TYPE} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${TYPE} update already in progress" >&2
exit 75
fi
# Spawn detached worker — parent returns immediately to LuCI.
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"' EXIT INT TERM
rc=0
TMP="${DEST}.new"
mkdir -p "$(dirname "$DEST")"
echo "$(date '+%F %T') begin: $URL"
if ! curl -fsSL --proto '=http,https' --proto-redir '=http,https' --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
echo "$(date '+%F %T') download failed"
rm -f "$TMP"
rc=1
else
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
magic=$(hexdump -n 1 -e '1/1 "%02x"' "$TMP" 2>/dev/null)
if [ "$size" -lt 1024 ]; then
echo "$(date '+%F %T') file too small ($size bytes)"
rm -f "$TMP"
rc=2
elif [ "$magic" != "0a" ]; then
echo "$(date '+%F %T') invalid geodata format"
rm -f "$TMP"
rc=3
else
mv "$TMP" "$DEST"
echo "$(date '+%F %T') updated $DEST ($size bytes)"
schedule_backend_reload
fi
fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
@@ -1,97 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# update-pkg.sh <honk|luci-app-honk>
# Refresh package indexes and upgrade the named package via apk. Forks to
# background; the result is streamed to /tmp/luci-app-honk.pkg.<name>.log.
PKG="$1"
case "$PKG" in
honk|luci-app-honk) ;;
*)
echo "usage: $0 <honk|luci-app-honk>" >&2
exit 64
;;
esac
# Run from a /tmp copy so upgrading luci-app-honk (which replaces this script)
# cannot corrupt the in-flight upgrade.
case "$0" in
/tmp/.honk-upd-*) ;;
*)
_self="/tmp/.honk-upd-$$"
cp "$0" "$_self" 2>/dev/null && exec sh "$_self" "$@"
;;
esac
LOCK="/tmp/luci-app-honk.pkg-${PKG}.lock"
LOG="/tmp/luci-app-honk.pkg-${PKG}.log"
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${PKG} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${PKG} update already in progress" >&2
exit 75
fi
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"; [ "${0#/tmp/.honk-upd-}" != "$0" ] && rm -f "$0"' EXIT INT TERM
echo "$(date '+%F %T') begin upgrade: $PKG"
if command -v apk >/dev/null 2>&1; then
(
flock 9
apk update 2>&1
ver=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '$1 ~ "^" p "-[0-9]" { v=$1; sub("^" p "-", "", v); print v }' | sort -V | tail -1)
if [ -n "$ver" ]; then
constraint="$PKG=$ver"
else
echo "result: 软件源中没有找到 $PKG,请检查网络或软件源配置"
exit 1
fi
echo "--- apk add -s $constraint ---"
if ! apk add -s "$constraint" 2>&1; then
echo "note: apk 预检失败,通常是系统上其它软件包的问题,继续尝试升级"
fi
echo "--- apk add $constraint ---"
apk add "$constraint" 2>&1
exit 0
) 9>/tmp/luci-app-honk.apk.lock
rc=$?
if [ "$rc" != 0 ]; then
:
elif ! apk list --installed 2>/dev/null | grep -q "^${PKG}-"; then
echo "result: $PKG is not installed"
rc=1
elif apk list -u 2>/dev/null | grep -q "^${PKG}-"; then
echo "result: $PKG still has a pending upgrade"
rc=1
else
echo "result: $PKG is at the latest available version"
rc=0
fi
else
echo "no package manager found"
exit 3
fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
if [ "$PKG" = "luci-app-honk" ] && [ "$rc" = "0" ]; then
echo "reloading rpcd to pick up new ACL"
/etc/init.d/rpcd reload 2>&1
fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
@@ -1,58 +0,0 @@
{
"admin/services/honk": {
"title": "Honk",
"order": 35,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-honk" ]
}
},
"admin/services/honk/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "honk/overview"
}
},
"admin/services/honk/configuration": {
"title": "Configuration",
"order": 20,
"action": {
"type": "view",
"path": "honk/configuration"
}
},
"admin/services/honk/logs": {
"title": "Logs",
"order": 30,
"action": {
"type": "view",
"path": "honk/logs"
}
},
"admin/services/honk/maintenance": {
"title": "Maintenance",
"order": 40,
"action": {
"type": "view",
"path": "honk/maintenance"
}
},
"admin/services/honk/dashboard": {
"hidden": true,
"action": {
"type": "alias",
"path": "admin/services/honk/overview"
}
},
"admin/services/honk/settings": {
"hidden": true,
"action": {
"type": "alias",
"path": "admin/services/honk/overview"
}
}
}
@@ -1,42 +0,0 @@
{
"luci-app-honk": {
"description": "Grant access to Honk controls and maintenance",
"read": {
"ubus": {
"honk": [ "status", "check", "job_status", "logs", "update_check" ],
"network": [ "get_proto_handlers" ],
"network.interface": [ "dump" ]
},
"file": {
"/tmp/honk-backup.tar.gz": [ "read" ],
"/usr/share/v2ray/geoip.dat": [ "read" ],
"/usr/share/v2ray/geosite.dat": [ "read" ],
"/tmp/luci-app-honk.geoip.log": [ "read" ],
"/tmp/luci-app-honk.geosite.log": [ "read" ],
"/tmp/luci-app-honk.pkg-honk.log": [ "read" ],
"/tmp/luci-app-honk.pkg-luci-app-honk.log": [ "read" ],
"/usr/share/luci-app-honk/pkg-info.sh honk": [ "exec" ],
"/usr/share/luci-app-honk/pkg-info.sh luci-app-honk": [ "exec" ],
"/usr/share/luci-app-honk/refresh-index.sh": [ "exec" ]
},
"uci": [ "network", "honk" ]
},
"write": {
"cgi-io": [ "upload" ],
"ubus": {
"honk": [ "initialize", "start", "stop", "restart", "settings", "add_subscription", "add_nodes", "import_dae", "repair", "backup", "restore", "restore_prepare", "reset", "update_apply" ]
},
"file": {
"/tmp/honk-maintenance/restore.tar.gz": [ "write" ],
"/tmp/honk-v2-upload/import.dae": [ "write" ],
"/usr/share/luci-app-honk/update-geo.sh geoip": [ "exec" ],
"/usr/share/luci-app-honk/update-geo.sh geosite": [ "exec" ],
"/usr/share/luci-app-honk/geo-cron.sh enable": [ "exec" ],
"/usr/share/luci-app-honk/geo-cron.sh disable": [ "exec" ],
"/usr/share/luci-app-honk/update-pkg.sh honk": [ "exec" ],
"/usr/share/luci-app-honk/update-pkg.sh luci-app-honk": [ "exec" ]
},
"uci": [ "honk" ]
}
}
}