update 2026-10-11 22:01:38

This commit is contained in:
action committed 2026-10-11 22:01:38 +08:00
1 parent ebe57dfba9
commit 6f8f22eee4
199 files changed
+8565 -41418

No files matched your search

@@ -0,0 +1,235 @@
// 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 added and Honk reloaded.'));
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 the subscription URL. Honk writes it into /etc/honk/config.dae, fetches the nodes and reloads.')),
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' }, _('Routing, DNS and groups are managed in the Doona dashboard once Honk is started. You can also edit /etc/honk/config.dae directly.'))
]);
page.appendChild(subSection);
page.appendChild(pasteSection);
page.appendChild(daeSection);
page.appendChild(note);
return page;
}
});
@@ -0,0 +1,501 @@
// 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
});
@@ -0,0 +1,133 @@
// 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;
}
});
@@ -0,0 +1,915 @@
/* 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;
}
}
@@ -0,0 +1,192 @@
// 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
])
]);
}
});
@@ -0,0 +1,535 @@
// 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;
}
});
@@ -0,0 +1,347 @@
// SPDX-License-Identifier: GPL-3.0-only
'use strict';
'require network';
'require poll';
'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 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: [] }; })
]).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 statusArea = E('div');
const actionMessage = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const setupArea = E('div');
let currentStatus = data.status;
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 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, 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)
])
]);
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));
}
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 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;
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(E('div', { 'class': 'cbi-page-actions' }, save));
form.appendChild(message);
form.addEventListener('submit', function(ev) {
ev.preventDefault();
const port = Number(portInput.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;
}
save.disabled = true;
settingsBusy = true;
message.textContent = _('Saving…');
honk.settings(networkSelect.value, port, bootEnabledInput.checked).then(honk.ensureOk).then(function(result) {
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;
}
});
@@ -0,0 +1,308 @@
// 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' ],
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 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_added: _('Subscription added and Honk reloaded.'),
nodes_added: _('Nodes added and Honk reloaded.'),
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: _('The imported configuration could not be parsed.'),
candidate_rejected: _('The imported configuration failed core validation.'),
source_changed: _('Honk or Doona configuration changed after the preview. Create a new preview.'),
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.'),
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.'),
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,
statusIssue: statusIssue,
resultMessage: resultMessage,
errorMessage: errorMessage,
jobPhaseMessage: jobPhaseMessage,
installStyles: installStyles
});
@@ -0,0 +1,103 @@
// 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 message = E('p', { 'class': 'honk-status-msg', 'role': 'status' }, '');
const save = E('button', { 'class': 'cbi-button cbi-button-positive', 'type': 'submit' }, _('Save settings'));
(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);
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(E('div', { 'class': 'cbi-page-actions' }, save));
section.appendChild(message);
section.addEventListener('submit', function(ev) {
ev.preventDefault();
const port = Number(portInput.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;
}
save.disabled = true;
message.textContent = _('Saving…');
honk.settings(networkSelect.value, port, bootEnabledInput.checked).then(honk.ensureOk).then(function(result) {
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;
}
});
@@ -0,0 +1,72 @@
// 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 });
@@ -0,0 +1,21 @@
(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.
@@ -0,0 +1,33 @@
(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