mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-05 18:44:06 +08:00
update 2026-09-24 16:56:26
This commit is contained in:
1 parent
b31a5c3878
commit
e4cee331e9
6885 files changed
+3240523
No files matched your search
@@ -0,0 +1,127 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.15
|
||||
PKG_RELEASE:=3
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
choice
|
||||
prompt "Backend dependency"
|
||||
default PACKAGE_$(PKG_NAME)_daed
|
||||
help
|
||||
Select which backend package luci-app-daede should pull in by default.
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_daed
|
||||
bool "daed backend"
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_dae
|
||||
bool "dae backend"
|
||||
|
||||
endchoice
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LuCI app for daede dashboard
|
||||
DEPENDS:=+luci-base +PACKAGE_$(PKG_NAME)_dae:dae +PACKAGE_$(PKG_NAME)_daed:daed
|
||||
PKGARCH:=all
|
||||
MAINTAINER:=kenzok8
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
LuCI web interface for managing dae or daed backends.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
po2lmo ./po/zh-cn/daede.po $(PKG_BUILD_DIR)/daede.zh-cn.lmo
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/daede
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DATA) ./root/etc/config/daede $(1)/etc/config/daede
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./root/etc/uci-defaults/90-luci-app-daede-init $(1)/etc/uci-defaults/90-luci-app-daede-init
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-daede.json $(1)/usr/share/luci/menu.d/luci-app-daede.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-daede.json $(1)/usr/share/rpcd/acl.d/luci-app-daede.json
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci-app-daede
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/pkg-info.sh $(1)/usr/share/luci-app-daede/pkg-info.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/update-geo.sh $(1)/usr/share/luci-app-daede/update-geo.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/update-pkg.sh $(1)/usr/share/luci-app-daede/update-pkg.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/gen-dae-config.sh $(1)/usr/share/luci-app-daede/gen-dae-config.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/proxy-check.sh $(1)/usr/share/luci-app-daede/proxy-check.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/fetch-clash-yaml.sh $(1)/usr/share/luci-app-daede/fetch-clash-yaml.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-backup.sh $(1)/usr/share/luci-app-daede/config-backup.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/snapshot-file.sh $(1)/usr/share/luci-app-daede/snapshot-file.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/config-defaults.sh $(1)/usr/share/luci-app-daede/config-defaults.sh
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci-app-daede/defaults
|
||||
$(INSTALL_DATA) $(CURDIR)/../dae/files/dae.config $(1)/usr/share/luci-app-daede/defaults/dae
|
||||
$(INSTALL_DATA) $(CURDIR)/../daed/files/daed.config $(1)/usr/share/luci-app-daede/defaults/daed
|
||||
$(INSTALL_DATA) ./root/etc/config/daede $(1)/usr/share/luci-app-daede/defaults/daede
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/refresh-index.sh $(1)/usr/share/luci-app-daede/refresh-index.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/geo-cron.sh $(1)/usr/share/luci-app-daede/geo-cron.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/daed-sub-update.sh $(1)/usr/share/luci-app-daede/daed-sub-update.sh
|
||||
$(INSTALL_BIN) ./root/usr/share/luci-app-daede/daed-sub-cron.sh $(1)/usr/share/luci-app-daede/daed-sub-cron.sh
|
||||
$(INSTALL_DIR) $(1)/www/cgi-bin
|
||||
$(INSTALL_BIN) ./root/www/cgi-bin/daede-sub $(1)/www/cgi-bin/daede-sub
|
||||
$(INSTALL_BIN) ./root/www/cgi-bin/daede-graphql $(1)/www/cgi-bin/daede-graphql
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/*.js $(1)/www/luci-static/resources/view/daede/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/daede/vendor
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/daede/vendor/* $(1)/www/luci-static/resources/view/daede/vendor/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/daede.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/daede.zh-cn.lmo
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
if [ -x /etc/uci-defaults/90-luci-app-daede-init ]; then
|
||||
( . /etc/uci-defaults/90-luci-app-daede-init )
|
||||
rm -f /etc/uci-defaults/90-luci-app-daede-init
|
||||
fi
|
||||
rm -f /usr/lib/lua/luci/i18n/daed.zh-cn.lmo
|
||||
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
|
||||
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,184 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
function deriveAirportName(value) {
|
||||
try {
|
||||
const url = new URL(String(value || ''));
|
||||
const filename = url.searchParams.get('filename');
|
||||
if (filename) {
|
||||
const name = filename.replace(/\.(?:ya?ml)$/i, '').trim();
|
||||
if (name)
|
||||
return name;
|
||||
}
|
||||
// Use the registrable label, not the whole hostname: sub.ssr.sh -> ssr.
|
||||
// Dropping the TLD (and any sub.* prefix) keeps the default group name
|
||||
// clean and avoids leaking the provider's full domain.
|
||||
const labels = (url.hostname || '').split('.').filter(Boolean);
|
||||
if (labels.length >= 2)
|
||||
return labels[labels.length - 2];
|
||||
return labels[0] || '机场_1';
|
||||
} catch (e) {
|
||||
return '机场_1';
|
||||
}
|
||||
}
|
||||
|
||||
function nextPastedName(names) {
|
||||
const used = {};
|
||||
(names || []).forEach(function(name) { used[String(name)] = true; });
|
||||
let index = 1;
|
||||
while (used['机场_' + index])
|
||||
index++;
|
||||
return '机场_' + index;
|
||||
}
|
||||
|
||||
function makeAirportId() {
|
||||
return 'airport_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
|
||||
}
|
||||
|
||||
function backendId(id) {
|
||||
return String(id || '').replace(/[^A-Za-z0-9]/g, '') || 'airport';
|
||||
}
|
||||
|
||||
function backendGroupName(name, backend, id) {
|
||||
const value = String(name || '').trim();
|
||||
if (isGroupNameValid(value, backend))
|
||||
return value || backendId(id);
|
||||
return backendId(id);
|
||||
}
|
||||
|
||||
function isGroupNameValid(name, backend) {
|
||||
const value = String(name || '').trim();
|
||||
return !!value && (backend !== 'dae' || /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value));
|
||||
}
|
||||
|
||||
// Coerce an auto-derived name (URL hostname/filename) into a name the third
|
||||
// step accepts, so the group name is pre-filled valid and needs no manual fix.
|
||||
// daed allows any non-empty name (incl. CJK); dae requires
|
||||
// /^[A-Za-z_][A-Za-z0-9_-]*$/, so collapse illegal chars to '_' and drop a
|
||||
// leading non-letter/underscore run.
|
||||
function sanitizeGroupName(name, backend) {
|
||||
const value = String(name || '').trim();
|
||||
if (!value || backend !== 'dae')
|
||||
return value;
|
||||
const cleaned = value
|
||||
.replace(/[^A-Za-z0-9_-]+/g, '_')
|
||||
.replace(/_{2,}/g, '_')
|
||||
.replace(/^[^A-Za-z_]+/, '')
|
||||
.replace(/[-_]+$/, '');
|
||||
return cleaned || 'airport';
|
||||
}
|
||||
|
||||
function hashSource(value) {
|
||||
// LuCI is commonly served over plain HTTP, where Web Crypto is unavailable.
|
||||
// This stable opaque key is only used to match a previously managed source.
|
||||
let h1 = 0x811c9dc5, h2 = 0x9e3779b9, h3 = 0x85ebca6b, h4 = 0xc2b2ae35;
|
||||
const text = unescape(encodeURIComponent(String(value || '')));
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const code = text.charCodeAt(index);
|
||||
h1 = Math.imul(h1 ^ code, 0x01000193);
|
||||
h2 = Math.imul(h2 ^ code, 0x5bd1e995);
|
||||
h3 = Math.imul(h3 ^ code, 0x27d4eb2d);
|
||||
h4 = Math.imul(h4 ^ code, 0x165667b1);
|
||||
}
|
||||
const words = [ h1, h2, h3, h4, h1 ^ h3, h2 ^ h4, h1 ^ h2 ^ h4, h1 ^ h2 ^ h3 ^ h4 ];
|
||||
return Promise.resolve(words.map(function(word) {
|
||||
return (word >>> 0).toString(16).padStart(8, '0');
|
||||
}).join(''));
|
||||
}
|
||||
|
||||
function list(value) {
|
||||
if (Array.isArray(value))
|
||||
return value.filter(Boolean).map(String);
|
||||
return value ? [ String(value) ] : [];
|
||||
}
|
||||
|
||||
function parseAirportSection(section) {
|
||||
if (!section)
|
||||
return null;
|
||||
return {
|
||||
sid: section['.name'],
|
||||
id: String(section.id || ''),
|
||||
backend: String(section.backend || ''),
|
||||
name: String(section.name || ''),
|
||||
source_hash: String(section.source_hash || ''),
|
||||
group_id: String(section.group_id || ''),
|
||||
subscription_id: String(section.subscription_id || ''),
|
||||
node_ids: list(section.node_id)
|
||||
};
|
||||
}
|
||||
|
||||
function airportSectionValues(record) {
|
||||
return {
|
||||
id: String(record.id || ''),
|
||||
backend: String(record.backend || ''),
|
||||
name: String(record.name || ''),
|
||||
source_hash: String(record.sourceHash || record.source_hash || ''),
|
||||
group_id: String(record.groupId || record.group_id || ''),
|
||||
subscription_id: String(record.subscriptionId || record.subscription_id || ''),
|
||||
node_id: list(record.nodeIds || record.node_ids)
|
||||
};
|
||||
}
|
||||
|
||||
function matchAirport(records, candidate) {
|
||||
const sameBackend = (records || []).filter(function(record) {
|
||||
return record.backend === candidate.backend;
|
||||
});
|
||||
if (candidate.selectedId) {
|
||||
const selected = sameBackend.find(function(record) { return record.id === candidate.selectedId; });
|
||||
if (selected)
|
||||
return selected;
|
||||
}
|
||||
if (candidate.sourceHash) {
|
||||
const bySource = sameBackend.find(function(record) { return record.source_hash === candidate.sourceHash; });
|
||||
if (bySource)
|
||||
return bySource;
|
||||
}
|
||||
return sameBackend.find(function(record) { return record.name === candidate.name; }) || null;
|
||||
}
|
||||
|
||||
function safeOldNodeIds(oldIds, newIds, otherAirportNodeIds, otherGroupNodeIds) {
|
||||
const keep = {};
|
||||
list(newIds).forEach(function(id) { keep[id] = true; });
|
||||
(otherAirportNodeIds || []).forEach(function(ids) { list(ids).forEach(function(id) { keep[id] = true; }); });
|
||||
(otherGroupNodeIds || []).forEach(function(ids) { list(ids).forEach(function(id) { keep[id] = true; }); });
|
||||
return list(oldIds).filter(function(id) { return !keep[id]; });
|
||||
}
|
||||
|
||||
function findManagedGroup(groups, record) {
|
||||
if (!record || !record.group_id)
|
||||
return null;
|
||||
return (groups || []).find(function(group) {
|
||||
return group.id === record.group_id && group.name === record.name;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findGroupByName(groups, name) {
|
||||
return (groups || []).find(function(group) { return group.name === name; }) || null;
|
||||
}
|
||||
|
||||
const api = {
|
||||
deriveAirportName: deriveAirportName,
|
||||
nextPastedName: nextPastedName,
|
||||
makeAirportId: makeAirportId,
|
||||
backendId: backendId,
|
||||
backendGroupName: backendGroupName,
|
||||
isGroupNameValid: isGroupNameValid,
|
||||
sanitizeGroupName: sanitizeGroupName,
|
||||
hashSource: hashSource,
|
||||
parseAirportSection: parseAirportSection,
|
||||
airportSectionValues: airportSectionValues,
|
||||
matchAirport: matchAirport,
|
||||
findManagedGroup: findManagedGroup,
|
||||
findGroupByName: findGroupByName,
|
||||
safeOldNodeIds: safeOldNodeIds
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
module.exports = api;
|
||||
|
||||
if (typeof baseclass !== 'undefined')
|
||||
return baseclass.extend(api);
|
||||
|
||||
return api;
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const BACKENDS = {
|
||||
daed: {
|
||||
name: 'daed',
|
||||
label: 'daed',
|
||||
uci: 'daed',
|
||||
initd: '/etc/init.d/daed',
|
||||
log: '/var/log/daed/daed.log',
|
||||
pkg: 'daed',
|
||||
hasWebUI: true,
|
||||
useNetns: true,
|
||||
defaultListen: '0.0.0.0:2023'
|
||||
},
|
||||
dae: {
|
||||
name: 'dae',
|
||||
label: 'dae',
|
||||
uci: 'dae',
|
||||
initd: '/etc/init.d/dae',
|
||||
log: '/var/log/dae/dae.log',
|
||||
pkg: 'dae',
|
||||
config: '/etc/dae/config.dae',
|
||||
hasWebUI: false,
|
||||
useNetns: false
|
||||
}
|
||||
};
|
||||
|
||||
function execOk(cmd, args) {
|
||||
return L.resolveDefault(fs.exec(cmd, args || []), { code: 1 }).then(function(res) {
|
||||
return res && res.code === 0;
|
||||
});
|
||||
}
|
||||
|
||||
function exists(path) {
|
||||
return L.resolveDefault(fs.stat(path), null).then(function(st) {
|
||||
return !!st;
|
||||
});
|
||||
}
|
||||
|
||||
function serviceStatus(name) {
|
||||
return L.resolveDefault(callServiceList(name), {}).then(function(svc) {
|
||||
let pid = 0, running = false;
|
||||
|
||||
try {
|
||||
const inst = svc[name].instances[name];
|
||||
running = !!inst.running;
|
||||
pid = inst.pid || 0;
|
||||
} catch (e) {}
|
||||
|
||||
return { running: running, pid: pid };
|
||||
});
|
||||
}
|
||||
|
||||
function detectInstalledBackends() {
|
||||
return Promise.all([
|
||||
exists(BACKENDS.dae.initd),
|
||||
exists(BACKENDS.daed.initd)
|
||||
]).then(function(r) {
|
||||
return { dae: r[0], daed: r[1] };
|
||||
});
|
||||
}
|
||||
|
||||
function detectRunning() {
|
||||
/* pidof matches the program name exactly and, unlike busybox `pgrep -x`
|
||||
(which compares the full cmdline `/usr/bin/dae run …` and never hits),
|
||||
reliably tells dae from daed without substring false positives. */
|
||||
return Promise.all([
|
||||
execOk('/bin/pidof', ['dae']),
|
||||
execOk('/bin/pidof', ['daed'])
|
||||
]).then(function(r) {
|
||||
return { dae: r[0], daed: r[1] };
|
||||
});
|
||||
}
|
||||
|
||||
function loadActiveBackend() {
|
||||
return L.resolveDefault(uci.load('daede'), null).then(function() {
|
||||
const active = uci.get('daede', 'config', 'active_backend');
|
||||
return BACKENDS[active] ? active : '';
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveBackend(name) {
|
||||
if (!BACKENDS[name])
|
||||
return Promise.reject(new Error('invalid backend: ' + name));
|
||||
|
||||
return exists('/etc/config/daede').then(function(ok) {
|
||||
if (!ok)
|
||||
return fs.write('/etc/config/daede', '');
|
||||
}).then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config=daede']); })
|
||||
.then(function() { return fs.exec('/sbin/uci', ['set', 'daede.config.active_backend=' + name]); })
|
||||
.then(function() { return fs.exec('/sbin/uci', ['commit', 'daede']); })
|
||||
.then(function() { uci.set('daede', 'config', 'active_backend', name); });
|
||||
}
|
||||
|
||||
function detectBackend() {
|
||||
return Promise.all([
|
||||
detectInstalledBackends(),
|
||||
detectRunning(),
|
||||
loadActiveBackend()
|
||||
]).then(function(r) {
|
||||
const installed = r[0];
|
||||
const running = r[1];
|
||||
const preferred = r[2];
|
||||
let name = preferred;
|
||||
|
||||
if (!name || !installed[name])
|
||||
name = installed.daed ? 'daed' : (installed.dae ? 'dae' : 'daed');
|
||||
|
||||
return {
|
||||
name: name,
|
||||
backend: BACKENDS[name],
|
||||
installed: installed,
|
||||
running: running,
|
||||
preferred: preferred,
|
||||
bothInstalled: !!(installed.dae && installed.daed)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
BACKENDS: BACKENDS,
|
||||
detectInstalledBackends: detectInstalledBackends,
|
||||
detectRunning: detectRunning,
|
||||
detectBackend: detectBackend,
|
||||
setActiveBackend: setActiveBackend,
|
||||
serviceStatus: serviceStatus
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
function utf8Base64(value) {
|
||||
const bytes = new TextEncoder().encode(String(value));
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function utf8FromBase64(value) {
|
||||
const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const bytes = Uint8Array.from(binary, function(ch) { return ch.charCodeAt(0); });
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
}
|
||||
|
||||
function base64Url(value) {
|
||||
return utf8Base64(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function endpoint(server, port) {
|
||||
const host = String(server).indexOf(':') >= 0 && String(server)[0] !== '['
|
||||
? '[' + server + ']'
|
||||
: server;
|
||||
return host + ':' + port;
|
||||
}
|
||||
|
||||
function requireFields(node, fields) {
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const key = fields[i];
|
||||
if (node[key] === undefined || node[key] === null || node[key] === '')
|
||||
throw new Error('Missing required field: ' + key);
|
||||
}
|
||||
}
|
||||
|
||||
function setIf(params, key, value) {
|
||||
if (value !== undefined && value !== null && value !== '')
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
function arrayValue(value) {
|
||||
return Array.isArray(value) ? value.join(',') : value;
|
||||
}
|
||||
|
||||
function addTransport(params, node) {
|
||||
const network = node.network || 'tcp';
|
||||
params.set('type', network);
|
||||
|
||||
if (network === 'ws') {
|
||||
const opts = node['ws-opts'] || {};
|
||||
setIf(params, 'path', opts.path);
|
||||
setIf(params, 'host', opts.headers && (opts.headers.Host || opts.headers.host));
|
||||
} else if (network === 'grpc') {
|
||||
const opts = node['grpc-opts'] || {};
|
||||
setIf(params, 'serviceName', opts['grpc-service-name'] || opts.serviceName);
|
||||
} else if (network === 'http' || network === 'h2') {
|
||||
const opts = node['h2-opts'] || node['http-opts'] || {};
|
||||
setIf(params, 'path', Array.isArray(opts.path) ? opts.path[0] : opts.path);
|
||||
setIf(params, 'host', arrayValue(opts.host));
|
||||
}
|
||||
}
|
||||
|
||||
function makeResult(node, link) {
|
||||
return {
|
||||
ok: true,
|
||||
name: String(node.name || 'Node'),
|
||||
type: String(node.type || '').toLowerCase(),
|
||||
link: link,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
function convertSs(node) {
|
||||
requireFields(node, [ 'server', 'port', 'cipher', 'password' ]);
|
||||
const auth = base64Url(node.cipher + ':' + node.password);
|
||||
let query = '';
|
||||
if (node.plugin) {
|
||||
const fields = [ String(node.plugin) ];
|
||||
const opts = node['plugin-opts'] || {};
|
||||
Object.keys(opts).forEach(function(key) {
|
||||
const value = opts[key];
|
||||
if (value === true)
|
||||
fields.push(key);
|
||||
else if (value !== false && value !== undefined && value !== null && value !== '')
|
||||
fields.push(key + '=' + value);
|
||||
});
|
||||
const params = new URLSearchParams();
|
||||
params.set('plugin', fields.join(';'));
|
||||
query = '?' + params.toString();
|
||||
}
|
||||
return 'ss://' + auth + '@' + endpoint(node.server, node.port) + query + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertSsr(node) {
|
||||
requireFields(node, [ 'server', 'port', 'cipher', 'password', 'protocol', 'obfs' ]);
|
||||
const query = [];
|
||||
query.push('remarks=' + base64Url(node.name || 'Node'));
|
||||
if (node['protocol-param'])
|
||||
query.push('protoparam=' + base64Url(node['protocol-param']));
|
||||
if (node['obfs-param'])
|
||||
query.push('obfsparam=' + base64Url(node['obfs-param']));
|
||||
|
||||
const payload = [
|
||||
node.server,
|
||||
node.port,
|
||||
node.protocol,
|
||||
node.cipher,
|
||||
node.obfs,
|
||||
base64Url(node.password)
|
||||
].join(':') + '/?' + query.join('&');
|
||||
return 'ssr://' + base64Url(payload);
|
||||
}
|
||||
|
||||
function convertVmess(node) {
|
||||
requireFields(node, [ 'server', 'port', 'uuid' ]);
|
||||
const network = node.network || 'tcp';
|
||||
const ws = node['ws-opts'] || {};
|
||||
const grpc = node['grpc-opts'] || {};
|
||||
const h2 = node['h2-opts'] || {};
|
||||
const payload = {
|
||||
v: '2',
|
||||
ps: String(node.name || 'Node'),
|
||||
add: String(node.server),
|
||||
port: String(node.port),
|
||||
id: String(node.uuid),
|
||||
aid: Number(node.alterId || node.alter_id || 0),
|
||||
scy: String(node.cipher || 'auto'),
|
||||
net: network,
|
||||
type: 'none',
|
||||
host: String((ws.headers && (ws.headers.Host || ws.headers.host)) || arrayValue(h2.host) || ''),
|
||||
path: String(ws.path || grpc['grpc-service-name'] || (Array.isArray(h2.path) ? h2.path[0] : h2.path) || ''),
|
||||
tls: node.tls ? 'tls' : 'none',
|
||||
sni: String(node.servername || node.sni || ''),
|
||||
alpn: String(arrayValue(node.alpn) || ''),
|
||||
fp: String(node.fingerprint || node['client-fingerprint'] || '')
|
||||
};
|
||||
return 'vmess://' + utf8Base64(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function convertVless(node) {
|
||||
requireFields(node, [ 'server', 'port', 'uuid' ]);
|
||||
const params = new URLSearchParams();
|
||||
addTransport(params, node);
|
||||
params.set('encryption', String(node.encryption || 'none'));
|
||||
setIf(params, 'flow', node.flow);
|
||||
|
||||
const reality = node['reality-opts'];
|
||||
if (reality || node.tls) {
|
||||
params.set('security', reality ? 'reality' : 'tls');
|
||||
setIf(params, 'sni', node.servername || node.sni);
|
||||
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allowInsecure', '1');
|
||||
if (reality) {
|
||||
setIf(params, 'pbk', reality['public-key']);
|
||||
setIf(params, 'sid', reality['short-id']);
|
||||
setIf(params, 'spx', reality['spider-x']);
|
||||
}
|
||||
}
|
||||
|
||||
return 'vless://' + encodeURIComponent(node.uuid) + '@' + endpoint(node.server, node.port) +
|
||||
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertTrojan(node) {
|
||||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
addTransport(params, node);
|
||||
setIf(params, 'sni', node.sni || node.servername);
|
||||
setIf(params, 'fp', node.fingerprint || node['client-fingerprint']);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allowInsecure', '1');
|
||||
return 'trojan://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||||
'?' + params.toString() + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertTuic(node) {
|
||||
requireFields(node, [ 'server', 'port', 'uuid', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
setIf(params, 'sni', node.sni);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
setIf(params, 'congestion_control', node['congestion-controller'] || node['congestion-control']);
|
||||
setIf(params, 'udp_relay_mode', node['udp-relay-mode']);
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('allow_insecure', '1');
|
||||
return 'tuic://' + encodeURIComponent(node.uuid) + ':' + encodeURIComponent(node.password) + '@' +
|
||||
endpoint(node.server, node.port) + (params.toString() ? '?' + params.toString() : '') +
|
||||
'#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertHysteria2(node) {
|
||||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
setIf(params, 'sni', node.sni);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
setIf(params, 'obfs', node.obfs);
|
||||
setIf(params, 'obfs-password', node['obfs-password']);
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('insecure', '1');
|
||||
return 'hysteria2://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||||
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function convertAnytls(node) {
|
||||
requireFields(node, [ 'server', 'port', 'password' ]);
|
||||
const params = new URLSearchParams();
|
||||
setIf(params, 'sni', node.sni || node.servername);
|
||||
setIf(params, 'alpn', arrayValue(node.alpn));
|
||||
setIf(params, 'client-fingerprint', node['client-fingerprint'] || node.fingerprint);
|
||||
if (node['skip-cert-verify'])
|
||||
params.set('insecure', '1');
|
||||
if (node.udp)
|
||||
params.set('udp', '1');
|
||||
return 'anytls://' + encodeURIComponent(node.password) + '@' + endpoint(node.server, node.port) +
|
||||
(params.toString() ? '?' + params.toString() : '') + '#' + encodeURIComponent(node.name || 'Node');
|
||||
}
|
||||
|
||||
function isMetadataProxy(node) {
|
||||
const name = String(node && node.name || '').trim();
|
||||
if (!name)
|
||||
return false;
|
||||
|
||||
return /(?:官网|QQ|流量|续费|应急|重置|到期|过期|剩余|套餐)/i.test(name) ||
|
||||
/^(?:traffic|bandwidth|expire|expiry|expiration|subscription(?:\s+info)?|official\s+website|website)\s*[::|]/i.test(name) ||
|
||||
/^(?:(?:剩余|可用|已用|总)?流量|套餐到期|到期时间|到期日|过期时间|有效期|订阅信息)\s*[::|]/i.test(name) ||
|
||||
/^(?:官方网站|官网地址|官方网址|网站地址)\s*[::|]/i.test(name) ||
|
||||
/^(?:加入|联系|客服)?\s*QQ\s*(?:群|交流群|客服|联系)\s*[::|]/i.test(name);
|
||||
}
|
||||
|
||||
function convertProxy(node) {
|
||||
const type = String(node && node.type || '').toLowerCase();
|
||||
const converters = {
|
||||
ss: convertSs,
|
||||
ssr: convertSsr,
|
||||
vmess: convertVmess,
|
||||
vless: convertVless,
|
||||
trojan: convertTrojan,
|
||||
tuic: convertTuic,
|
||||
hysteria2: convertHysteria2,
|
||||
hy2: convertHysteria2,
|
||||
anytls: convertAnytls
|
||||
};
|
||||
|
||||
try {
|
||||
if (!node || typeof node !== 'object')
|
||||
throw new Error('Node must be an object');
|
||||
if (!converters[type])
|
||||
throw new Error('Unsupported protocol: ' + (type || 'unknown'));
|
||||
return makeResult(node, converters[type](node));
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
name: String(node && node.name || 'Node'),
|
||||
type: type || 'unknown',
|
||||
link: '',
|
||||
error: String(e && e.message || e)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function convertProxies(proxies) {
|
||||
return Array.isArray(proxies) ? proxies.map(convertProxy) : [];
|
||||
}
|
||||
|
||||
function normalizeLink(link) {
|
||||
const value = String(link || '').trim();
|
||||
if (value.indexOf('vmess://') === 0) {
|
||||
try {
|
||||
const payload = JSON.parse(utf8FromBase64(value.slice(8)));
|
||||
delete payload.ps;
|
||||
return 'vmess://' + utf8Base64(JSON.stringify(payload));
|
||||
} catch (e) {}
|
||||
}
|
||||
return value.replace(/#.*$/, '');
|
||||
}
|
||||
|
||||
const api = {
|
||||
convertProxy: convertProxy,
|
||||
convertProxies: convertProxies,
|
||||
isMetadataProxy: isMetadataProxy,
|
||||
normalizeLink: normalizeLink
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
module.exports = api;
|
||||
|
||||
if (typeof baseclass !== 'undefined')
|
||||
return baseclass.extend(api);
|
||||
|
||||
return api;
|
||||
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require uci';
|
||||
'require network';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
'require view.daede.styles as styles';
|
||||
'require view.daede.widgets as widgets';
|
||||
'require view.daede.dae as daeView';
|
||||
'require view.daede.daed as daedView';
|
||||
|
||||
return view.extend({
|
||||
_loadContext: function() {
|
||||
return backend.detectBackend().then(function(ctx) {
|
||||
return uci.load(ctx.backend.uci).catch(function() {}).then(function() {
|
||||
/* enumerate L3 devices so the dae LAN/WAN fields offer a dropdown
|
||||
(still free-text, so undetectable WANs can be typed) */
|
||||
return network.getDevices().catch(function() { return []; }).then(function(devs) {
|
||||
ctx.netDevs = (devs || []).map(function(d) { return d.getName(); })
|
||||
.filter(function(n) { return n && n !== 'dae0'; }).sort();
|
||||
return ctx;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return this._loadContext();
|
||||
},
|
||||
|
||||
render: function(ctx) {
|
||||
const self = this;
|
||||
const themeHref = Array.prototype.map.call(document.styleSheets, function(sheet) { return sheet.href || ''; }).join(' ');
|
||||
document.documentElement.setAttribute('data-daede-theme', /\/argon\//.test(themeHref) ? 'argon' : 'bootstrap');
|
||||
|
||||
/* themes signal dark differently — BootstrapDark sets data-darkmode,
|
||||
Argon just loads a dark stylesheet with no flag. Read the first opaque
|
||||
background up the tree; a transparent body (CSS not applied yet) must
|
||||
NOT count as black, else argon light is misdetected as dark. */
|
||||
try {
|
||||
const probe = [document.body, document.documentElement];
|
||||
for (let i = 0; i < probe.length; i++) {
|
||||
const m = getComputedStyle(probe[i]).backgroundColor.match(/[\d.]+/g);
|
||||
if (!m) continue;
|
||||
const a = m.length >= 4 ? parseFloat(m[3]) : 1;
|
||||
if (a < 0.1) continue; // transparent — keep looking
|
||||
if (0.299 * m[0] + 0.587 * m[1] + 0.114 * m[2] < 128)
|
||||
document.documentElement.setAttribute('data-darkmode', 'true');
|
||||
break; // first opaque background decides
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const redrawBackend = function(name, message) {
|
||||
self._backendHint = message;
|
||||
return self._loadContext()
|
||||
.then(function(nextCtx) { return self.render(nextCtx); })
|
||||
.then(function(nextRoot) {
|
||||
const current = document.querySelector('.dd-config-page');
|
||||
if (!current) return;
|
||||
Array.prototype.forEach.call(current.querySelectorAll('.dd-status-card'), function(card) {
|
||||
if (card._ddCleanup) card._ddCleanup();
|
||||
});
|
||||
current.replaceWith(nextRoot);
|
||||
if (self._backendHintTimer) clearTimeout(self._backendHintTimer);
|
||||
self._backendHintTimer = setTimeout(function() {
|
||||
self._backendHint = '';
|
||||
const hint = document.querySelector('.dd-config-page .dd-backend-help');
|
||||
if (hint) hint.textContent = '';
|
||||
}, 3500);
|
||||
});
|
||||
};
|
||||
|
||||
const listenAddr = uci.get('daed', 'config', 'listen_addr') || backend.BACKENDS.daed.defaultListen;
|
||||
const children = [
|
||||
E('style', {}, styles.CSS),
|
||||
widgets.renderStatusCard(ctx, listenAddr),
|
||||
widgets.renderBackendSwitcher(ctx, redrawBackend, self._backendHint)
|
||||
].filter(function(node) { return !!node; });
|
||||
|
||||
if (!ctx.installed[ctx.name]) {
|
||||
children.push(E('div', { 'class': 'dd-card dd-warning' }, _('Selected backend is not installed. Install dae or daed from the package feed first.')));
|
||||
} else if (ctx.name === 'dae') {
|
||||
children.push(daeView.renderDaeImportBanner());
|
||||
children.push(daeView.renderDaeForms(ctx));
|
||||
children.push(daeView.renderDaeEditor());
|
||||
} else {
|
||||
children.push(daedView.renderDaedSettings());
|
||||
}
|
||||
|
||||
return Promise.all(children.map(function(child) {
|
||||
return child && child.then ? child : Promise.resolve(child);
|
||||
})).then(function(nodes) {
|
||||
return E('div', { 'class': 'dd-wrap dd-config-page' }, nodes.filter(function(n) { return !!n; }));
|
||||
});
|
||||
},
|
||||
|
||||
/* this view drives its own save buttons (dae form / daed settings / manual
|
||||
editor) — suppress LuCI's global Save/Apply/Reset footer so there is one
|
||||
unambiguous primary action per backend */
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,792 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view.daede.backend as backend';
|
||||
|
||||
/* dae 示例里的占位订阅特征 —— example.com、relative/path/to 或中文占位文本 */
|
||||
const RE_PLACEHOLDER_URL = /(['"])(?:(?:https?|https-file|file):\/\/[^'"]*?(?:example\.com|relative\/path\/to)[^'"]*|你的订阅链接或者机场链接)\1/;
|
||||
|
||||
/* Minimal ready-to-run default: gen-dae-config.sh defaults + a placeholder subscription to swap. */
|
||||
const DEFAULT_TEMPLATE =
|
||||
'# luci-app-daede 默认配置:把 subscription 里的占位链接换成你的机场订阅,保存即可运行。\n' +
|
||||
'global {\n' +
|
||||
' # 端口与网卡:lan_interface 填内网桥(通常 br-lan),wan_interface 保持 auto 即可。\n' +
|
||||
' tproxy_port: 12345\n' +
|
||||
' tproxy_port_protect: true\n' +
|
||||
' so_mark_from_dae: 0\n' +
|
||||
' log_level: info\n' +
|
||||
' lan_interface: br-lan\n' +
|
||||
' wan_interface: auto\n' +
|
||||
' auto_config_kernel_parameter: true\n' +
|
||||
' dial_mode: domain\n' +
|
||||
" tcp_check_url: 'http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111'\n" +
|
||||
" udp_check_dns: 'dns.google:53,8.8.8.8,2001:4860:4860::8888'\n" +
|
||||
' check_interval: 30s\n' +
|
||||
' check_tolerance: 50ms\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'subscription {\n' +
|
||||
' # ⚠ 把下面这行的 URL 换成你机场的订阅链接,然后保存。\n' +
|
||||
" my_airport: 'https://example.com/sub'\n" +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'dns {\n' +
|
||||
' # 国内域名走 cndns 解析,其余走 fallbackdns,避免 DNS 污染。\n' +
|
||||
' ipversion_prefer: 4\n' +
|
||||
' response_ttl: 0\n' +
|
||||
' upstream {\n' +
|
||||
" cndns: 'udp://dns.alidns.com:53'\n" +
|
||||
" fallbackdns: 'tcp+udp://dns.google:53'\n" +
|
||||
' }\n' +
|
||||
' routing {\n' +
|
||||
' request {\n' +
|
||||
' qname(geosite:cn) -> cndns\n' +
|
||||
' fallback: fallbackdns\n' +
|
||||
' }\n' +
|
||||
' }\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'group {\n' +
|
||||
' # 节点分组:filter 决定用订阅里的哪些节点,policy 决定怎么选(min_moving_avg = 自动挑最快)。\n' +
|
||||
' proxy {\n' +
|
||||
' filter: subtag(my_airport)\n' +
|
||||
' policy: min_moving_avg\n' +
|
||||
' }\n' +
|
||||
'}\n' +
|
||||
'\n' +
|
||||
'routing {\n' +
|
||||
' # 分流规则:从上往下匹配,命中即停,最后由 fallback 兜底。\n' +
|
||||
' # 想让某个网站直连,在 fallback 之前加一行,例如:domain(example.com) -> direct\n' +
|
||||
' pname(NetworkManager) -> direct\n' +
|
||||
' dip(224.0.0.0/3) -> direct\n' +
|
||||
' dip(255.255.255.255/32) -> direct\n' +
|
||||
" dip('ff00::/8') -> direct\n" +
|
||||
' dip(geoip:private) -> direct\n' +
|
||||
' l4proto(udp) && dport(123) -> direct\n' +
|
||||
' domain(connectivitycheck.gstatic.com) -> direct\n' +
|
||||
' domain(msftconnecttest.com) -> direct\n' +
|
||||
' dip(geoip:cn) -> direct\n' +
|
||||
' domain(geosite:cn) -> direct\n' +
|
||||
' fallback: proxy\n' +
|
||||
'}\n';
|
||||
|
||||
function detectPlaceholders(text) {
|
||||
if (!text) return [];
|
||||
const lines = text.split('\n');
|
||||
const out = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i];
|
||||
const stripped = ln.replace(/^\s*#.*$/, '');
|
||||
if (!stripped) continue;
|
||||
const m = stripped.match(RE_PLACEHOLDER_URL);
|
||||
if (m) out.push({ line: i + 1, url: m[2], raw: ln.trim() });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const GEN = '/usr/share/luci-app-daede/gen-dae-config.sh';
|
||||
|
||||
/* Table cell renderer: truncate long links so rows stay compact; full value
|
||||
stays visible in the edit dialog and on hover. */
|
||||
function ellipsisCell(section_id) {
|
||||
const v = this.cfgvalue(section_id) || '';
|
||||
/* middle-ellipsis keeps the host head + tail visible (the meaningful parts
|
||||
of a sub link) while staying short enough for one line on a phone */
|
||||
const short = v.length > 30 ? v.slice(0, 18) + '…' + v.slice(-9) : v;
|
||||
return E('span', { 'title': v, 'style': 'font-family:ui-monospace,Menlo,monospace;font-size:12px' }, short);
|
||||
}
|
||||
|
||||
/* Tag is auto-generated and rarely relevant to beginners — render it muted. */
|
||||
function mutedCell(section_id) {
|
||||
const v = this.cfgvalue(section_id) || '';
|
||||
return E('span', { 'class': 'dd-meta', 'style': 'opacity:.55;font-size:11.5px' }, v);
|
||||
}
|
||||
|
||||
/* Fill blank tags of a section type with unique <prefix>_<n> values so dae's
|
||||
config generator always has a stable identifier. Runs after m.save() wrote
|
||||
the form values into the uci session, before uci.save(). */
|
||||
function autofillTags(stype, prefix) {
|
||||
const secs = uci.sections('dae', stype) || [];
|
||||
const used = {};
|
||||
secs.forEach(function(s) { if (s.tag) used[s.tag] = 1; });
|
||||
let n = 1;
|
||||
secs.forEach(function(s) {
|
||||
if (s.tag) return;
|
||||
while (used[prefix + '_' + n]) n++;
|
||||
const t = prefix + '_' + n;
|
||||
uci.set('dae', s['.name'], 'tag', t);
|
||||
used[t] = 1; n++;
|
||||
});
|
||||
}
|
||||
|
||||
/* Wrap every form section in a collapsible accordion. openTitles lists the
|
||||
section titles that should start expanded; the rest start collapsed. */
|
||||
function accordionizeSections(mapNode, openTitles) {
|
||||
const secs = mapNode.querySelectorAll('.cbi-section');
|
||||
for (let i = 0; i < secs.length; i++) {
|
||||
const sec = secs[i];
|
||||
const h = sec.querySelector('h3');
|
||||
const title = h ? h.textContent.trim() : _('Section');
|
||||
const open = (openTitles || []).indexOf(title) >= 0;
|
||||
|
||||
const body = E('div', { 'class': 'dd-adv-body' });
|
||||
while (sec.firstChild) body.appendChild(sec.firstChild);
|
||||
/* drop the moved <h3> so it doesn't duplicate the accordion bar title */
|
||||
const dupTitle = body.querySelector('h3');
|
||||
if (dupTitle) dupTitle.parentNode.removeChild(dupTitle);
|
||||
|
||||
const wrap = E('div', { 'class': 'dd-adv' + (open ? '' : ' dd-closed') }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, title),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
body
|
||||
]);
|
||||
wrap.firstChild.addEventListener('click', function() { wrap.classList.toggle('dd-closed'); });
|
||||
sec.appendChild(wrap);
|
||||
}
|
||||
}
|
||||
|
||||
function organizeDaeSections(mapNode, openTitles) {
|
||||
accordionizeSections(mapNode, openTitles);
|
||||
|
||||
const advTitles = [ _('Groups'), _('Routing'), _('DNS'), _('Logging') ];
|
||||
const advWraps = [];
|
||||
mapNode.querySelectorAll('.dd-adv').forEach(function(w) {
|
||||
const t = w.querySelector('.dd-adv-bar span');
|
||||
if (t && advTitles.indexOf(t.textContent.trim()) >= 0) advWraps.push(w);
|
||||
});
|
||||
if (!advWraps.length) return;
|
||||
|
||||
const outerOpen = (openTitles || []).indexOf(_('Advanced settings')) >= 0;
|
||||
const outerBody = E('div', { 'class': 'dd-adv-body' });
|
||||
const outer = E('div', { 'class': 'dd-adv' + (outerOpen ? '' : ' dd-closed') }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, _('Advanced settings')),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
outerBody
|
||||
]);
|
||||
outer.firstChild.addEventListener('click', function() { outer.classList.toggle('dd-closed'); });
|
||||
advWraps[0].parentNode.insertBefore(outer, advWraps[0]);
|
||||
advWraps.forEach(function(w) { outerBody.appendChild(w); });
|
||||
}
|
||||
|
||||
function captureAccordionState(mapNode) {
|
||||
if (!mapNode) return [];
|
||||
return Array.prototype.map.call(
|
||||
mapNode.querySelectorAll('.dd-adv:not(.dd-closed) > .dd-adv-bar span:first-child'),
|
||||
function(node) { return node.textContent.trim(); }
|
||||
);
|
||||
}
|
||||
|
||||
function restoreAccordionState(mapNode, openTitles) {
|
||||
if (!mapNode || mapNode.querySelector('.dd-adv')) return;
|
||||
organizeDaeSections(mapNode, openTitles || []);
|
||||
}
|
||||
|
||||
/* Friendly form UI for the dae backend. Form is the source of truth: on save we
|
||||
commit the `dae` UCI package, then gen-dae-config.sh renders config.dae,
|
||||
validates it and hot-reloads. */
|
||||
function renderDaeForms(ctx) {
|
||||
let m, s, o;
|
||||
m = new form.Map('dae', null, null);
|
||||
|
||||
/* Subscriptions */
|
||||
s = m.section(form.GridSection, 'subscription', _('Subscriptions'), null);
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s.sortable = false;
|
||||
o = s.option(form.Value, 'tag', _('Tag'));
|
||||
o.placeholder = _('auto-generated');
|
||||
o.textvalue = mutedCell;
|
||||
o = s.option(form.Value, 'url', _('Subscription URL'));
|
||||
o.rmempty = false;
|
||||
o.placeholder = 'https://example.com/sub';
|
||||
o.textvalue = ellipsisCell; /* keep long links from blowing up the row */
|
||||
o.validate = function(sid, v) {
|
||||
if (!v) return _('URL is required');
|
||||
if (!/^(https?|https-file|file):\/\//.test(v)) return _('Must start with http(s):// or file://');
|
||||
return true;
|
||||
};
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.editable = true;
|
||||
|
||||
/* Manual nodes (share links) */
|
||||
s = m.section(form.GridSection, 'node', _('Manual nodes'),
|
||||
_('One node share link per line — ss, vmess, vless, trojan, tuic, hysteria2, socks5.'));
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
s.sortable = false;
|
||||
o = s.option(form.Value, 'tag', _('Tag'));
|
||||
o.placeholder = _('auto-generated');
|
||||
o.textvalue = mutedCell;
|
||||
o = s.option(form.Value, 'link', _('Share Link'));
|
||||
o.rmempty = false;
|
||||
o.placeholder = 'vmess://...';
|
||||
o.textvalue = ellipsisCell;
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.editable = true;
|
||||
|
||||
/* Subscription / manual-node tags for the group filter dropdowns */
|
||||
const subTags = (uci.sections('dae', 'subscription') || [])
|
||||
.map(function(x) { return x.tag; }).filter(function(x) { return !!x; });
|
||||
const nodeTags = (uci.sections('dae', 'node') || [])
|
||||
.map(function(x) { return x.tag; }).filter(function(x) { return !!x; });
|
||||
|
||||
/* Groups (outbound) */
|
||||
s = m.section(form.TypedSection, 'group', _('Groups'),
|
||||
_('Outbound groups. Leave the source empty to use all nodes. Set the route fallback to one of these.'));
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
o = s.option(form.Value, 'name', _('Name'));
|
||||
o.rmempty = false;
|
||||
o.default = 'proxy'; /* real default value, editable — a bright placeholder looked filled and tripped the required check */
|
||||
o.placeholder = 'proxy';
|
||||
o.validate = function(sid, v) {
|
||||
if (!v) return _('Name is required');
|
||||
/* dae fatals on duplicate outbound names — keep group names unique */
|
||||
const dup = (uci.sections('dae', 'group') || []).some(function(g) {
|
||||
return g['.name'] !== sid && g.name === v;
|
||||
});
|
||||
return dup ? _('Group name must be unique') : true;
|
||||
};
|
||||
o = s.option(form.ListValue, 'policy', _('Policy'));
|
||||
o.value('min_moving_avg', _('Min moving average latency'));
|
||||
o.value('min', _('Min last latency'));
|
||||
o.value('min_avg10', _('Min average of last 10'));
|
||||
o.value('random', _('Random'));
|
||||
o.value('fixed(0)', _('Fixed (first node)'));
|
||||
o.default = 'min_moving_avg';
|
||||
o = s.option(form.DynamicList, 'source', _('Source'),
|
||||
_('Subscriptions and nodes that feed this group. Pick from the list or type a name. Leave empty to use all nodes.'));
|
||||
subTags.forEach(function(t) { o.value(t, t + ' (' + _('subscription') + ')'); });
|
||||
nodeTags.forEach(function(t) { o.value(t, t + ' (' + _('node') + ')'); });
|
||||
o = s.option(form.DynamicList, 'name_filter', _('Name filter'),
|
||||
_('Optional. Narrows subscriptions in the source to nodes whose name matches — e.g. 香港 or 新加坡|日本. Nodes you picked by name are always kept.'));
|
||||
o.placeholder = '新加坡|日本';
|
||||
|
||||
/* Routing presets */
|
||||
const groupNames = (uci.sections('dae', 'group') || [])
|
||||
.map(function(x) { return x.name; }).filter(function(x) { return !!x; });
|
||||
s = m.section(form.NamedSection, 'routing', 'routing', _('Routing'));
|
||||
s.addremove = false;
|
||||
o = s.option(form.Flag, 'private_direct', _('Private IPs direct'),
|
||||
_('LAN / private addresses bypass the proxy.'));
|
||||
o.default = '1';
|
||||
o = s.option(form.Flag, 'cn_direct', _('China direct'),
|
||||
_('China mainland IPs and domains bypass the proxy.'));
|
||||
o.default = '1';
|
||||
o = s.option(form.Flag, 'block_ads', _('Block ads'),
|
||||
_('Drop traffic matching the ad domain list.'));
|
||||
o.default = '0';
|
||||
o = s.option(form.Value, 'fallback', _('Fallback group'),
|
||||
_('Default outbound for everything else.'));
|
||||
o.default = 'proxy'; /* matches the default proxy group so the box works out of the box */
|
||||
if (groupNames.length)
|
||||
groupNames.forEach(function(n) { o.value(n, n); });
|
||||
else
|
||||
o.value('proxy', 'proxy');
|
||||
o = s.option(form.DynamicList, 'custom', _('Custom rules'),
|
||||
_('Raw dae routing lines, evaluated before fallback. Target must be a group name above (or direct/block).'));
|
||||
o.placeholder = 'dip(geoip:jp) -> proxy';
|
||||
|
||||
/* DNS presets */
|
||||
s = m.section(form.NamedSection, 'dns', 'dns', _('DNS'));
|
||||
s.addremove = false;
|
||||
o = s.option(form.Value, 'cn_upstream', _('China upstream'),
|
||||
_('Resolves China-mainland domains.'));
|
||||
o.default = 'udp://dns.alidns.com:53';
|
||||
o.placeholder = 'udp://dns.alidns.com:53';
|
||||
o = s.option(form.Value, 'fallback_upstream', _('Fallback upstream'),
|
||||
_('Resolves everything else.'));
|
||||
o.default = 'tcp+udp://dns.google:53';
|
||||
o.placeholder = 'tcp+udp://dns.google:53';
|
||||
o = s.option(form.Value, 'response_ttl', _('DNS response TTL'),
|
||||
_('TTL returned to LAN DNS clients. 0 keeps dae default behavior.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '0';
|
||||
o.placeholder = '60';
|
||||
|
||||
/* Logging — folded into the main form so the single Save button covers it
|
||||
too (no separate native save bar) */
|
||||
/* Network interfaces — override dae auto WAN detect, fails on legacy swconfig (#15).
|
||||
Combobox: dropdown of real devices but still free-text for undetectable WANs */
|
||||
const netDevs = (ctx && ctx.netDevs) || [];
|
||||
s = m.section(form.NamedSection, 'config', 'dae', _('Network interfaces'));
|
||||
s.addremove = false;
|
||||
o = s.option(form.Value, 'lan_interface', _('LAN interface'),
|
||||
_('Interface to proxy the LAN from. Usually br-lan.'));
|
||||
o.default = 'br-lan';
|
||||
o.placeholder = 'br-lan';
|
||||
netDevs.forEach(function(d) { o.value(d); });
|
||||
o = s.option(form.Value, 'wan_interface', _('WAN interface'),
|
||||
_('Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set it, e.g. eth0.2.'));
|
||||
o.default = 'auto';
|
||||
o.placeholder = 'auto';
|
||||
o.value('auto', 'auto');
|
||||
netDevs.forEach(function(d) { o.value(d); });
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'dae', _('Logging'));
|
||||
s.addremove = false;
|
||||
o = s.option(form.ListValue, 'log_level', _('Log level'),
|
||||
_('Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting.'));
|
||||
o.value('error', 'ERROR');
|
||||
o.value('warn', 'WARN');
|
||||
o.value('info', 'INFO');
|
||||
o.value('debug', 'DEBUG');
|
||||
o.value('trace', 'TRACE');
|
||||
o.default = 'info';
|
||||
o.rmempty = false;
|
||||
o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'),
|
||||
_('Rotate the log file once it grows past this many megabytes.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
o = s.option(form.Value, 'log_maxbackups', _('Max Log Backups'),
|
||||
_('Number of rotated log files to keep.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
|
||||
const status = E('span', { 'class': 'dd-editor-status' }, '');
|
||||
let renderSaveActions = function() {};
|
||||
let statusTimer = null;
|
||||
function flash(text, kind, hold) {
|
||||
status.textContent = text;
|
||||
status.classList.remove('ok', 'err');
|
||||
if (kind) status.classList.add(kind);
|
||||
status.classList.add('show');
|
||||
if (statusTimer) clearTimeout(statusTimer);
|
||||
statusTimer = setTimeout(function() { status.classList.remove('show'); }, hold || 3000);
|
||||
}
|
||||
|
||||
/* one save pipeline, parameterised by intent:
|
||||
- opts.start: after generate, enable + start dae (stopped → "Save and Start")
|
||||
- otherwise just save + generate; generate hot-reloads itself when dae runs */
|
||||
function doSave(opts, btns) {
|
||||
const settingsCard = status.closest('.dd-settings-card');
|
||||
const openTitles = captureAccordionState(settingsCard && settingsCard.querySelector('.cbi-map'));
|
||||
const restoreLiveAccordions = function() {
|
||||
const liveCard = status.closest('.dd-settings-card');
|
||||
restoreAccordionState(liveCard && liveCard.querySelector('.cbi-map'), openTitles);
|
||||
};
|
||||
btns.forEach(function(b) { b.disabled = true; });
|
||||
flash(_('Saving…'));
|
||||
/* ensure the singleton sections exist before parsing the form —
|
||||
on installs where uci-defaults never seeded them, writing an
|
||||
option to a missing named section fails with ubus NOT_FOUND (4) */
|
||||
if (!uci.get('dae', 'routing')) uci.add('dae', 'routing', 'routing');
|
||||
if (!uci.get('dae', 'dns')) uci.add('dae', 'dns', 'dns');
|
||||
if (!uci.get('dae', 'config')) uci.add('dae', 'dae', 'config');
|
||||
return m.save(null, true)
|
||||
.then(function() {
|
||||
restoreLiveAccordions();
|
||||
/* assign unique tags to any rows the user left blank */
|
||||
autofillTags('subscription', 'subscription');
|
||||
autofillTags('node', 'node');
|
||||
})
|
||||
.then(function() { return uci.save(); })
|
||||
.then(function() {
|
||||
/* apply flushes the rpc session to /etc/config so the
|
||||
generator (reads /etc/config/dae) sees the form data;
|
||||
skip it when nothing changed — apply on an empty
|
||||
changeset throws ubus NO_DATA (code 5). */
|
||||
return uci.changes().then(function(ch) {
|
||||
return (ch && Object.keys(ch).length) ? uci.apply() : null;
|
||||
});
|
||||
})
|
||||
.then(function() { return fs.exec(GEN, ['generate']); })
|
||||
.then(function(res) {
|
||||
if (res && res.code !== 0) {
|
||||
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
|
||||
flash(_('Apply failed: %s').format(err), 'err', 9000);
|
||||
throw new Error('gen-failed');
|
||||
}
|
||||
if (!opts.start) {
|
||||
/* generate already hot-reloaded if dae was running */
|
||||
flash(opts.okMsg || _('Saved'), 'ok');
|
||||
return;
|
||||
}
|
||||
flash(_('Starting…'));
|
||||
return fs.exec('/sbin/uci', ['set', 'dae.config.enabled=1'])
|
||||
.then(function() { return fs.exec('/sbin/uci', ['commit', 'dae']); })
|
||||
.then(function() { return fs.exec(backend.BACKENDS.dae.initd, ['enable']); })
|
||||
.then(function() { return fs.exec(backend.BACKENDS.dae.initd, ['start']); })
|
||||
.then(function(r2) {
|
||||
if (r2 && r2.code !== 0)
|
||||
flash(_('Start failed: %s').format(r2.stderr || r2.stdout || ('exit ' + r2.code)), 'err', 9000);
|
||||
else {
|
||||
flash(_('Saved · started'), 'ok');
|
||||
renderSaveActions(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (e && e.message === 'gen-failed') return;
|
||||
if (e && e.name === 'CBIValidationError') {
|
||||
flash(_('Please fix the highlighted fields.'), 'err', 6000);
|
||||
return;
|
||||
}
|
||||
flash(_('Save failed: %s').format(e.message || e), 'err', 9000);
|
||||
})
|
||||
.finally(function() {
|
||||
restoreLiveAccordions();
|
||||
btns.forEach(function(b) { b.disabled = false; });
|
||||
window.setTimeout(function() { ui.changes.init(); }, 1800);
|
||||
});
|
||||
}
|
||||
|
||||
return m.render().then(function(mapNode) {
|
||||
|
||||
/* beginner default: only Subscriptions starts open; nodes/groups/routing/dns/logging collapse */
|
||||
organizeDaeSections(mapNode, [ _('Subscriptions') ]);
|
||||
|
||||
const cardChildren = [ E('h4', { 'class': 'dd-card-title' }, _('dae Configuration')) ];
|
||||
cardChildren.push(mapNode);
|
||||
|
||||
/* state-aware primary action: running → one "Save and Apply"; stopped →
|
||||
"Save config" (secondary) + "Save and Start" (primary). Starting the
|
||||
proxy is an explicit choice, never an implicit side effect of saving. */
|
||||
return backend.detectRunning().then(function(rr) {
|
||||
const running = rr && rr.dae;
|
||||
|
||||
/* "Update all subscriptions" only helps while dae runs (hot reload
|
||||
re-pulls them). A stopped dae re-fetches every subscription on
|
||||
start, so there is nothing to update — hide the button then. */
|
||||
if (running) {
|
||||
const upBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Update all subscriptions'));
|
||||
const upMsg = E('span', { 'class': 'dd-meta', 'style': 'margin-left:8px;display:none' }, '');
|
||||
const setUp = function(text, kind) {
|
||||
upMsg.textContent = text;
|
||||
upMsg.style.display = '';
|
||||
upMsg.style.color = (kind === 'err') ? 'var(--error-color, #d33)' : 'var(--success-color, #2a8)';
|
||||
if (kind !== 'err')
|
||||
setTimeout(function() { if (upMsg.textContent === text) upMsg.style.display = 'none'; }, 5000);
|
||||
};
|
||||
upBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
upBtn.disabled = true;
|
||||
setUp(_('Updating…'));
|
||||
fs.exec(backend.BACKENDS.dae.initd, ['hot_reload']).then(function(res) {
|
||||
if (res && res.code !== 0)
|
||||
setUp(_('Update failed: %s').format(res.stderr || res.stdout || ('exit ' + res.code)), 'err');
|
||||
else
|
||||
setUp(_('Subscriptions updated (dae reloaded)'), 'ok');
|
||||
}).catch(function(e) {
|
||||
setUp(_('Update failed: %s').format(e.message || e), 'err');
|
||||
}).finally(function() { upBtn.disabled = false; });
|
||||
});
|
||||
const upWrap = E('div', { 'class': 'dd-actions', 'style': 'margin:0 0 8px' }, [ upBtn, upMsg ]);
|
||||
const advs = mapNode.querySelectorAll('.dd-adv');
|
||||
for (let i = 0; i < advs.length; i++) {
|
||||
const t = advs[i].querySelector('.dd-adv-bar span');
|
||||
if (t && t.textContent.trim() === _('Subscriptions')) {
|
||||
const body = advs[i].querySelector('.dd-adv-body');
|
||||
const tbl = body && body.querySelector('.cbi-section-table');
|
||||
if (tbl) tbl.parentNode.insertBefore(upWrap, tbl);
|
||||
else if (body) body.insertBefore(upWrap, body.firstChild);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actionsWrap = E('div', { 'class': 'dd-editor-actions' });
|
||||
renderSaveActions = function(isRunning) {
|
||||
while (actionsWrap.firstChild) actionsWrap.removeChild(actionsWrap.firstChild);
|
||||
let btns;
|
||||
if (isRunning) {
|
||||
const apply = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Apply'));
|
||||
btns = [ apply ];
|
||||
apply.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved · applied') }, btns); });
|
||||
actionsWrap.appendChild(apply);
|
||||
} else {
|
||||
const saveOnly = E('button', { 'class': 'cbi-button' }, _('Save config'));
|
||||
const saveStart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Start'));
|
||||
btns = [ saveOnly, saveStart ];
|
||||
saveOnly.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ okMsg: _('Saved') }, btns); });
|
||||
saveStart.addEventListener('click', function(ev) { ev.preventDefault(); doSave({ start: true }, btns); });
|
||||
actionsWrap.appendChild(saveOnly);
|
||||
actionsWrap.appendChild(saveStart);
|
||||
}
|
||||
actionsWrap.appendChild(status);
|
||||
};
|
||||
renderSaveActions(running);
|
||||
cardChildren.push(actionsWrap);
|
||||
return E('div', { 'class': 'dd-card dd-settings-card' }, cardChildren);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* Import banner: offer to pull subscription/node from an existing hand-written
|
||||
config.dae into the form when the form is still empty. */
|
||||
function renderDaeImportBanner() {
|
||||
const hasForms = (uci.sections('dae', 'subscription') || []).length
|
||||
|| (uci.sections('dae', 'node') || []).length;
|
||||
if (hasForms)
|
||||
return Promise.resolve(null);
|
||||
|
||||
/* stat first (ubus): avoid a 404 when config.dae doesn't exist yet */
|
||||
return fs.stat(backend.BACKENDS.dae.config).then(function() {
|
||||
return fs.read_direct(backend.BACKENDS.dae.config, 'text');
|
||||
}).then(function(content) {
|
||||
if (!content || !/\b(subscription|node)\s*\{/.test(content))
|
||||
return null;
|
||||
|
||||
const btn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Import from existing config'));
|
||||
const note = E('span', { 'class': 'dd-meta', style: 'margin-left:8px' }, '');
|
||||
btn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
btn.disabled = true;
|
||||
note.textContent = _('Importing…');
|
||||
fs.exec(GEN, ['import']).then(function(res) {
|
||||
if (res && res.code === 0) {
|
||||
note.textContent = _('Imported, reloading…');
|
||||
setTimeout(function() { window.location.reload(); }, 600);
|
||||
} else {
|
||||
note.textContent = _('Import failed');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}).catch(function() { note.textContent = _('Import failed'); btn.disabled = false; });
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'dd-card', style: 'border-color:rgba(217,158,0,.4)' }, [
|
||||
E('div', { 'class': 'dd-ph-warn-title' }, _('Existing config detected')),
|
||||
E('div', { 'class': 'dd-settings-descr', style: 'margin:4px 0 8px' },
|
||||
_('You have a hand-written config.dae. Import its subscriptions and nodes into the form, or keep editing manually below. Saving the form overwrites config.dae.')),
|
||||
E('div', { 'class': 'dd-actions' }, [ btn, note ])
|
||||
]);
|
||||
}).catch(function() { return null; });
|
||||
}
|
||||
|
||||
/* Lightweight dae-DSL syntax highlighter: tokenises into spans for a <pre>
|
||||
layer drawn behind a transparent textarea (no editor library needed). */
|
||||
function highlightDae(src) {
|
||||
const re = /(#[^\n]*)|('[^'\n]*')|\b(global|subscription|node|group|routing|dns|include|upstream|request|response)\b|\b([A-Za-z_][\w-]*)(?=\s*\()|(->|&&|\|\|)|\b(geoip|geosite)(?=:)/g;
|
||||
let out = '', last = 0, m;
|
||||
const esc = function(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); };
|
||||
while ((m = re.exec(src)) !== null) {
|
||||
out += esc(src.slice(last, m.index));
|
||||
const cls = m[1] ? 'c' : m[2] ? 's' : m[3] ? 'k' : m[4] ? 'f' : m[5] ? 'o' : 'g';
|
||||
out += '<span class="dh-' + cls + '">' + esc(m[0]) + '</span>';
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
out += esc(src.slice(last));
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderDaeEditor() {
|
||||
const textarea = E('textarea', {
|
||||
'class': 'dd-editor dd-editor-hl',
|
||||
'spellcheck': 'false',
|
||||
'wrap': 'off',
|
||||
'placeholder': _('dae config file is empty. Click "Load default config" to start, or paste your config here.')
|
||||
}, '');
|
||||
const hlCode = E('code', {});
|
||||
const hlPre = E('pre', { 'class': 'dd-hl', 'aria-hidden': 'true' }, hlCode);
|
||||
const editWrap = E('div', { 'class': 'dd-edit-wrap' }, [ hlPre, textarea ]);
|
||||
function syncHL() {
|
||||
hlCode.innerHTML = highlightDae(textarea.value) + '\n';
|
||||
hlPre.scrollTop = textarea.scrollTop;
|
||||
hlPre.scrollLeft = textarea.scrollLeft;
|
||||
}
|
||||
textarea.addEventListener('scroll', function() {
|
||||
hlPre.scrollTop = textarea.scrollTop;
|
||||
hlPre.scrollLeft = textarea.scrollLeft;
|
||||
});
|
||||
const save = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save manual config'));
|
||||
const init = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Load default config'));
|
||||
const status = E('span', { 'class': 'dd-editor-status' }, '');
|
||||
|
||||
const phWarn = E('div', { 'class': 'dd-ph-warn' }, [
|
||||
E('div', { 'class': 'dd-ph-warn-title' }, ''),
|
||||
E('div', { 'class': 'dd-ph-howto' }, ''),
|
||||
E('div', { 'class': 'dd-ph-list-label' }, _('Placeholder lines (click line number to jump):')),
|
||||
E('ul', { 'class': 'dd-ph-list' })
|
||||
]);
|
||||
|
||||
/* Footer:dae version · 行数 · 占位剩余 */
|
||||
const fbVer = E('span', { 'class': 'dd-fb-item' }, [ E('span', { 'class': 'dd-fb-key' }, 'dae'), E('span', {}, '—') ]);
|
||||
const fbLine = E('span', { 'class': 'dd-fb-item' }, [ E('span', { 'class': 'dd-fb-key' }, _('lines')), E('span', {}, '0') ]);
|
||||
const fbStat = E('span', { 'class': 'dd-fb-item dd-fb-ok' }, '✓ ready');
|
||||
const footer = E('div', { 'class': 'dd-editor-footer' }, [ fbVer, fbLine, fbStat ]);
|
||||
|
||||
let statusTimer = null;
|
||||
function flashStatus(text, kind, holdMs) {
|
||||
status.textContent = text;
|
||||
status.classList.remove('ok', 'err');
|
||||
if (kind) status.classList.add(kind);
|
||||
status.classList.add('show');
|
||||
if (statusTimer) clearTimeout(statusTimer);
|
||||
statusTimer = setTimeout(function() {
|
||||
status.classList.remove('show');
|
||||
}, holdMs || 3000);
|
||||
}
|
||||
|
||||
function jumpToLine(lineNo) {
|
||||
const lines = textarea.value.split('\n');
|
||||
let offset = 0;
|
||||
for (let i = 0; i < lineNo - 1 && i < lines.length; i++)
|
||||
offset += lines[i].length + 1;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(offset, offset + (lines[lineNo - 1] || '').length);
|
||||
const lineHeight = 18;
|
||||
textarea.scrollTop = Math.max(0, (lineNo - 4) * lineHeight);
|
||||
}
|
||||
|
||||
function refreshFooter(phCount) {
|
||||
const lines = textarea.value ? textarea.value.split('\n').length : 0;
|
||||
fbLine.lastChild.textContent = String(lines);
|
||||
while (fbStat.firstChild) fbStat.removeChild(fbStat.firstChild);
|
||||
fbStat.classList.remove('dd-fb-ok', 'dd-fb-warn');
|
||||
if (phCount > 0) {
|
||||
fbStat.classList.add('dd-fb-warn');
|
||||
fbStat.textContent = _('⚠ %d placeholder').format(phCount);
|
||||
} else {
|
||||
fbStat.classList.add('dd-fb-ok');
|
||||
fbStat.textContent = lines > 0 ? '✓ ready' : '— empty';
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPlaceholders() {
|
||||
syncHL();
|
||||
const hits = detectPlaceholders(textarea.value);
|
||||
const titleEl = phWarn.querySelector('.dd-ph-warn-title');
|
||||
const howtoEl = phWarn.querySelector('.dd-ph-howto');
|
||||
const listEl = phWarn.querySelector('.dd-ph-list');
|
||||
while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
|
||||
refreshFooter(hits.length);
|
||||
if (!hits.length) {
|
||||
phWarn.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
titleEl.textContent = _('⚠ Still needs a subscription link: %d placeholder URL line(s) detected').format(hits.length);
|
||||
while (howtoEl.firstChild) howtoEl.removeChild(howtoEl.firstChild);
|
||||
/* assemble rich text from DOM nodes so the code spans keep their styling */
|
||||
howtoEl.appendChild(document.createTextNode(_('These ')));
|
||||
const c1 = document.createElement('code'); c1.textContent = 'example.com'; howtoEl.appendChild(c1);
|
||||
howtoEl.appendChild(document.createTextNode(' / '));
|
||||
const c2 = document.createElement('code'); c2.textContent = 'relative/path/to'; howtoEl.appendChild(c2);
|
||||
howtoEl.appendChild(document.createTextNode(_(' are placeholder addresses from dae\'s template, not real subscriptions. Replace the URL on any one line with your airport subscription link (delete the spare lines) and save.')));
|
||||
hits.forEach(function(h) {
|
||||
const lnSpan = E('span', { 'class': 'dd-ph-ln', 'title': _('Jump to line') }, 'L' + h.line);
|
||||
lnSpan.addEventListener('click', function() { jumpToLine(h.line); });
|
||||
const txt = E('span', { 'class': 'dd-ph-txt' }, h.url);
|
||||
listEl.appendChild(E('li', {}, [ lnSpan, txt ]));
|
||||
});
|
||||
phWarn.classList.add('show');
|
||||
}
|
||||
|
||||
textarea.addEventListener('input', refreshPlaceholders);
|
||||
|
||||
function loadConfig() {
|
||||
/* stat first (ubus, no HTTP): a fresh install has no config.dae yet, and
|
||||
reading a missing file via fs.read_direct logs a noisy 404 */
|
||||
return fs.stat(backend.BACKENDS.dae.config).then(function() {
|
||||
return fs.read_direct(backend.BACKENDS.dae.config, 'text');
|
||||
}).then(function(content) {
|
||||
textarea.value = content || '';
|
||||
}).catch(function() {
|
||||
textarea.value = '';
|
||||
}).finally(refreshPlaceholders);
|
||||
}
|
||||
|
||||
/* 校验流程:先写到 /tmp/dae-validate.dae 干测 → dae validate 通过才覆盖正式 config */
|
||||
const VALIDATE_PATH = '/tmp/dae-validate.dae';
|
||||
|
||||
save.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
save.disabled = true;
|
||||
flashStatus(_('Validating…'));
|
||||
fs.write(VALIDATE_PATH, textarea.value, 384)
|
||||
.then(function() { return fs.exec('/usr/bin/dae', ['validate', '-c', VALIDATE_PATH]); })
|
||||
.then(function(res) {
|
||||
if (res && res.code !== 0) {
|
||||
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
|
||||
flashStatus(_('Validate failed: %s').format(err), 'err', 8000);
|
||||
throw new Error('validate-failed');
|
||||
}
|
||||
flashStatus(_('Saving…'));
|
||||
return fs.write(backend.BACKENDS.dae.config, textarea.value, 384);
|
||||
})
|
||||
.then(function() { return backend.detectRunning(); })
|
||||
.then(function(running) {
|
||||
if (running && running.dae)
|
||||
return fs.exec(backend.BACKENDS.dae.initd, ['hot_reload']).then(function(res) {
|
||||
if (res && res.code !== 0)
|
||||
flashStatus(_('Reload failed: %s').format(res.stderr || res.stdout || ('exit ' + res.code)), 'err', 8000);
|
||||
else
|
||||
flashStatus(_('Validated · saved · hot-reloaded'), 'ok');
|
||||
});
|
||||
flashStatus(_('Saved OK (start dae to apply)'), 'ok');
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (e && e.message === 'validate-failed') return;
|
||||
flashStatus(_('Save failed: %s').format(e.message || e), 'err', 8000);
|
||||
})
|
||||
.finally(function() {
|
||||
save.disabled = false;
|
||||
fs.exec('/bin/rm', ['-f', VALIDATE_PATH]).catch(function() {});
|
||||
});
|
||||
});
|
||||
|
||||
init.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (textarea.value.trim() && !confirm(_('This will replace your current config. Continue?')))
|
||||
return;
|
||||
textarea.value = DEFAULT_TEMPLATE;
|
||||
refreshPlaceholders();
|
||||
flashStatus(_('Default config loaded — replace the subscription link, then save'), 'ok');
|
||||
});
|
||||
|
||||
/* 一次性探测 dae --version;失败就显示 unknown,不影响主流程 */
|
||||
fs.exec('/usr/bin/dae', ['--version']).then(function(res) {
|
||||
const out = (res && res.stdout || '').trim();
|
||||
const m = out.match(/dae\s+version\s+(\S+)/);
|
||||
fbVer.lastChild.textContent = m ? m[1].split('-')[0] : 'unknown';
|
||||
}).catch(function() {
|
||||
fbVer.lastChild.textContent = 'unknown';
|
||||
});
|
||||
|
||||
loadConfig();
|
||||
|
||||
return E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Advanced / Manual Mode')),
|
||||
(function() {
|
||||
var adv = E('div', { 'class': 'dd-adv dd-closed', style: 'margin-top:8px' }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, _('Configuration Editor')),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
E('div', { 'class': 'dd-adv-body' }, [
|
||||
E('div', { 'class': 'dd-editor-hint', style: 'margin:0 0 10px' }, [
|
||||
E('b', {}, _('Text-only mode.')),
|
||||
' ',
|
||||
_('Edit the dae DSL directly. "Load default config" gives a ready-to-run starting point. Use daed for a GUI.')
|
||||
]),
|
||||
phWarn,
|
||||
editWrap,
|
||||
footer,
|
||||
E('div', { 'class': 'dd-editor-actions' }, [ save, init, status ])
|
||||
])
|
||||
]);
|
||||
adv.firstChild.addEventListener('click', function() { adv.classList.toggle('dd-closed'); });
|
||||
return adv;
|
||||
})()
|
||||
]);
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
renderDaeForms: renderDaeForms,
|
||||
|
||||
renderDaeImportBanner: renderDaeImportBanner,
|
||||
|
||||
renderDaeEditor: renderDaeEditor
|
||||
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
const TOKEN_KEY = 'luci-app-daede.daed-token';
|
||||
|
||||
function clear(storage) {
|
||||
try { storage.removeItem(TOKEN_KEY); } catch (e) {}
|
||||
}
|
||||
|
||||
function tokenExpiry(token) {
|
||||
try {
|
||||
const part = String(token || '').split('.')[1];
|
||||
if (!part)
|
||||
return 0;
|
||||
const normalized = part.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const payload = JSON.parse(atob(normalized));
|
||||
return Number(payload.exp) || 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function load(storage, now) {
|
||||
let token = '';
|
||||
try { token = storage.getItem(TOKEN_KEY) || ''; } catch (e) { return ''; }
|
||||
const current = now == null ? Date.now() / 1000 : Number(now);
|
||||
if (!token || tokenExpiry(token) <= current + 30) {
|
||||
clear(storage);
|
||||
return '';
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function save(storage, token) {
|
||||
try { storage.setItem(TOKEN_KEY, String(token || '')); } catch (e) {}
|
||||
}
|
||||
|
||||
function isAccessDenied(error) {
|
||||
return /access denied|unauthori[sz]ed|invalid token/i.test(String(error && error.message || error || ''));
|
||||
}
|
||||
|
||||
const api = {
|
||||
TOKEN_KEY: TOKEN_KEY,
|
||||
load: load,
|
||||
save: save,
|
||||
clear: clear,
|
||||
isAccessDenied: isAccessDenied
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
module.exports = api;
|
||||
|
||||
if (typeof baseclass !== 'undefined')
|
||||
return baseclass.extend(api);
|
||||
|
||||
return api;
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require view.daede.widgets as widgets';
|
||||
|
||||
function applyUciChanges() {
|
||||
return uci.save().then(function() {
|
||||
return uci.changes().then(function(ch) {
|
||||
return (ch && Object.keys(ch).length) ? uci.apply() : null;
|
||||
});
|
||||
}).then(function() {
|
||||
return new Promise(function(resolve) { window.setTimeout(resolve, 1800); });
|
||||
}).then(function() {
|
||||
return ui.changes.init();
|
||||
});
|
||||
}
|
||||
|
||||
function renderDaedSettings() {
|
||||
let m, s, o;
|
||||
m = new form.Map('daed', null, null);
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'daed');
|
||||
s.addremove = false;
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'listen_addr', _('Listen Address'));
|
||||
o.datatype = 'ipaddrport(1)';
|
||||
o.default = '0.0.0.0:2023';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'dashboard_username', _('daed Username'),
|
||||
_('Used by LuCI to access daed (subscription updates and import). If daed is not initialized yet, this account is used to create the daed admin.'));
|
||||
o.placeholder = 'admin';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'dashboard_password', _('daed Password'));
|
||||
o.password = true;
|
||||
|
||||
o = s.option(form.Flag, 'subscribe_auto_update', _('Enable subscription auto-update'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.ListValue, 'subscribe_update_cycle', _('Update Cycle'));
|
||||
o.value('daily', _('Daily'));
|
||||
o.value('weekly', _('Weekly'));
|
||||
o.default = 'daily';
|
||||
o.depends('subscribe_auto_update', '1');
|
||||
|
||||
o = s.option(form.ListValue, 'subscribe_update_hour', _('Update Time'));
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const hh = ('0' + h).slice(-2);
|
||||
o.value(String(h), hh + ':00');
|
||||
}
|
||||
o.default = '4';
|
||||
o.depends('subscribe_auto_update', '1');
|
||||
|
||||
o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'),
|
||||
_('Rotate the log file once it grows past this many megabytes.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
|
||||
o = s.option(form.Value, 'log_maxbackups', _('Max Log Backups'),
|
||||
_('Number of rotated log files to keep.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
|
||||
return widgets.wrapSettingsCard(
|
||||
_('daede Settings'),
|
||||
null,
|
||||
m.render(),
|
||||
_('Log Settings'),
|
||||
['log_maxsize', 'log_maxbackups']
|
||||
).then(function(card) {
|
||||
/* native Save/Apply footer is suppressed view-wide, so daed carries its
|
||||
own primary action */
|
||||
const status = E('span', { 'class': 'dd-editor-status' }, '');
|
||||
let statusTimer = null;
|
||||
function flash(text, kind, hold) {
|
||||
status.textContent = text;
|
||||
status.classList.remove('ok', 'err');
|
||||
if (kind) status.classList.add(kind);
|
||||
status.classList.add('show');
|
||||
if (statusTimer) clearTimeout(statusTimer);
|
||||
statusTimer = setTimeout(function() { status.classList.remove('show'); }, hold || 3000);
|
||||
}
|
||||
|
||||
const save = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Save and Apply'));
|
||||
const update = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Update subscriptions now'));
|
||||
update.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
update.disabled = true;
|
||||
flash(_('Updating daed subscriptions…'));
|
||||
fs.exec('/usr/share/luci-app-daede/daed-sub-update.sh', []).then(function(res) {
|
||||
if (res && res.code !== 0) {
|
||||
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
|
||||
flash(_('Subscription update failed: %s').format(err), 'err', 9000);
|
||||
} else {
|
||||
flash(_('Subscriptions updated and applied'), 'ok');
|
||||
}
|
||||
}).catch(function(e) {
|
||||
flash(_('Subscription update failed: %s').format(e.message || e), 'err', 9000);
|
||||
}).finally(function() { update.disabled = false; });
|
||||
});
|
||||
save.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
save.disabled = true;
|
||||
m.save(null, true)
|
||||
.then(applyUciChanges)
|
||||
.then(function() {
|
||||
const enabled = uci.get('daed', 'config', 'subscribe_auto_update') === '1';
|
||||
return fs.exec('/usr/share/luci-app-daede/daed-sub-cron.sh', [ enabled ? 'enable' : 'disable' ]);
|
||||
})
|
||||
.then(function(res) {
|
||||
if (res && res.code !== 0)
|
||||
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
|
||||
})
|
||||
.then(function() {
|
||||
/* no success popup — the reload is feedback enough */
|
||||
setTimeout(function() { window.location.reload(); }, 500);
|
||||
})
|
||||
.catch(function(e) {
|
||||
if (e && e.name === 'CBIValidationError')
|
||||
flash(_('Please fix the highlighted fields.'), 'err', 6000);
|
||||
else
|
||||
flash(_('Save failed: %s').format(e.message || e), 'err', 9000);
|
||||
})
|
||||
.finally(function() { save.disabled = false; });
|
||||
});
|
||||
card.appendChild(E('div', { 'class': 'dd-editor-actions' }, [ update, save, status ]));
|
||||
return card;
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({ renderDaedSettings: renderDaedSettings });
|
||||
@@ -0,0 +1,413 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
'require view.daede.styles as styles';
|
||||
|
||||
const MAX_LINES = 5000;
|
||||
|
||||
function execChecked(command, args) {
|
||||
return fs.exec(command, args || []).then(function(res) {
|
||||
if (res && res.code !== 0)
|
||||
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
const CSS = [
|
||||
'.dd-log-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-log-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:10px 14px;margin-bottom:10px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
'.dd-log-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:0 0 8px;margin-bottom:8px;border-bottom:1px dashed rgba(128,128,128,.2)}',
|
||||
'.dd-log-toolbar label{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;cursor:pointer;margin:0;opacity:.85}',
|
||||
/* Argon shifts label>checkbox down by top:.4rem; reset so flex centers it with the text */
|
||||
'.dd-log-toolbar input[type="checkbox"]{position:static;top:auto;right:auto;margin:0}',
|
||||
'.dd-log-toolbar input[type="text"]{font-size:11.5px;padding:4px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;min-width:160px}',
|
||||
'.dd-log-toolbar .dd-log-btn{font-size:11.5px;line-height:1.4;min-height:0;height:auto;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit;cursor:pointer}',
|
||||
'.dd-log-toolbar .dd-log-btn:hover{background:rgba(128,128,128,.1)}',
|
||||
'.dd-log-toolbar .dd-log-stop{color:#d9534f;border-color:rgba(217,83,79,.45)}',
|
||||
'.dd-log-toolbar .dd-log-meta{margin-left:auto;font-size:10.5px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
|
||||
'.dd-log-pane{height:60vh;min-height:360px;overflow:auto;padding:8px 10px;border:1px solid rgba(0,0,0,.08);border-radius:7px;background:#1a1d21;color:#d8dde6;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-all}',
|
||||
'.dd-log-pane .dd-line{padding:1px 4px;border-radius:3px;display:block}',
|
||||
'.dd-log-pane .dd-line.dd-info{color:#cfd6df}',
|
||||
'.dd-log-pane .dd-line.dd-warn{color:#e8b95a}',
|
||||
'.dd-log-pane .dd-line.dd-error{color:#ea7878;background:rgba(234,120,120,.06)}',
|
||||
'.dd-log-pane .dd-line.dd-debug{color:#7a8290;opacity:.7}',
|
||||
'.dd-log-pane .dd-line.dd-trace{color:#68717d;opacity:.62}',
|
||||
'.dd-log-pane .dd-line.dd-hidden{display:none}',
|
||||
'.dd-log-pane .dd-empty{opacity:.5;font-style:italic}',
|
||||
/* 简化后的字段视觉 */
|
||||
'.dd-log-pane .dd-ts{color:#6b7480;margin-right:8px;white-space:nowrap}',
|
||||
'.dd-log-pane .dd-lvl{display:inline-block;min-width:38px;padding:0 5px;margin-right:8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:.4px;text-align:center;vertical-align:1px}',
|
||||
'.dd-log-pane .dd-lvl-info{color:#7fc7a8;background:rgba(127,199,168,.08)}',
|
||||
'.dd-log-pane .dd-lvl-warn{color:#e8b95a;background:rgba(232,185,90,.10)}',
|
||||
'.dd-log-pane .dd-lvl-error{color:#ea7878;background:rgba(234,120,120,.10)}',
|
||||
'.dd-log-pane .dd-lvl-debug{color:#7a8290;background:rgba(122,130,144,.10)}',
|
||||
'.dd-log-pane .dd-lvl-trace{color:#68717d;background:rgba(104,113,125,.10)}',
|
||||
'.dd-log-pane .dd-msg{color:inherit}',
|
||||
'.dd-log-pane .dd-kv{margin-left:6px;color:#6b7480;font-size:11px;opacity:.85}',
|
||||
'body.dark .dd-log-card,html[data-theme="dark"] .dd-log-card,html[data-bs-theme="dark"] .dd-log-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}',
|
||||
'body.dark .dd-log-pane,html[data-theme="dark"] .dd-log-pane,html[data-bs-theme="dark"] .dd-log-pane{border-color:rgba(255,255,255,.1)}'
|
||||
].join('');
|
||||
|
||||
/* 拆字段:time="May 25 07:04:59" level=info msg="..." key=val key="val with space" ... */
|
||||
const RE_LINE = /^time="([^"]*)"\s+level=(\w+)\s+msg=(?:"((?:[^"\\]|\\.)*)"|(\S+))\s*(.*)$/;
|
||||
const RE_PREFIXED_LINE = /^\[([^\]]+)\]\s+(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s*(.*)$/i;
|
||||
const RE_PLAIN_LEVEL_LINE = /^\s*(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\s+(.*)$/i;
|
||||
|
||||
function detectLevel(line) {
|
||||
// daed/dae logs use lvl=info / [INFO] / level=warning style
|
||||
const m = line.match(/\b(TRACE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i);
|
||||
if (!m) return '';
|
||||
const lvl = m[1].toUpperCase();
|
||||
if (lvl === 'TRACE') return 'dd-trace';
|
||||
if (lvl === 'DEBUG') return 'dd-debug';
|
||||
if (lvl === 'INFO') return 'dd-info';
|
||||
if (lvl.startsWith('WARN')) return 'dd-warn';
|
||||
return 'dd-error';
|
||||
}
|
||||
|
||||
function matchesFilter(line, filter) {
|
||||
if (!filter)
|
||||
return true;
|
||||
|
||||
if (filter === 'trace' || filter === 'debug' || filter === 'info' || filter === 'warn' || filter === 'error')
|
||||
return detectLevel(line) === 'dd-' + filter;
|
||||
|
||||
if (filter === 'node_status')
|
||||
return /\balive\b/i.test(line)
|
||||
|| /\bgroup\b.*\bselects dialer\b/i.test(line)
|
||||
|| /\bgroup\s+'[^']+'\s+\[[^\]]+\]:/i.test(line);
|
||||
|
||||
if (filter === 'proxy_traffic')
|
||||
return line.indexOf('<->') !== -1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 格式化时间戳并按北京时间显示:
|
||||
* - daed 默认输出 UTC ISO 8601 (e.g. "2026-05-28T19:07:54Z"),转成北京时间
|
||||
* - 新格式和旧 logrus 短格式无时区信息,保留原始完整日期时间
|
||||
*/
|
||||
function formatTs(raw) {
|
||||
if (!raw) return raw;
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/.test(raw)) {
|
||||
const d = new Date(raw);
|
||||
if (!isNaN(d.getTime())) {
|
||||
const beijing = new Date(d.getTime() + 8 * 60 * 60 * 1000);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return [
|
||||
beijing.getUTCFullYear(),
|
||||
pad(beijing.getUTCMonth() + 1),
|
||||
pad(beijing.getUTCDate())
|
||||
].join('-') + ' ' + [
|
||||
pad(beijing.getUTCHours()),
|
||||
pad(beijing.getUTCMinutes()),
|
||||
pad(beijing.getUTCSeconds())
|
||||
].join(':');
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
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 'dd-lvl-trace';
|
||||
if (u === 'DEBUG') return 'dd-lvl-debug';
|
||||
if (u === 'INFO') return 'dd-lvl-info';
|
||||
if (u === 'WARN' || u === 'WARNING') return 'dd-lvl-warn';
|
||||
return 'dd-lvl-error';
|
||||
}
|
||||
|
||||
function parseLine(line) {
|
||||
let m = line.match(RE_LINE);
|
||||
if (m) {
|
||||
return {
|
||||
ts: formatTs(m[1]),
|
||||
lvl: m[2],
|
||||
msg: m[3] !== undefined ? m[3] : (m[4] || ''),
|
||||
kv: (m[5] || '').trim()
|
||||
};
|
||||
}
|
||||
|
||||
m = line.match(RE_PREFIXED_LINE);
|
||||
let ts = '', lvl = '', body = '';
|
||||
if (m) {
|
||||
ts = formatTs(m[1]);
|
||||
lvl = m[2];
|
||||
body = m[3] || '';
|
||||
} else {
|
||||
m = line.match(RE_PLAIN_LEVEL_LINE);
|
||||
if (!m) return null;
|
||||
lvl = m[1];
|
||||
body = m[2] || '';
|
||||
}
|
||||
const kvStart = body.search(/(?:^|\s)(?=[A-Za-z_][\w.-]*=)/);
|
||||
return {
|
||||
ts: ts,
|
||||
lvl: lvl,
|
||||
msg: kvStart === -1 ? body : body.slice(0, kvStart).trimEnd(),
|
||||
kv: kvStart === -1 ? '' : body.slice(kvStart).trim()
|
||||
};
|
||||
}
|
||||
|
||||
function buildLine(ln) {
|
||||
const cls = detectLevel(ln);
|
||||
const parsed = parseLine(ln);
|
||||
if (!parsed) {
|
||||
return E('div', { 'class': 'dd-line ' + cls }, ln);
|
||||
}
|
||||
const parts = [];
|
||||
if (parsed.ts) parts.push(E('span', { 'class': 'dd-ts' }, parsed.ts));
|
||||
parts.push(E('span', { 'class': 'dd-lvl ' + lvlClass(parsed.lvl) }, lvlShort(parsed.lvl)));
|
||||
parts.push(E('span', { 'class': 'dd-msg' }, parsed.msg));
|
||||
if (parsed.kv) parts.push(E('span', { 'class': 'dd-kv' }, parsed.kv));
|
||||
return E('div', { 'class': 'dd-line ' + cls }, parts);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return backend.detectBackend();
|
||||
},
|
||||
|
||||
render(ctx) {
|
||||
const self = this;
|
||||
const LOG_PATH = ctx.backend.log;
|
||||
const state = {
|
||||
lastContent: '',
|
||||
lastSize: -1,
|
||||
paused: false,
|
||||
autoScroll: true,
|
||||
filter: '',
|
||||
userScrolled: false,
|
||||
reading: false
|
||||
};
|
||||
|
||||
const pane = E('div', { 'class': 'dd-log-pane', 'id': 'dd-log-pane' }, [
|
||||
E('div', { 'class': 'dd-empty' }, _('Loading…'))
|
||||
]);
|
||||
|
||||
// detect manual scroll → auto-pause auto-scroll
|
||||
pane.addEventListener('scroll', function() {
|
||||
const atBottom = pane.scrollHeight - pane.scrollTop - pane.clientHeight < 4;
|
||||
state.userScrolled = !atBottom;
|
||||
});
|
||||
|
||||
const meta = E('span', { 'class': 'dd-log-meta' }, '');
|
||||
|
||||
const btnAuto = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-auto' });
|
||||
const btnPause = E('button', { 'type': 'button', 'class': 'dd-log-btn dd-log-toggle', 'id': 'dd-log-pause' });
|
||||
const syncControls = function() {
|
||||
btnAuto.textContent = state.autoScroll ? '✓ ' + _('Auto-scroll: On') : '○ ' + _('Auto-scroll: Off');
|
||||
btnAuto.setAttribute('aria-pressed', String(state.autoScroll));
|
||||
btnPause.textContent = state.paused ? '▶ ' + _('Resume') : 'Ⅱ ' + _('Pause');
|
||||
btnPause.setAttribute('aria-pressed', String(state.paused));
|
||||
meta.textContent = state.paused ? _('Paused') : '';
|
||||
};
|
||||
btnAuto.addEventListener('click', function() {
|
||||
state.autoScroll = !state.autoScroll;
|
||||
if (state.autoScroll) {
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
state.userScrolled = false;
|
||||
}
|
||||
syncControls();
|
||||
});
|
||||
btnPause.addEventListener('click', function() {
|
||||
state.paused = !state.paused;
|
||||
syncControls();
|
||||
if (!state.paused) tick();
|
||||
});
|
||||
syncControls();
|
||||
|
||||
const selFilter = E('select', { 'class': 'dd-log-btn' }, [
|
||||
E('option', { 'value': '' }, _('All')),
|
||||
E('option', { 'value': 'trace' }, 'TRACE'),
|
||||
E('option', { 'value': 'debug' }, 'DEBUG'),
|
||||
E('option', { 'value': 'info' }, 'INFO'),
|
||||
E('option', { 'value': 'warn' }, 'WARN'),
|
||||
E('option', { 'value': 'error' }, 'ERROR'),
|
||||
E('option', { 'value': 'node_status' }, _('Node Status')),
|
||||
E('option', { 'value': 'proxy_traffic' }, _('Proxy Traffic'))
|
||||
]);
|
||||
selFilter.addEventListener('change', function() {
|
||||
state.filter = selFilter.value;
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
const btnClear = E('button', { 'class': 'dd-log-btn' }, _('Clear View'));
|
||||
btnClear.addEventListener('click', function() {
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
state.lastContent = '';
|
||||
});
|
||||
|
||||
const btnDownload = E('button', { 'class': 'dd-log-btn' }, _('Download'));
|
||||
btnDownload.addEventListener('click', function() {
|
||||
const blob = new Blob([state.lastContent || ''], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = ctx.name + '-' + (new Date()).toISOString().replace(/[:.]/g, '-') + '.log';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
const btnTruncate = E('button', { 'class': 'dd-log-btn' }, _('Clear File'));
|
||||
btnTruncate.addEventListener('click', function() {
|
||||
if (!confirm(_('Truncate %s log on the router? This cannot be undone.').format(ctx.name)))
|
||||
return;
|
||||
fs.write(LOG_PATH, '').then(function() {
|
||||
state.lastContent = '';
|
||||
state.lastSize = 0;
|
||||
}).catch(function() {});
|
||||
});
|
||||
|
||||
let btnStop = null;
|
||||
if (ctx.name === 'daed') {
|
||||
btnStop = E('button', { 'class': 'dd-log-btn dd-log-stop' }, _('Stop daed'));
|
||||
btnStop.addEventListener('click', function() {
|
||||
if (!confirm(_('Stop daed now?')))
|
||||
return;
|
||||
btnStop.disabled = true;
|
||||
execChecked('/sbin/uci', ['set', 'daed.config.enabled=0'])
|
||||
.then(function() { return execChecked('/sbin/uci', ['commit', 'daed']); })
|
||||
.then(function() { return execChecked('/etc/init.d/daed', ['disable']); })
|
||||
.then(function() { return execChecked('/etc/init.d/daed', ['stop']); })
|
||||
.then(function() { btnStop.textContent = _('daed stopped'); })
|
||||
.catch(function(e) {
|
||||
btnStop.disabled = false;
|
||||
ui.addNotification(null, E('p', {}, _('Failed to stop daed: %s').format(e.message || e)), 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
const f = state.filter;
|
||||
pane.querySelectorAll('.dd-line').forEach(function(el) {
|
||||
if (matchesFilter(el.textContent, f))
|
||||
el.classList.remove('dd-hidden');
|
||||
else
|
||||
el.classList.add('dd-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function appendLines(text) {
|
||||
const lines = text.split('\n');
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i];
|
||||
if (!ln) continue;
|
||||
const el = buildLine(ln);
|
||||
if (!matchesFilter(ln, state.filter))
|
||||
el.classList.add('dd-hidden');
|
||||
frag.appendChild(el);
|
||||
}
|
||||
// drop empty-state placeholder if present
|
||||
const empty = pane.querySelector('.dd-empty');
|
||||
if (empty) pane.removeChild(empty);
|
||||
pane.appendChild(frag);
|
||||
|
||||
// cap rendered rows
|
||||
const overflow = pane.children.length - MAX_LINES;
|
||||
if (overflow > 0) {
|
||||
for (let i = 0; i < overflow; i++) pane.removeChild(pane.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmpty(msg) {
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
pane.appendChild(E('div', { 'class': 'dd-empty' }, msg));
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (state.paused || state.reading) return Promise.resolve();
|
||||
state.reading = true;
|
||||
|
||||
return fs.stat(LOG_PATH).then(function(st) {
|
||||
if (state.paused) return;
|
||||
const size = st.size || 0;
|
||||
if (size === state.lastSize) {
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
return;
|
||||
}
|
||||
|
||||
// File rotated/truncated → full reload
|
||||
const rotated = size < state.lastSize;
|
||||
return fs.read_direct(LOG_PATH, 'text').then(function(content) {
|
||||
if (state.paused) return;
|
||||
content = content || '';
|
||||
let delta;
|
||||
if (rotated || state.lastContent === '') {
|
||||
// full replace
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
delta = content;
|
||||
} else if (content.indexOf(state.lastContent) === 0) {
|
||||
// pure append
|
||||
delta = content.slice(state.lastContent.length);
|
||||
} else {
|
||||
// content changed mid-stream → full replace
|
||||
while (pane.firstChild) pane.removeChild(pane.firstChild);
|
||||
delta = content;
|
||||
}
|
||||
|
||||
if (delta) appendLines(delta);
|
||||
state.lastContent = content;
|
||||
state.lastSize = size;
|
||||
meta.textContent = '%d bytes · live'.format(size);
|
||||
|
||||
if (state.autoScroll && !state.userScrolled)
|
||||
pane.scrollTop = pane.scrollHeight;
|
||||
});
|
||||
}).catch(function(e) {
|
||||
if (state.paused) return;
|
||||
const msg = String(e);
|
||||
if (msg.indexOf('NotFoundError') !== -1 || msg.indexOf('No such') !== -1)
|
||||
renderEmpty(_('Log file does not exist yet.'));
|
||||
else
|
||||
renderEmpty(_('Error reading log: %s').format(msg));
|
||||
state.lastSize = -1;
|
||||
state.lastContent = '';
|
||||
meta.textContent = '';
|
||||
}).finally(function() { state.reading = false; });
|
||||
}
|
||||
|
||||
poll.add(tick);
|
||||
tick();
|
||||
|
||||
const toolbarItems = [
|
||||
btnAuto,
|
||||
btnPause,
|
||||
selFilter,
|
||||
btnClear,
|
||||
btnDownload,
|
||||
btnTruncate
|
||||
];
|
||||
if (btnStop) toolbarItems.push(btnStop);
|
||||
toolbarItems.push(meta);
|
||||
const toolbar = E('div', { 'class': 'dd-log-toolbar' }, toolbarItems);
|
||||
|
||||
return E('div', { 'class': 'dd-log-wrap' }, [
|
||||
E('style', {}, CSS + styles.CSS),
|
||||
E('div', { 'class': 'dd-log-card' }, [
|
||||
E('h4', { 'class': 'dd-log-card-title' }, _('%s Realtime Log').format(ctx.name)),
|
||||
toolbar,
|
||||
pane
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
const CSS = [
|
||||
'.dd-log-toolbar .dd-log-toggle[aria-pressed="true"]{background:rgba(74,160,101,.16);border-color:#4aa065;box-shadow:inset 0 0 0 1px rgba(74,160,101,.2)}',
|
||||
'.dd-log-toolbar .dd-log-toggle:focus-visible{outline:2px solid #4aa065;outline-offset:2px}',
|
||||
|
||||
'.dd-wrap{padding:4px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:9px 14px;margin-bottom:7px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
/* padding:0 neutralizes Argon's h4{padding:.75rem 1.25rem}, which otherwise
|
||||
indents the title 20px past the card body and looks misaligned */
|
||||
'.dd-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;padding:0;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-status-row{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:0}',
|
||||
'.dd-status-row .dd-grow{flex:1 1 auto}',
|
||||
'.dd-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;font-size:10.5px;font-weight:700;letter-spacing:.3px;border:1px solid transparent;line-height:1.3}',
|
||||
'.dd-badge-run{color:#3da66a;border-color:rgba(61,166,106,.5)}',
|
||||
'.dd-badge-stop{color:#d96d6d;border-color:rgba(217,109,109,.55)}',
|
||||
'.dd-badge-dot{width:6px;height:6px;border-radius:50%;background:currentColor;display:inline-block}',
|
||||
'.dd-meta{font-size:11.5px;opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}',
|
||||
'.dd-meta-label{opacity:.55;margin-right:4px}',
|
||||
'.dd-actions{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 0}',
|
||||
'.dd-actions .cbi-button{font-size:11.5px;padding:4px 12px;border-radius:5px}',
|
||||
'.dd-actions a.cbi-button{display:inline-flex;align-items:center;gap:4px}',
|
||||
'.dd-wrap .cbi-button{font-size:11px !important;line-height:1.4 !important;min-height:0 !important;height:auto !important;padding:4px 12px !important;border-radius:5px !important;border:1px solid rgba(128,128,128,.35) !important;background:transparent !important;color:inherit !important;box-shadow:none !important;cursor:pointer;white-space:nowrap}',
|
||||
'.dd-wrap .cbi-button:hover{background:rgba(128,128,128,.12) !important}',
|
||||
'.dd-wrap .cbi-button:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-wrap .cbi-button-action,.dd-wrap .cbi-button-positive,.dd-wrap .cbi-button-add,.dd-wrap .cbi-button-edit{border-color:#4aa065 !important;color:#4aa065 !important}',
|
||||
'.dd-wrap .cbi-button-remove{border-color:#d96d6d !important;color:#d96d6d !important}',
|
||||
'.dd-wrap .cbi-button-remove:hover{background:rgba(217,109,109,.12) !important}',
|
||||
'.dd-switch{position:relative;width:42px;height:22px;border:0;border-radius:999px;background:rgba(128,128,128,.28);padding:0;cursor:pointer;transition:background .18s ease,opacity .18s ease;flex-shrink:0}',
|
||||
'.dd-switch .dd-switch-knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:rgba(255,255,255,.96);box-shadow:0 1px 4px rgba(0,0,0,.2);transition:transform .18s ease}',
|
||||
'.dd-switch.is-on{background:rgba(74,160,101,.65)}',
|
||||
'.dd-switch.is-on .dd-switch-knob{transform:translateX(20px)}',
|
||||
'.dd-switch:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-switch-label{font-size:10.5px;font-weight:600;opacity:.62;letter-spacing:.3px}',
|
||||
'.dd-switch-wrap{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}',
|
||||
'.dd-backend-card{padding:10px 14px}',
|
||||
'.dd-backend-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
|
||||
'.dd-backend-label{flex:0 0 calc(200px - 10px);font-size:12px;font-weight:600;opacity:.72}',
|
||||
'.dd-backend-segment{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:7px;background:rgba(128,128,128,.10)}',
|
||||
'.dd-backend-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:78px;height:24px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:inherit;font-size:11px;font-weight:500;opacity:.65;cursor:pointer;transition:background .18s ease,color .18s ease,opacity .18s ease}',
|
||||
'.dd-backend-btn:hover{background:rgba(128,128,128,.10)}',
|
||||
'.dd-backend-btn.is-active{background:linear-gradient(#3886a1,#2f7288);color:#fff;opacity:1;font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.12)}',
|
||||
'.dd-backend-btn:disabled{opacity:.55;cursor:not-allowed}',
|
||||
'.dd-backend-state{font-size:10.5px;font-weight:500;opacity:.70;margin-left:2px}',
|
||||
'.dd-backend-btn.is-active .dd-backend-state{opacity:.85}',
|
||||
'.dd-backend-help{margin:6px 0 0 200px;font-size:11.5px;line-height:1.45;opacity:.66}',
|
||||
'.dd-backend-help:empty{display:none}',
|
||||
'html[data-daede-theme="argon"] .dd-backend-label{flex-basis:calc(240px - 10px)}',
|
||||
'html[data-daede-theme="argon"] .dd-backend-help{margin-left:240px}',
|
||||
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-card{background:rgba(255,255,255,.78);border-color:rgba(50,50,93,.12);box-shadow:0 2px 8px rgba(50,50,93,.06)}',
|
||||
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-card-title{opacity:.78}',
|
||||
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-title{opacity:1}',
|
||||
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field input,html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field select,html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-settings-card .cbi-value-field textarea{background:#fff!important;border-color:rgba(50,50,93,.24)!important}',
|
||||
'html[data-daede-theme="argon"]:not([data-darkmode="true"]) .dd-config-page .dd-adv-bar{background:rgba(50,50,93,.04);border-color:rgba(50,50,93,.14);opacity:.78}',
|
||||
'@media (max-width:640px){.dd-backend-label{flex-basis:100%!important;width:100%}.dd-backend-segment{width:100%}.dd-backend-btn{flex:1;min-width:0}.dd-backend-help{margin-left:0!important}}',
|
||||
/* daed/dae settings card —— LuCI form.Map 字体/边框对齐 dd 卡片体系 */
|
||||
'.dd-settings-card{padding:10px 14px}',
|
||||
'.dd-settings-card>h2,.dd-settings-card .cbi-map>h2,.dd-settings-card .cbi-section>h3{display:none}',
|
||||
'.dd-settings-card .cbi-map>.cbi-map-descr,.dd-settings-card .cbi-section-descr{font-size:11.5px;opacity:.62;margin:0 0 8px;line-height:1.45}',
|
||||
'.dd-settings-descr{font-size:11.5px;opacity:.62;margin:0 0 8px;line-height:1.45}',
|
||||
'.dd-settings-card .cbi-section{margin:0;padding:0;background:transparent;border:0;box-shadow:none}',
|
||||
'.dd-settings-card .cbi-value{padding:6px 0;border:0;min-height:0}',
|
||||
/* Argon's .td.cbi-value-field padding + 40px .cbi-checkbox make table rows
|
||||
too tall — tighten both for our compact subscription/node tables */
|
||||
'.dd-settings-card .cbi-section-table-row>td{padding:6px 10px !important}',
|
||||
'.dd-settings-card .cbi-section-table-row .cbi-checkbox{height:20px !important;min-height:0 !important}',
|
||||
'.dd-settings-card .cbi-value-title{font-size:12.5px !important;font-weight:500;opacity:.85;padding:6px 12px 6px 0;min-width:140px}',
|
||||
'.dd-settings-card .cbi-value-field input,.dd-settings-card .cbi-value-field select,.dd-settings-card .cbi-value-field textarea{font-size:12.5px !important;padding:5px 8px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:inherit}',
|
||||
'.dd-settings-card .cbi-value-field input:focus,.dd-settings-card .cbi-value-field select:focus,.dd-settings-card .cbi-value-field textarea:focus{border-color:rgba(56,134,161,.7);outline:0;box-shadow:0 0 0 2px rgba(56,134,161,.15)}',
|
||||
'.dd-settings-card .cbi-value-description,.dd-settings-card .cbi-value-helptext{font-size:11.5px !important;opacity:.6;line-height:1.45;padding-top:3px}',
|
||||
'.dd-settings-card .cbi-section-remove{background:transparent !important;border:0 !important;box-shadow:none !important}',
|
||||
'.dd-settings-card .cbi-section-remove .cbi-button{border-color:#d96d6d !important;color:#d96d6d !important}',
|
||||
/* themes (esp. Argon) restyle checkboxes into faint, near-invisible boxes;
|
||||
force native rendering + brand accent so the checkmark is always clear */
|
||||
'.dd-settings-card input[type="checkbox"]{-webkit-appearance:checkbox !important;appearance:checkbox !important;width:16px;height:16px;background:none !important;border:0 !important;box-shadow:none !important;background-image:none !important}',
|
||||
'.dd-settings-card input[type="checkbox"]:before,.dd-settings-card input[type="checkbox"]:after{content:none !important;display:none !important}',
|
||||
/* line the Subscriptions and Nodes grid tables up column-for-column. fixed
|
||||
layout + explicit per-column percentages make the widths authoritative
|
||||
across themes (Argon's auto layout otherwise sizes the empty Nodes table
|
||||
by content, and even ignores px cell widths, so they drift apart) */
|
||||
'.dd-settings-card .cbi-section-table{table-layout:fixed !important;width:100%}',
|
||||
/* flatten the table header: Argon tints header cells (#f6f9fc), which reads
|
||||
as an off-colour band on the white card — keep it transparent */
|
||||
'.dd-settings-card .cbi-section-table-titles,.dd-settings-card .cbi-section-table-titles .cbi-section-table-cell{background:transparent !important;background-image:none !important}',
|
||||
'.dd-settings-card .cbi-section-table-cell:nth-child(1){width:14% !important}',
|
||||
'.dd-settings-card .cbi-section-table-cell:nth-child(2){width:62% !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
'.dd-settings-card .cbi-section-table-cell:nth-child(3){width:10% !important}',
|
||||
'.dd-settings-card .cbi-section-table-cell:nth-child(4){width:auto !important;min-width:0 !important;white-space:nowrap;text-align:right}',
|
||||
/* icon row-action buttons stay narrow, never stretch to fill the cell */
|
||||
'.dd-settings-card .cbi-section-actions .cbi-button-edit,.dd-settings-card .cbi-section-actions .cbi-button-remove{width:auto !important;min-width:0 !important;display:inline-block !important;flex:0 0 auto !important}',
|
||||
/* proxy-test result coloring */
|
||||
'.dd-meta.dd-ok{color:#3da66a;font-weight:600}',
|
||||
'.dd-meta.dd-err{color:#d96d6d;font-weight:600}',
|
||||
/* 手风琴折叠组 —— 视觉与 clashoo cl-component-adv 一致 */
|
||||
'.dd-adv{margin-top:10px}',
|
||||
'.dd-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;cursor:pointer;user-select:none;font-size:12px;font-weight:600;background:rgba(128,128,128,.06);border:1px solid rgba(128,128,128,.16);border-radius:7px;color:inherit;opacity:.55}',
|
||||
'.dd-adv-bar:hover{background:rgba(56,134,161,.08);opacity:1}',
|
||||
'.dd-adv-chevron{font-size:14px;font-weight:700;opacity:.55;transition:transform .2s}',
|
||||
'.dd-adv:not(.dd-closed) .dd-adv-chevron{transform:rotate(90deg)}',
|
||||
'.dd-adv-body{margin-top:8px;padding:2px 4px 4px}',
|
||||
'.dd-adv.dd-closed .dd-adv-body{display:none}',
|
||||
'.dd-editor{width:100%;min-height:460px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:12px;line-height:1.5;box-sizing:border-box;resize:vertical;border-radius:6px 6px 0 0}',
|
||||
/* syntax-highlight overlay: a <pre> sits behind a transparent <textarea>.
|
||||
both share identical box metrics so glyphs line up; the textarea stays the
|
||||
real editor (caret, selection, insert-at-cursor, jump-to-line all native) */
|
||||
'.dd-edit-wrap{position:relative}',
|
||||
'.dd-edit-wrap .dd-editor,.dd-edit-wrap .dd-hl{margin:0;padding:10px 12px;border-width:1px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-size:12px;line-height:1.5;letter-spacing:0;tab-size:4;white-space:pre;word-break:normal;overflow-x:auto;box-sizing:border-box}',
|
||||
'.dd-edit-wrap .dd-hl{position:absolute;inset:0;margin:0;overflow:hidden;border:1px solid transparent;border-radius:6px 6px 0 0;pointer-events:none;background:#f6f8fa;color:#3b4252;z-index:1}',
|
||||
/* reset Argon's code{background:var(--lighter)} so the overlay inherits the
|
||||
pre background uniformly — otherwise a dark pre shows as side bars.
|
||||
color:inherit so plain text follows .dd-hl (light in dark mode) instead of
|
||||
the theme's own code{color}, which is dark and vanishes on a dark pre */
|
||||
'.dd-edit-wrap .dd-hl code{font:inherit;white-space:inherit;word-break:inherit;display:block;color:inherit !important;background:transparent !important}',
|
||||
'.dd-edit-wrap .dd-editor-hl{position:relative;z-index:2;color:transparent !important;background:transparent !important;caret-color:#2f7288;border:1px solid rgba(128,128,128,.28)}',
|
||||
'.dd-edit-wrap .dd-editor-hl::placeholder{color:rgba(128,128,128,.55)}',
|
||||
/* keep selected text transparent too — else the browser force-colors it and
|
||||
it ghosts over the highlight pre underneath */
|
||||
'.dd-edit-wrap .dd-editor-hl::selection{background:rgba(56,134,161,.25);color:transparent}',
|
||||
'@media screen and (max-device-width:600px){.dd-edit-wrap .dd-editor,.dd-edit-wrap .dd-hl,.dd-edit-wrap .dd-hl code{font-size:16px}}',
|
||||
'.dh-c{color:#8a919a}',
|
||||
'.dh-s{color:#2a8a4a}',
|
||||
'.dh-k{color:#9a3fb5}',
|
||||
'.dh-f{color:#2f7288}',
|
||||
'.dh-o{color:#c2772a}',
|
||||
'.dh-g{color:#b0226a}',
|
||||
'html[data-darkmode="true"] .dd-edit-wrap .dd-hl,body.dark .dd-edit-wrap .dd-hl,html[data-bs-theme="dark"] .dd-edit-wrap .dd-hl{color:#c8cdd6;background:#1e2228}',
|
||||
'html[data-darkmode="true"] .dd-edit-wrap .dd-editor-hl,body.dark .dd-edit-wrap .dd-editor-hl,html[data-bs-theme="dark"] .dd-edit-wrap .dd-editor-hl{caret-color:#7fd0e8}',
|
||||
'html[data-darkmode="true"] .dh-c,body.dark .dh-c{color:#6b7280}',
|
||||
'html[data-darkmode="true"] .dh-s,body.dark .dh-s{color:#7ec699}',
|
||||
'html[data-darkmode="true"] .dh-k,body.dark .dh-k{color:#c792ea}',
|
||||
'html[data-darkmode="true"] .dh-f,body.dark .dh-f{color:#82c4dd}',
|
||||
'html[data-darkmode="true"] .dh-o,body.dark .dh-o{color:#e0a35a}',
|
||||
'html[data-darkmode="true"] .dh-g,body.dark .dh-g{color:#ec6fa8}',
|
||||
'.dd-editor-footer{display:flex;flex-wrap:wrap;align-items:center;gap:14px;padding:5px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10.5px;background:rgba(128,128,128,.06);border:1px solid rgba(0,0,0,.06);border-top:0;border-radius:0 0 6px 6px;color:inherit;opacity:.78}',
|
||||
'.dd-editor-footer .dd-fb-item{display:inline-flex;align-items:center;gap:5px}',
|
||||
'.dd-editor-footer .dd-fb-key{opacity:.55}',
|
||||
'.dd-editor-footer .dd-fb-warn{color:#b07d00;font-weight:600}',
|
||||
'.dd-editor-footer .dd-fb-ok{color:#3da66a;font-weight:600}',
|
||||
'body.dark .dd-editor-footer,html[data-theme="dark"] .dd-editor-footer,html[data-bs-theme="dark"] .dd-editor-footer,html[data-darkmode="true"] .dd-editor-footer{border-color:rgba(255,255,255,.1);background:rgba(255,255,255,.04)}',
|
||||
'body.dark .dd-editor-footer .dd-fb-warn,html[data-theme="dark"] .dd-editor-footer .dd-fb-warn,html[data-bs-theme="dark"] .dd-editor-footer .dd-fb-warn,html[data-darkmode="true"] .dd-editor-footer .dd-fb-warn{color:#e0b34a}',
|
||||
/* match the action buttons' size (h25 / 11px / 5px radius) for a consistent row */
|
||||
'.dd-insert-select{font-size:11px;line-height:1.4;height:25px;padding:0 10px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;box-sizing:border-box}',
|
||||
'.dd-insert-select:hover{border-color:rgba(56,134,161,.55)}',
|
||||
'.dd-editor-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px}',
|
||||
'.dd-editor-status{margin-left:auto;font-size:11.5px;opacity:0;transition:opacity .25s ease;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
|
||||
'.dd-editor-status.show{opacity:1}',
|
||||
'.dd-editor-status.ok{color:#3da66a}',
|
||||
'.dd-editor-status.err{color:#d96d6d}',
|
||||
'.dd-editor-hint{font-size:11.5px;opacity:.62;line-height:1.5;margin:0 0 8px;padding:7px 10px;border-radius:6px;background:rgba(56,134,161,.06)}',
|
||||
'.dd-editor-hint b{font-weight:600;opacity:.85}',
|
||||
'.dd-sub-card{margin:0 0 10px;padding:10px 14px;border:0;border-radius:8px;background:transparent}',
|
||||
'.dd-sub-card-title{font-size:11px;font-weight:600;opacity:.55;margin:0 0 8px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-sub-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
|
||||
'.dd-sub-row input{flex:1;min-width:200px;font-size:12px;padding:6px 10px;border-radius:5px;border:1px solid rgba(128,128,128,.28);background:transparent;color:var(--text-color,inherit)}',
|
||||
'.dd-sub-row input:focus{border-color:rgba(74,160,101,.55);outline:0;box-shadow:0 0 0 2px rgba(74,160,101,.12)}.dd-sub-row input::placeholder{opacity:.45;color:rgba(92,102,120,.8)}',
|
||||
'.dd-sub-apply{font-size:11px;padding:5px 14px;border-radius:5px;border:1px solid #4aa065;color:#4aa065;background:transparent;cursor:pointer;white-space:nowrap;font-weight:600}',
|
||||
'.dd-sub-apply:hover{background:rgba(74,160,101,.12)}',
|
||||
'.dd-sub-apply:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-sub-status{font-size:11px;margin-left:8px;opacity:0;transition:opacity .2s;white-space:nowrap}',
|
||||
'.dd-sub-status.show{opacity:1}',
|
||||
'.dd-ph-warn{display:none;font-size:12px;line-height:1.6;margin:0 0 8px;padding:10px 12px;border-radius:6px;background:rgba(217,158,0,.08);color:inherit}',
|
||||
'.dd-ph-warn.show{display:block}',
|
||||
'.dd-ph-warn-title{font-weight:600;margin-bottom:6px;color:#b07d00;font-size:12.5px}',
|
||||
'body.dark .dd-ph-warn-title,html[data-theme="dark"] .dd-ph-warn-title,html[data-bs-theme="dark"] .dd-ph-warn-title,html[data-darkmode="true"] .dd-ph-warn-title{color:#e0b34a}',
|
||||
'.dd-ph-howto{margin:4px 0 8px;opacity:.85}',
|
||||
'.dd-ph-howto code{padding:1px 5px;border-radius:3px;background:rgba(128,128,128,.18);font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}',
|
||||
'.dd-ph-list{margin:6px 0 0;padding:6px 0 0;list-style:none;border-top:1px dashed rgba(217,158,0,.3)}',
|
||||
'.dd-ph-list-label{font-size:11px;opacity:.6;margin:0 0 4px}',
|
||||
'.dd-ph-list li{padding:2px 0;display:flex;gap:8px;align-items:baseline;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px}',
|
||||
'.dd-ph-list .dd-ph-ln{display:inline-block;min-width:38px;text-align:center;padding:1px 4px;border-radius:3px;background:rgba(217,158,0,.18);color:#8a6300;opacity:.95;cursor:pointer;font-weight:600;text-decoration:none}',
|
||||
'.dd-ph-list .dd-ph-ln:hover{background:rgba(217,158,0,.32)}',
|
||||
'body.dark .dd-ph-list .dd-ph-ln,html[data-theme="dark"] .dd-ph-list .dd-ph-ln,html[data-bs-theme="dark"] .dd-ph-list .dd-ph-ln,html[data-darkmode="true"] .dd-ph-list .dd-ph-ln{color:#f0c763;background:rgba(217,158,0,.22)}',
|
||||
'.dd-ph-list .dd-ph-txt{opacity:.7;word-break:break-all}',
|
||||
'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,html[data-darkmode="true"] .dd-card{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}',
|
||||
'body.dark .dd-adv-bar,html[data-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-bar,html[data-darkmode="true"] .dd-adv-bar{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.10)}',
|
||||
'body.dark .dd-settings-card .cbi-value-field input,body.dark .dd-settings-card .cbi-value-field select,body.dark .dd-settings-card .cbi-value-field textarea,html[data-theme="dark"] .dd-settings-card .cbi-value-field input,html[data-theme="dark"] .dd-settings-card .cbi-value-field select,html[data-theme="dark"] .dd-settings-card .cbi-value-field textarea,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field input,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field select,html[data-bs-theme="dark"] .dd-settings-card .cbi-value-field textarea,html[data-darkmode="true"] .dd-settings-card .cbi-value-field input,html[data-darkmode="true"] .dd-settings-card .cbi-value-field select,html[data-darkmode="true"] .dd-settings-card .cbi-value-field textarea{border-color:rgba(255,255,255,.18)}',
|
||||
/* No prefers-color-scheme dark block: OS dark mode must NOT force a dark
|
||||
editor when the user picked a LIGHT theme. Dark styling is driven solely
|
||||
by data-darkmode (config.js reads the real page background). */
|
||||
/* status card line-2 meta (backend · pid), below the badge+toggle line */
|
||||
'.dd-status-meta{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}',
|
||||
/* compact icon-only row actions — the 编辑/删除 text buttons are too wide
|
||||
(esp. Bootstrap); show a pencil / trash glyph instead, both themes */
|
||||
'.dd-settings-card .cbi-section-actions .cbi-button-edit,.dd-settings-card .cbi-section-actions .cbi-button-remove{font-size:0 !important;padding:4px 7px !important;min-width:0 !important}',
|
||||
'.dd-settings-card .cbi-section-actions .cbi-button-edit::before{content:"\\270E";font-size:13px;line-height:1}',
|
||||
'.dd-settings-card .cbi-section-actions .cbi-button-remove::before{content:"\\1F5D1\\FE0E";font-size:13px;line-height:1}',
|
||||
/* phone: keep each subscription/node row on ONE line; the URL cell
|
||||
middle-ellipsizes (via ellipsisCell) and flex-shrinks to fit the screen */
|
||||
'@media (max-width:600px){',
|
||||
'.dd-settings-card .cbi-section-table-titles{display:none !important}',
|
||||
/* drop table layout so flex rows are constrained to the card width and the
|
||||
URL cell can actually shrink (a flex <tr> inside table-layout:fixed grows
|
||||
past the table and overflows the screen on narrow/Argon) */
|
||||
'.dd-settings-card .cbi-section-table{display:block !important;table-layout:auto !important;width:auto !important;overflow:visible !important}',
|
||||
'.dd-settings-card .cbi-section-table>tbody,.dd-settings-card .cbi-section-table>div{display:block !important;width:auto !important}',
|
||||
'.dd-settings-card .cbi-section-table-row{display:flex !important;flex-wrap:nowrap !important;width:auto !important;align-items:center;gap:6px;padding:5px 0;margin:0;border:0;border-bottom:1px solid rgba(128,128,128,.15);background:transparent}',
|
||||
/* target the real cells (td.cbi-value-field / actions) — LuCI forces them
|
||||
block+full-width at mobile, so override on the direct children */
|
||||
'.dd-settings-card .cbi-section-table-row>td{display:inline-block !important;align-self:center !important;width:auto !important;padding:0 !important;border:0 !important;background:transparent !important;box-shadow:none !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0 !important}',
|
||||
'.dd-settings-card .cbi-section-table-row>td::before{content:none !important;display:none !important}',
|
||||
'.dd-settings-card .cbi-section-table-row>td input{vertical-align:middle !important;margin:0 !important}',
|
||||
'.dd-settings-card .cbi-section-table-row>td:nth-child(1){flex:0 0 auto !important;max-width:60px;opacity:.6;font-size:11px}',
|
||||
'.dd-settings-card .cbi-section-table-row>td:nth-child(2){flex:1 1 auto !important}',
|
||||
'.dd-settings-card .cbi-section-table-row>td:nth-child(3){flex:0 0 auto !important;text-align:center}',
|
||||
'.dd-settings-card .cbi-section-table-row>td.cbi-section-actions{flex:0 0 auto !important;display:flex;gap:4px}',
|
||||
/* clear the floated title so a Flag description gets the full row width
|
||||
instead of wrapping around it (e.g. "局域网 / 私有地址不走代理。") */
|
||||
'.dd-settings-card .cbi-value-description,.dd-settings-card .cbi-value-helptext{clear:both;width:100%}',
|
||||
'}',
|
||||
'.dd-converter{width:100%!important;max-width:1180px;margin:0 auto}',
|
||||
'.dd-conv-card{padding:18px 20px;margin-bottom:12px}',
|
||||
'.dd-conv-url-row{display:grid;grid-template-columns:minmax(280px,1fr) 230px max-content;align-items:center;gap:8px}',
|
||||
'.dd-conv-import,.dd-conv-toolbar,.dd-conv-heading{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
||||
'.dd-converter input,.dd-converter select,.dd-converter .cbi-button{box-sizing:border-box!important;height:32px!important;min-height:32px!important;margin:0!important;font-size:12px!important;line-height:22px!important}',
|
||||
'.dd-converter input,.dd-converter select{padding:4px 8px!important;border:1px solid rgba(128,128,128,.28)!important;border-radius:6px!important;background-color:transparent;color:inherit;box-shadow:none!important}',
|
||||
'.dd-converter input::placeholder,.dd-conv-yaml::placeholder,.dd-conv-filter::placeholder,.dd-conv-airport-name::placeholder{color:inherit;opacity:.55}',
|
||||
'.dd-converter select{padding-right:28px!important}',
|
||||
'.dd-converter .cbi-button{padding:4px 12px!important}',
|
||||
'.dd-conv-url-row input,.dd-conv-filter,.dd-conv-target,.dd-conv-ua,.dd-conv-yaml{box-sizing:border-box;border:1px solid rgba(128,128,128,.28);border-radius:6px;background:transparent;color:inherit;font-size:12px;padding:7px 9px}',
|
||||
'.dd-conv-url-row input{width:100%;min-width:0}',
|
||||
'.dd-conv-ua{width:230px;max-width:230px}',
|
||||
'.dd-conv-yaml{width:100%;min-height:150px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.5}',
|
||||
'.dd-conv-or{text-align:center;font-size:11px;opacity:.5;margin:7px 0}',
|
||||
'.dd-conv-heading{justify-content:space-between;gap:10px;margin-bottom:10px}',
|
||||
'.dd-conv-heading .dd-card-title{margin:0}',
|
||||
'.dd-conv-summary{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:4px 10px;font-size:11px;font-variant-numeric:tabular-nums}',
|
||||
'.dd-conv-summary-item{display:inline-flex;align-items:baseline;gap:4px;white-space:nowrap;color:inherit;opacity:.62}',
|
||||
'.dd-conv-summary-item strong{font-size:12px;font-weight:650;opacity:1;color:inherit}',
|
||||
'.dd-conv-summary-label{font-size:10.5px}',
|
||||
'.dd-conv-toolbar{margin-bottom:8px}',
|
||||
'.dd-conv-filter{margin-left:auto;min-width:220px}',
|
||||
'.dd-conv-results{border:1px solid rgba(128,128,128,.18);border-radius:7px;max-height:420px;overflow:auto}',
|
||||
'.dd-conv-empty{display:flex;align-items:center;justify-content:center;min-height:72px;padding:16px;text-align:center;font-size:11.5px;opacity:.48}',
|
||||
'.dd-conv-result{display:grid;grid-template-columns:22px minmax(100px,1fr) 90px minmax(130px,1.3fr);gap:8px;align-items:center;padding:7px 9px;border-bottom:1px solid rgba(128,128,128,.13);font-size:12px}',
|
||||
'.dd-conv-result:last-child{border-bottom:0}',
|
||||
'.dd-conv-result input{appearance:checkbox !important;-webkit-appearance:checkbox !important;accent-color:#4aa065;width:15px;height:15px}',
|
||||
'.dd-conv-name,.dd-conv-result-state{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
'.dd-conv-proto{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;opacity:.7}',
|
||||
'.dd-conv-result-state.good,.dd-conv-status.ok{color:#3da66a}',
|
||||
'.dd-conv-result-state.dup{opacity:.55}',
|
||||
'.dd-conv-result-state.bad,.dd-conv-status.err{color:#d96d6d}',
|
||||
'.dd-conv-import label{font-size:12px;font-weight:600;opacity:.72}',
|
||||
'.dd-conv-airport{display:grid;grid-template-columns:max-content minmax(240px,420px) max-content 130px;gap:8px;align-items:center;margin-bottom:9px;padding-bottom:9px;border-bottom:1px solid rgba(128,128,128,.15)}',
|
||||
'.dd-conv-airport .dd-conv-target,.dd-conv-airport-name{box-sizing:border-box;width:100% !important;min-width:0 !important;height:32px !important;min-height:32px !important;margin:0 !important;padding:4px 8px !important;border:1px solid rgba(128,128,128,.28);border-radius:6px;background-color:transparent;color:inherit;font-size:12px !important;line-height:22px !important;box-shadow:none}',
|
||||
'.dd-conv-airport-name{max-width:420px}',
|
||||
'.dd-conv-group-summary{margin:2px 0 10px;padding:9px 11px;border-left:3px solid rgba(74,160,101,.65);border-radius:3px;background:rgba(74,160,101,.07);font-size:12px;line-height:1.5}',
|
||||
'.dd-conv-group-summary.err{border-left-color:#d96d6d;background:rgba(217,109,109,.07);color:#d96d6d}',
|
||||
'.dd-conv-submit{justify-content:flex-end}',
|
||||
'.dd-conv-import .cbi-button{margin-left:auto}',
|
||||
'.dd-conv-status{margin-top:9px;font-size:11.5px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;min-height:16px}',
|
||||
'.dd-daed-login{box-sizing:border-box;width:min(560px,100%);margin:0 auto}',
|
||||
'.dd-daed-login .cbi-value{display:grid!important;grid-template-columns:110px minmax(0,1fr)!important;gap:12px;align-items:center;padding:6px 0!important}',
|
||||
'.dd-daed-login .cbi-value-title,.dd-daed-login .cbi-value-field{box-sizing:border-box;width:auto!important;min-width:0!important;margin:0!important;padding:0!important}',
|
||||
'.dd-daed-login .cbi-value-field input{box-sizing:border-box;width:100%!important;max-width:none!important;min-width:0!important;height:36px!important;margin:0!important;padding:6px 10px!important}',
|
||||
'.dd-daed-login-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}',
|
||||
'@media(max-width:640px){.dd-conv-card{padding:14px}.dd-conv-heading{align-items:flex-start}.dd-conv-heading .dd-card-title{width:100%}.dd-conv-summary{justify-content:flex-start;width:100%;gap:5px 12px;padding-top:2px}.dd-conv-summary-item{min-width:74px}.dd-conv-filter{margin-left:0;width:100%}.dd-conv-result{grid-template-columns:20px minmax(90px,1fr) 68px}.dd-conv-result-state{grid-column:2/4;font-size:11px}.dd-conv-submit .cbi-button{width:100%!important}.dd-conv-airport{grid-template-columns:82px minmax(0,1fr)}.dd-conv-airport>*{max-width:none !important}.dd-conv-url-row{grid-template-columns:1fr}.dd-conv-url-row input,.dd-conv-url-row .dd-conv-ua,.dd-conv-url-row .cbi-button{width:100%;max-width:none}.dd-daed-login .cbi-value{grid-template-columns:1fr!important;gap:4px}.dd-daed-login-actions .cbi-button{flex:1 1 0}}'
|
||||
].join('');
|
||||
|
||||
return baseclass.extend({ CSS: CSS });
|
||||
@@ -0,0 +1,608 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
'require view.daede.backend as backend';
|
||||
'require view.daede.daed-session as daedSession';
|
||||
|
||||
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 currentPreset(gi, gs) {
|
||||
if (!gi && !gs) return 'loyalsoldier';
|
||||
if (gi === GEO_PRESETS.v2fly.geoip && gs === GEO_PRESETS.v2fly.geosite) return 'v2fly';
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
const HEALTH_PATHS = {
|
||||
btf: '/sys/kernel/btf/vmlinux',
|
||||
btfDetached: '/usr/lib/debug/boot/vmlinux',
|
||||
netns: '/run/netns/daens'
|
||||
};
|
||||
|
||||
const CSS = [
|
||||
'.dd-up-wrap{padding:6px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}',
|
||||
'.dd-card{border:1px solid rgba(0,0,0,.06);border-radius:10px;padding:14px 16px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.03);background:rgba(255,255,255,.02)}',
|
||||
'.dd-card-title{font-size:12px;font-weight:600;opacity:.55;margin:0 0 10px;letter-spacing:.3px;text-transform:uppercase}',
|
||||
'.dd-up-row{display:grid;grid-template-columns:24px 130px 1fr auto auto;gap:10px;align-items:center;font-size:12px;padding:7px 0;border-top:1px dashed rgba(128,128,128,.18)}',
|
||||
'.dd-up-row:first-of-type{border-top:0}',
|
||||
'.dd-up-icon{font-size:14px;text-align:center;line-height:1}',
|
||||
'.dd-up-ok{color:#3da66a}',
|
||||
'.dd-up-warn{color:#d39e00}',
|
||||
'.dd-up-new{color:#4a8cff}',
|
||||
'.dd-up-err{color:#d96d6d}',
|
||||
'.dd-up-name{font-weight:600;opacity:.85}',
|
||||
'.dd-up-meta{opacity:.7;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||
'.dd-up-version{font-size:11px;opacity:.55;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:nowrap}',
|
||||
'.dd-up-btn{font-size:11px;line-height:1.4;min-height:0;height:auto;padding:4px 12px;border-radius:5px;border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer;white-space:nowrap}',
|
||||
'.dd-up-btn:hover{background:rgba(128,128,128,.12)}',
|
||||
'.dd-up-btn:disabled{opacity:.45;cursor:not-allowed}',
|
||||
'.dd-up-btn-primary{border-color:#4aa065;color:#4aa065}',
|
||||
'.dd-backup-buttons{display:flex;align-items:center;gap:8px}',
|
||||
'.dd-backup-reset{border-color:rgba(217,109,109,.55);color:#d96d6d}',
|
||||
'@media(max-width:640px){.dd-backup-row{grid-template-columns:24px minmax(0,1fr)}.dd-backup-row .dd-up-meta{grid-column:2;white-space:normal}.dd-backup-row>span:nth-child(4){display:none}.dd-backup-buttons{grid-column:2;flex-wrap:wrap}}',
|
||||
'.dd-geo-row{display:grid;grid-template-columns:96px 1fr;gap:10px;align-items:center;font-size:12px;padding:6px 0}',
|
||||
'.dd-geo-row label{opacity:.75;font-weight:600}',
|
||||
'.dd-geo-row input[type=text],.dd-geo-row select{font-size:12px;padding:4px 8px;border:1px solid rgba(128,128,128,.35);border-radius:5px;background:transparent;color:inherit;width:100%}',
|
||||
/* selects cap at 200px on wide screens, shrink to the column on mobile (no overflow) */
|
||||
'.dd-geo-row select{width:100%!important;max-width:200px;box-sizing:border-box}',
|
||||
'.dd-geo-actions{margin-top:10px;display:flex;gap:10px;align-items:center}',
|
||||
'.dd-up-log{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;padding:10px;border:1px solid rgba(128,128,128,.14);border-radius:8px;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;display:none;background:inherit;color:#4a8c63}',
|
||||
'.dd-up-log.show{display:block}',
|
||||
'body.dark .dd-up-log,html[data-theme="dark"] .dd-up-log,html[data-bs-theme="dark"] .dd-up-log{color:#a3d9ad}',
|
||||
/* accordion — same structure as config.js dd-adv */
|
||||
'.dd-adv{margin-bottom:14px}',
|
||||
'.dd-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;cursor:pointer;user-select:none;font-size:11.5px;font-weight:600;background:rgba(128,128,128,.04);border:1px solid rgba(0,0,0,.06);border-radius:10px;color:inherit;letter-spacing:.3px;text-transform:uppercase;opacity:.7;box-shadow:0 2px 8px rgba(0,0,0,.03)}',
|
||||
'.dd-adv-bar:hover{background:rgba(56,134,161,.06);opacity:.95}',
|
||||
'.dd-adv-chevron{font-size:14px;font-weight:700;opacity:.55;transition:transform .2s;text-transform:none}',
|
||||
'.dd-adv:not(.dd-closed) .dd-adv-chevron{transform:rotate(90deg)}',
|
||||
'.dd-adv-body{margin-top:8px;padding:10px 14px;border:1px solid rgba(0,0,0,.06);border-radius:10px;background:rgba(255,255,255,.02);box-shadow:0 2px 8px rgba(0,0,0,.03)}',
|
||||
'.dd-adv.dd-closed{margin-bottom:14px}',
|
||||
'.dd-adv.dd-closed .dd-adv-body{display:none}',
|
||||
'body.dark .dd-card,html[data-theme="dark"] .dd-card,html[data-bs-theme="dark"] .dd-card,body.dark .dd-adv-bar,body.dark .dd-adv-body,html[data-theme="dark"] .dd-adv-bar,html[data-theme="dark"] .dd-adv-body,html[data-bs-theme="dark"] .dd-adv-bar,html[data-bs-theme="dark"] .dd-adv-body{border-color:rgba(255,255,255,.08);background:rgba(255,255,255,.02)}'
|
||||
].join('');
|
||||
|
||||
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`: split into numeric / non-numeric
|
||||
// chunks and compare chunk by chunk (numeric chunks numerically). Returns
|
||||
// <0 if a<b, 0 if equal, >0 if a>b. Used so a stale third-party feed that
|
||||
// happens to provide an OLDER build (e.g. compile-jell) is never offered as an
|
||||
// "upgrade" over a newer locally installed package.
|
||||
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;
|
||||
}
|
||||
|
||||
function probeFile(path) {
|
||||
return L.resolveDefault(fs.stat(path), null).then(function(st) {
|
||||
return { exists: !!st, size: st ? st.size : 0, mtime: st ? st.mtime : 0 };
|
||||
});
|
||||
}
|
||||
|
||||
function probePkg(pkg) {
|
||||
return fs.exec('/usr/share/luci-app-daede/pkg-info.sh', [pkg]).then(function(res) {
|
||||
const out = (res.stdout || '').trim().split('\t');
|
||||
return { installed: out[0] || '', latest: out[1] || '' };
|
||||
}).catch(function() {
|
||||
return { installed: '', latest: '' };
|
||||
});
|
||||
}
|
||||
|
||||
// YYYYMMDD-HHMM stamp for backup filenames
|
||||
function stamp() {
|
||||
const d = new Date(), p = n => (n < 10 ? '0' : '') + n;
|
||||
return '' + d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '-' + p(d.getHours()) + p(d.getMinutes());
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
// background apk update so new versions show on next poll
|
||||
fs.exec('/usr/share/luci-app-daede/refresh-index.sh', []).catch(function() {});
|
||||
return Promise.all([
|
||||
backend.detectBackend(),
|
||||
uci.load('daed').catch(function() {}),
|
||||
uci.load('daede').catch(function() {})
|
||||
]).then(function(r) {
|
||||
return r[0];
|
||||
});
|
||||
},
|
||||
|
||||
render: function(ctx) {
|
||||
const logPane = E('pre', { 'class': 'dd-up-log', 'id': 'dd-up-log' }, '');
|
||||
const corePkgs = [];
|
||||
const addCorePkg = function(name) {
|
||||
if (corePkgs.indexOf(name) === -1)
|
||||
corePkgs.push(name);
|
||||
};
|
||||
addCorePkg(ctx.name);
|
||||
addCorePkg('daed');
|
||||
addCorePkg('dae');
|
||||
|
||||
const dataBody = E('div', { 'id': 'dd-up-data' }, E('em', {}, _('Probing…')));
|
||||
const pkgBody = E('div', { 'id': 'dd-up-pkg' }, E('em', {}, _('Probing…')));
|
||||
const healthBody = E('div', { 'id': 'dd-up-health' }, E('em', {}, _('Probing…')));
|
||||
|
||||
const mkRow = function(icon, iconCls, name, meta, btn) {
|
||||
return E('div', { 'class': 'dd-up-row' }, [
|
||||
E('span', { 'class': 'dd-up-icon ' + iconCls }, icon),
|
||||
E('span', { 'class': 'dd-up-name' }, name),
|
||||
E('span', { 'class': 'dd-up-meta', 'title': meta }, meta),
|
||||
E('span', {}, ''),
|
||||
btn || E('span', {}, '')
|
||||
]);
|
||||
};
|
||||
|
||||
// run a backgrounded script and stream its log into the log pane until it
|
||||
// logs a final ✓/✗ status line; then refresh the badges. Inline, no popup.
|
||||
const runJob = function(script, arg, btn, logPath) {
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
let tries = 0;
|
||||
const poll = function() {
|
||||
return L.resolveDefault(fs.read_direct(logPath, 'text'), '').then(function(c) {
|
||||
if (c) { logPane.textContent = c; logPane.classList.add('show'); }
|
||||
if (/[✓✗]/.test(c)) { refresh(); return; }
|
||||
if (tries++ > 90) return;
|
||||
return new Promise(function(r) { setTimeout(r, 2000); }).then(poll);
|
||||
});
|
||||
};
|
||||
return fs.exec('/usr/share/luci-app-daede/' + script, [arg]).then(function(res) {
|
||||
if (res.code === 0) return poll();
|
||||
}).catch(function() {}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
});
|
||||
};
|
||||
|
||||
// === Data updates (GeoIP / GeoSite) ===
|
||||
const updateGeo = function(kind, btn) {
|
||||
return runJob('update-geo.sh', kind, btn, '/tmp/luci-app-daede.' + kind + '.log');
|
||||
};
|
||||
|
||||
// === Package updates (dae|daed / luci-app-daede) ===
|
||||
const upgradePkg = function(pkg, btn) {
|
||||
return runJob('update-pkg.sh', pkg, btn, '/tmp/luci-app-daede.pkg-' + pkg + '.log');
|
||||
};
|
||||
|
||||
// Configuration operations share a backend lock and one UI busy state.
|
||||
const backupScript = '/usr/share/luci-app-daede/config-backup.sh';
|
||||
const exportBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Export'));
|
||||
const importBtn = E('button', { 'class': 'dd-up-btn', 'type': 'button' }, _('Import'));
|
||||
const resetBtn = E('button', { 'class': 'dd-up-btn dd-backup-reset', 'type': 'button' }, _('Restore Defaults'));
|
||||
const fileInput = E('input', { 'type': 'file', 'accept': '.tar.gz,.gz,application/gzip', 'style': 'display:none' });
|
||||
let backupBusy = false;
|
||||
const showBackupError = function(e) {
|
||||
ui.addNotification(null, E('p', {}, e.message || String(e)), 'error');
|
||||
};
|
||||
const configOperation = function(button, operation) {
|
||||
if (backupBusy) return Promise.resolve();
|
||||
backupBusy = true;
|
||||
const label = button.textContent;
|
||||
[exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = true; });
|
||||
button.textContent = _('Working…');
|
||||
return uci.changes().then(function(changes) {
|
||||
if (['dae', 'daed', 'daede'].some(function(name) { return changes && changes[name] && changes[name].length; }))
|
||||
throw new Error(_('Apply or discard pending daede changes first.'));
|
||||
return operation();
|
||||
}).catch(showBackupError).finally(function() {
|
||||
backupBusy = false;
|
||||
[exportBtn, importBtn, resetBtn].forEach(function(b) { b.disabled = false; });
|
||||
button.textContent = label;
|
||||
fileInput.value = '';
|
||||
});
|
||||
};
|
||||
const confirmBackup = function(message) {
|
||||
return new Promise(function(resolve) {
|
||||
const answer = function(value) { ui.hideModal(); resolve(value); };
|
||||
ui.showModal(_('Confirm configuration operation'), [
|
||||
E('p', {}, message),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': function() { answer(false); } }, _('Cancel')),
|
||||
' ',
|
||||
E('button', { 'class': 'cbi-button cbi-button-negative', 'click': function() { answer(true); } }, _('Continue'))
|
||||
])
|
||||
]);
|
||||
});
|
||||
};
|
||||
const execBackup = function(args) {
|
||||
return fs.exec(backupScript, args).then(function(res) {
|
||||
if (!res || res.code !== 0)
|
||||
throw new Error(String(res && (res.stderr || res.stdout) || _('Configuration operation failed.')).trim());
|
||||
return res;
|
||||
});
|
||||
};
|
||||
const waitBackup = function() {
|
||||
let tries = 0;
|
||||
const check = function() {
|
||||
return fs.read_direct('/tmp/luci-app-daede.backup.log', 'text').then(function(content) {
|
||||
logPane.textContent = content;
|
||||
logPane.classList.add('show');
|
||||
if (/^✗/m.test(content)) throw new Error(_('Configuration operation failed. See the log below.'));
|
||||
if (/^✓/m.test(content)) return;
|
||||
if (++tries > 90) throw new Error(_('Operation is still running. Check the log before retrying.'));
|
||||
return new Promise(function(resolve) { setTimeout(resolve, 2000); }).then(check);
|
||||
});
|
||||
};
|
||||
return check();
|
||||
};
|
||||
const reloadConfig = function(message) {
|
||||
daedSession.clear(window.localStorage);
|
||||
['dae', 'daed', 'daede'].forEach(function(name) { uci.unload(name); });
|
||||
ui.showModal(_('Configuration restored'), [
|
||||
E('p', {}, message),
|
||||
E('div', { 'class': 'right' }, E('button', {
|
||||
'class': 'cbi-button cbi-button-positive', 'click': function() { window.location.reload(); }
|
||||
}, _('Reload Page')))
|
||||
]);
|
||||
};
|
||||
exportBtn.addEventListener('click', async function() {
|
||||
if (!await confirmBackup(_('Export briefly stops running dae/daed services to back up the database safely, then resumes them. Continue?'))) return;
|
||||
configOperation(exportBtn, function() {
|
||||
return execBackup(['export']).then(function(res) {
|
||||
const bin = atob((res.stdout || '').trim());
|
||||
if (!bin) throw new Error(_('Export failed: empty archive'));
|
||||
const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); });
|
||||
const url = URL.createObjectURL(new Blob([bytes], { 'type': 'application/gzip' }));
|
||||
const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' });
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
importBtn.addEventListener('click', function() { fileInput.click(); });
|
||||
fileInput.addEventListener('change', async function(ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
if (file.size > 184320) {
|
||||
showBackupError(new Error(_('Backup exceeds the 180 KiB limit. Use System Backup for larger files.')));
|
||||
fileInput.value = ''; return;
|
||||
}
|
||||
if (!await confirmBackup(_('Import replaces daede settings, nodes, subscriptions and dashboard data. Services stop first; the saved active backend starts only if enabled in the backup. Continue?'))) {
|
||||
fileInput.value = ''; return;
|
||||
}
|
||||
configOperation(importBtn, function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = function() { reject(new Error(_('Unable to read backup file.'))); };
|
||||
reader.onload = function() { resolve(String(reader.result).split(',')[1] || ''); };
|
||||
reader.readAsDataURL(file);
|
||||
}).then(function(b64) {
|
||||
// Per-tab uploads cannot overwrite each other before the backend locks.
|
||||
const token = Array.from(window.crypto.getRandomValues(new Uint8Array(16)), function(n) { return n.toString(16).padStart(2, '0'); }).join('');
|
||||
const upload = '/tmp/daede-import.' + token + '.b64';
|
||||
return fs.write(upload, b64).then(function() { return execBackup(['import', upload]); })
|
||||
.finally(function() { return fs.remove(upload).catch(function() {}); });
|
||||
}).then(waitBackup).then(function() {
|
||||
reloadConfig(_('Backup restored. The active backend follows the saved enabled setting.'));
|
||||
});
|
||||
});
|
||||
});
|
||||
resetBtn.addEventListener('click', async function() {
|
||||
if (!await confirmBackup(_('Restore ALL daede defaults? This clears dae/daed settings, nodes, subscriptions and daed dashboard accounts/data. Export a backup first if needed. Both backends will remain disabled. Installed programs and GeoData are kept.'))) return;
|
||||
configOperation(resetBtn, function() {
|
||||
return execBackup(['reset']).then(waitBackup).then(function() {
|
||||
reloadConfig(_('All defaults restored. Both backends are disabled. Configure your nodes and routing before starting manually.'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const refresh = function() {
|
||||
const probes = [
|
||||
probeFile(DATA_PATHS.geoip),
|
||||
probeFile(DATA_PATHS.geosite),
|
||||
probeFile(HEALTH_PATHS.btf),
|
||||
probeFile(HEALTH_PATHS.btfDetached),
|
||||
backend.detectRunning(),
|
||||
backend.serviceStatus(ctx.name)
|
||||
];
|
||||
|
||||
corePkgs.forEach(function(pkg) {
|
||||
probes.push(probePkg(pkg));
|
||||
});
|
||||
probes.push(probePkg('luci-app-daede'));
|
||||
|
||||
if (ctx.backend.useNetns)
|
||||
probes.push(probeFile(HEALTH_PATHS.netns));
|
||||
|
||||
return Promise.all(probes).then(function(r) {
|
||||
const geoip = r[0], geosite = r[1], btf = r[2], btfDetached = r[3], running = r[4], service = r[5];
|
||||
const pkgOffset = 6;
|
||||
const coreInfo = {};
|
||||
corePkgs.forEach(function(pkg, i) {
|
||||
coreInfo[pkg] = r[pkgOffset + i];
|
||||
});
|
||||
const luci = r[pkgOffset + corePkgs.length];
|
||||
const ns = r[pkgOffset + corePkgs.length + 1];
|
||||
|
||||
// data rows
|
||||
while (dataBody.firstChild) dataBody.removeChild(dataBody.firstChild);
|
||||
[
|
||||
{ k: 'geoip', name: 'GeoIP', r: geoip },
|
||||
{ k: 'geosite', name: 'GeoSite', r: geosite }
|
||||
].forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Update'));
|
||||
btn.addEventListener('click', function() { 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(mkRow(
|
||||
entry.r.exists ? '✓' : '✗',
|
||||
entry.r.exists ? 'dd-up-ok' : 'dd-up-err',
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
|
||||
// pkg rows
|
||||
while (pkgBody.firstChild) pkgBody.removeChild(pkgBody.firstChild);
|
||||
corePkgs.map(function(pkg) {
|
||||
const label = _('%s binary').format(pkg) + (ctx.name === pkg ? ' · ' + _('active') : '');
|
||||
return { k: pkg, name: label, r: coreInfo[pkg] };
|
||||
}).concat([
|
||||
{ k: 'luci-app-daede', name: 'luci-app-daede', r: luci }
|
||||
]).forEach(function(entry) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn dd-up-btn-primary' }, _('Upgrade'));
|
||||
btn.addEventListener('click', function() { upgradePkg(entry.k, btn); });
|
||||
// Only treat it as upgradable when the feed version is STRICTLY
|
||||
// higher than what's installed. A lower/equal feed version (e.g.
|
||||
// a stale compile-jell build) must never be offered as an upgrade.
|
||||
const cmp = (entry.r.installed && entry.r.latest)
|
||||
? cmpVer(entry.r.latest, entry.r.installed) : null;
|
||||
const updatable = cmp !== null && cmp > 0;
|
||||
let meta;
|
||||
if (!entry.r.installed) {
|
||||
meta = _('not installed via package manager');
|
||||
btn.disabled = true;
|
||||
btn.textContent = _('Unavailable');
|
||||
} else if (!entry.r.latest) {
|
||||
// latest unknown (registry unreachable) - nothing to upgrade to
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('latest version unknown');
|
||||
btn.disabled = true;
|
||||
} else if (updatable) {
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' → ' + _('latest') + ': ' + entry.r.latest;
|
||||
} else {
|
||||
// installed >= feed: already up to date (feed may even be older)
|
||||
meta = _('installed') + ': ' + entry.r.installed + ' · ' + _('up to date');
|
||||
btn.disabled = true;
|
||||
}
|
||||
pkgBody.appendChild(mkRow(
|
||||
updatable ? '↑' : (entry.r.installed ? '✓' : '✗'),
|
||||
updatable ? 'dd-up-new' : (entry.r.installed ? 'dd-up-ok' : 'dd-up-err'),
|
||||
entry.name,
|
||||
meta,
|
||||
btn
|
||||
));
|
||||
});
|
||||
|
||||
// health rows
|
||||
while (healthBody.firstChild) healthBody.removeChild(healthBody.firstChild);
|
||||
healthBody.appendChild(mkRow(
|
||||
running[ctx.name] ? '✓' : '✗',
|
||||
running[ctx.name] ? 'dd-up-ok' : 'dd-up-err',
|
||||
_('%s process').format(ctx.name),
|
||||
running[ctx.name] ? _('pgrep reports the backend is running') : _('backend process is not running')
|
||||
));
|
||||
|
||||
if (btf.exists)
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Kernel BTF'), HEALTH_PATHS.btf + ' · ' + fmtBytes(btf.size)));
|
||||
else if (btfDetached.exists)
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Kernel BTF'), HEALTH_PATHS.btfDetached + ' · ' + fmtBytes(btfDetached.size)));
|
||||
else
|
||||
healthBody.appendChild(mkRow('✗', 'dd-up-err', _('Kernel BTF'), _('not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf')));
|
||||
|
||||
if (ctx.backend.hasWebUI) {
|
||||
const listenAddr = uci.get('daed', 'config', 'listen_addr') || ctx.backend.defaultListen;
|
||||
const port = listenAddr.split(':').slice(-1)[0];
|
||||
healthBody.appendChild(mkRow(
|
||||
service.running ? '✓' : '✗',
|
||||
service.running ? 'dd-up-ok' : 'dd-up-err',
|
||||
_('WebUI/API'),
|
||||
service.running ? _('service is running, expected on port %s').format(port) : _('service is stopped')
|
||||
));
|
||||
} else {
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('Hot reload'), _('/etc/init.d/dae hot_reload is available')));
|
||||
}
|
||||
|
||||
if (ctx.backend.useNetns && ns && ns.exists) {
|
||||
const btn = E('button', { 'class': 'dd-up-btn' }, _('Clean'));
|
||||
btn.addEventListener('click', function() {
|
||||
const daedRunning = !!(running && running[ctx.name]);
|
||||
const msg = daedRunning
|
||||
? _('%s is running. Deleting the daens netns now will break its networking until you restart it. Continue?').format(ctx.name)
|
||||
: _('Delete the daens netns?');
|
||||
if (!confirm(msg))
|
||||
return;
|
||||
btn.disabled = true;
|
||||
fs.exec('/sbin/ip', ['netns', 'del', 'daens']).finally(function() { btn.disabled = false; });
|
||||
});
|
||||
healthBody.appendChild(mkRow('⚠', 'dd-up-warn', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('exists, may block daed start'), btn));
|
||||
} else if (ctx.backend.useNetns) {
|
||||
healthBody.appendChild(mkRow('✓', 'dd-up-ok', _('netns daens'), HEALTH_PATHS.netns + ' · ' + _('clean')));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
poll.add(refresh);
|
||||
refresh();
|
||||
|
||||
// === Geo data source (preset / custom URL + auto-update) ===
|
||||
const geoSettings = (function() {
|
||||
const gi0 = uci.get('daede', 'config', 'geoip_url') || '';
|
||||
const gs0 = uci.get('daede', 'config', 'geosite_url') || '';
|
||||
const auto0 = uci.get('daede', 'config', 'geo_auto') === '1';
|
||||
const freq0 = uci.get('daede', 'config', 'geo_auto_freq') || 'daily';
|
||||
const preset0 = currentPreset(gi0, gs0);
|
||||
|
||||
const presetSel = E('select', {}, [
|
||||
E('option', { 'value': 'loyalsoldier' }, 'Loyalsoldier'),
|
||||
E('option', { 'value': 'v2fly' }, 'v2fly'),
|
||||
E('option', { 'value': 'custom' }, _('Custom'))
|
||||
]);
|
||||
presetSel.value = preset0;
|
||||
|
||||
const giInput = E('input', { 'type': 'text', 'placeholder': 'https://…/geoip.dat' });
|
||||
const gsInput = E('input', { 'type': 'text', 'placeholder': 'https://…/geosite.dat' });
|
||||
giInput.value = preset0 === 'custom' ? gi0 : '';
|
||||
gsInput.value = preset0 === 'custom' ? gs0 : '';
|
||||
|
||||
const customRows = E('div', {}, [
|
||||
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, 'GeoIP URL'), giInput ]),
|
||||
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, 'GeoSite URL'), gsInput ])
|
||||
]);
|
||||
const syncCustom = function() {
|
||||
customRows.style.display = presetSel.value === 'custom' ? '' : 'none';
|
||||
};
|
||||
presetSel.addEventListener('change', syncCustom);
|
||||
syncCustom();
|
||||
|
||||
const autoSel = E('select', {}, [
|
||||
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': 'dd-up-btn dd-up-btn-primary' }, _('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(); }
|
||||
uci.set('daede', 'config', 'geoip_url', gi);
|
||||
uci.set('daede', 'config', 'geosite_url', gs);
|
||||
uci.set('daede', 'config', 'geo_auto', geoAuto ? '1' : '0');
|
||||
uci.set('daede', 'config', 'geo_auto_freq', geoFreq);
|
||||
const orig = saveBtn.textContent;
|
||||
saveBtn.disabled = true; saveBtn.textContent = '...';
|
||||
uci.save().then(function() {
|
||||
return uci.changes();
|
||||
}).then(function(changes) {
|
||||
// apply only when changed; empty changeset fails apply (#16)
|
||||
if (changes && Object.keys(changes).length)
|
||||
return uci.apply();
|
||||
}).then(function() {
|
||||
return fs.exec('/usr/share/luci-app-daede/geo-cron.sh', [geoAuto ? 'enable' : 'disable']);
|
||||
}).then(function() {
|
||||
logPane.textContent = _('Geo data source saved.');
|
||||
logPane.classList.add('show');
|
||||
ui.changes.init();
|
||||
}).catch(function(e) {
|
||||
logPane.textContent = _('Save failed') + ': ' + (e && e.message ? e.message : e);
|
||||
logPane.classList.add('show');
|
||||
}).finally(function() {
|
||||
saveBtn.disabled = false; saveBtn.textContent = orig;
|
||||
});
|
||||
});
|
||||
|
||||
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, _('Data Source')),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
E('div', { 'class': 'dd-adv-body' }, [
|
||||
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, _('Source')), presetSel ]),
|
||||
customRows,
|
||||
E('div', { 'class': 'dd-geo-row' }, [
|
||||
E('label', {}, _('Auto-update')),
|
||||
autoSel
|
||||
]),
|
||||
E('div', { 'class': 'dd-geo-actions' }, [ saveBtn ])
|
||||
])
|
||||
]);
|
||||
adv.firstChild.addEventListener('click', function() {
|
||||
adv.classList.toggle('dd-closed');
|
||||
});
|
||||
return adv;
|
||||
})();
|
||||
|
||||
return E('div', { 'class': 'dd-up-wrap' }, [
|
||||
E('style', {}, CSS),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Data Updates')),
|
||||
dataBody,
|
||||
geoSettings
|
||||
]),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Package Updates')),
|
||||
pkgBody
|
||||
]),
|
||||
E('div', { 'class': 'dd-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Config Backup')),
|
||||
E('div', { 'class': 'dd-up-row dd-backup-row' }, [
|
||||
E('span', { 'class': 'dd-up-icon dd-up-new' }, '⇩'),
|
||||
E('span', { 'class': 'dd-up-name' }, _('dae + daed')),
|
||||
E('span', {
|
||||
'class': 'dd-up-meta',
|
||||
'title': _('Backups contain account and subscription credentials. Store them securely.')
|
||||
}, _('Back up / restore the whole daede config (kernels excluded)')),
|
||||
E('span', {}, ''),
|
||||
E('div', { 'class': 'dd-backup-buttons' }, [exportBtn, importBtn, resetBtn])
|
||||
]),
|
||||
fileInput
|
||||
]),
|
||||
(function() {
|
||||
const adv = E('div', { 'class': 'dd-adv dd-closed' }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, _('System Health')),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
E('div', { 'class': 'dd-adv-body' }, [ healthBody ])
|
||||
]);
|
||||
adv.firstChild.addEventListener('click', function() {
|
||||
adv.classList.toggle('dd-closed');
|
||||
});
|
||||
return adv;
|
||||
})(),
|
||||
logPane
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
+21
@@ -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
|
||||
@@ -0,0 +1,392 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require view.daede.backend as backend';
|
||||
|
||||
function notifyAction() {}
|
||||
|
||||
function execChecked(command, args) {
|
||||
return fs.exec(command, args || []).then(function(res) {
|
||||
if (res && res.code !== 0)
|
||||
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function execInit(be, action) {
|
||||
return fs.exec(be.initd, [action]).then(function(res) {
|
||||
notifyAction(action, res);
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
function restartService(be) {
|
||||
return execChecked(be.initd, ['restart'])
|
||||
.then(function() { return waitForService(be, true, 80); });
|
||||
}
|
||||
|
||||
function rejectIfOtherRunning(be, running) {
|
||||
const other = be.name === 'dae' ? 'daed' : 'dae';
|
||||
if (!running || !running[other])
|
||||
return Promise.resolve();
|
||||
|
||||
return Promise.reject(new Error(_('%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment.').format(other, other, be.name)));
|
||||
}
|
||||
|
||||
function toggleService(be, turnOn) {
|
||||
const enabled = turnOn ? '1' : '0';
|
||||
const action = turnOn ? 'start' : 'stop';
|
||||
|
||||
// Only guard on start: two backends share the eBPF/cgroup attachment, so
|
||||
// starting one while the other runs is unsafe. Stopping is always allowed.
|
||||
const guard = turnOn
|
||||
? backend.detectRunning().then(function(running) { return rejectIfOtherRunning(be, running); })
|
||||
: Promise.resolve();
|
||||
|
||||
return guard
|
||||
.then(function() { return execChecked('/sbin/uci', ['set', be.uci + '.config.enabled=' + enabled]); })
|
||||
.then(function() { return execChecked('/sbin/uci', ['commit', be.uci]); })
|
||||
.then(function() {
|
||||
if (turnOn)
|
||||
return execChecked(be.initd, ['enable']);
|
||||
return execChecked(be.initd, ['disable']);
|
||||
})
|
||||
.then(function() {
|
||||
if (turnOn && be.useNetns)
|
||||
return fs.exec('/sbin/ip', ['netns', 'del', 'daens']).catch(function() {});
|
||||
})
|
||||
.then(function() { return execChecked(be.initd, [action]); });
|
||||
}
|
||||
|
||||
function waitForService(be, turnOn, attempts) {
|
||||
return backend.serviceStatus(be.name).then(function(state) {
|
||||
if (!!state.running === turnOn)
|
||||
return state;
|
||||
if (attempts <= 0)
|
||||
throw new Error(turnOn ? _('Service did not start in time.') : _('Service did not stop in time.'));
|
||||
return new Promise(function(resolve) { setTimeout(resolve, 250); })
|
||||
.then(function() { return waitForService(be, turnOn, attempts - 1); });
|
||||
});
|
||||
}
|
||||
|
||||
function renderBackendSwitcher(ctx, onSwitched, initialHint) {
|
||||
if (!ctx.installed.dae && !ctx.installed.daed)
|
||||
return null;
|
||||
|
||||
const wrap = E('div', { 'class': 'dd-card dd-backend-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Backend')),
|
||||
E('div', { 'class': 'dd-backend-row' }, [
|
||||
E('span', { 'class': 'dd-backend-label' }, _('Active backend')),
|
||||
E('div', { 'class': 'dd-backend-segment' })
|
||||
])
|
||||
]);
|
||||
const segment = wrap.querySelector('.dd-backend-segment');
|
||||
const hint = E('div', { 'class': 'dd-backend-help' }, initialHint || '');
|
||||
let busy = false;
|
||||
|
||||
const showHint = function(msg) {
|
||||
hint.textContent = msg;
|
||||
};
|
||||
|
||||
const stopIfRunning = function(running) {
|
||||
const stops = [];
|
||||
|
||||
['dae', 'daed'].forEach(function(name) {
|
||||
if (running && running[name])
|
||||
stops.push(execChecked(backend.BACKENDS[name].initd, ['stop'])
|
||||
.then(function() { return waitForService(backend.BACKENDS[name], false, 40); }));
|
||||
});
|
||||
|
||||
if (stops.length)
|
||||
showHint(_('Stopping current backend…'));
|
||||
|
||||
return Promise.all(stops);
|
||||
};
|
||||
|
||||
['daed', 'dae'].forEach(function(name) {
|
||||
const active = ctx.name === name;
|
||||
const installed = !!ctx.installed[name];
|
||||
const btn = E('button', {
|
||||
'class': 'dd-backend-btn' + (active ? ' is-active' : ''),
|
||||
'type': 'button',
|
||||
'disabled': null,
|
||||
'title': installed ? _('Switch to %s').format(name) : _('%s is not installed').format(name)
|
||||
}, [
|
||||
E('span', {}, name),
|
||||
E('span', { 'class': 'dd-backend-state' }, active ? _('Active') : (installed ? '' : _('Not installed')))
|
||||
]);
|
||||
btn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (busy || active)
|
||||
return;
|
||||
if (!installed) {
|
||||
showHint(_('%s is not installed. Install it first, then switch backend.').format(name));
|
||||
return;
|
||||
}
|
||||
|
||||
busy = true;
|
||||
Array.prototype.forEach.call(segment.querySelectorAll('button'), function(b) { b.disabled = true; });
|
||||
showHint(_('Switching to %s…').format(name));
|
||||
|
||||
backend.detectRunning()
|
||||
.then(stopIfRunning)
|
||||
.then(function() { return backend.setActiveBackend(name); })
|
||||
.then(function() {
|
||||
const message = _('Switched to %s. Click start when you want it to run.').format(name);
|
||||
showHint(message);
|
||||
return onSwitched ? onSwitched(name, message) : null;
|
||||
})
|
||||
.catch(function(e) {
|
||||
busy = false;
|
||||
Array.prototype.forEach.call(segment.querySelectorAll('button'), function(b) { b.disabled = false; });
|
||||
showHint(_('Switch failed.'));
|
||||
});
|
||||
});
|
||||
segment.appendChild(btn);
|
||||
});
|
||||
|
||||
wrap.appendChild(hint);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderStatusCard(ctx, listenAddr) {
|
||||
const be = ctx.backend;
|
||||
let busy = false;
|
||||
let lastError = '';
|
||||
let refreshGeneration = 0;
|
||||
const probeState = {
|
||||
running: false,
|
||||
text: '',
|
||||
kind: '',
|
||||
visibleUntil: 0,
|
||||
hideTimer: null
|
||||
};
|
||||
const body = E('div', { 'id': 'dd-status-body' }, E('em', {}, _('Collecting data…')));
|
||||
const card = E('div', { 'class': 'dd-card dd-status-card' }, [
|
||||
E('h4', { 'class': 'dd-card-title' }, _('Service Status')),
|
||||
body
|
||||
]);
|
||||
|
||||
const render = function(state) {
|
||||
while (body.firstChild) body.removeChild(body.firstChild);
|
||||
|
||||
const badge = state.running
|
||||
? E('span', { 'class': 'dd-badge dd-badge-run' }, [ E('span', { 'class': 'dd-badge-dot' }), _('RUNNING') ])
|
||||
: E('span', { 'class': 'dd-badge dd-badge-stop' }, [ E('span', { 'class': 'dd-badge-dot' }), _('STOPPED') ]);
|
||||
|
||||
const meta = [
|
||||
E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, _('Backend')), be.name ])
|
||||
];
|
||||
|
||||
if (state.running && state.pid)
|
||||
meta.push(E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, 'PID'), state.pid ]));
|
||||
|
||||
const swErr = E('span', { 'class': 'dd-meta dd-err', 'style': lastError ? '' : 'display:none' }, lastError);
|
||||
const sw = E('button', { 'class': 'dd-switch' + (state.running ? ' is-on' : ''), 'type': 'button', 'aria-label': _('Toggle service') }, [
|
||||
E('span', { 'class': 'dd-switch-knob' })
|
||||
]);
|
||||
sw.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
refreshGeneration++;
|
||||
busy = true;
|
||||
sw.disabled = true;
|
||||
lastError = '';
|
||||
swErr.style.display = 'none';
|
||||
const turnOn = !state.running;
|
||||
/* instant optimistic feedback — the start/stop chain (esp. dae's eBPF
|
||||
load) takes a few seconds; flip the switch and show a pending label
|
||||
right away instead of looking frozen */
|
||||
sw.classList.toggle('is-on', turnOn);
|
||||
const lbl = sw.parentNode && sw.parentNode.querySelector('.dd-switch-label');
|
||||
if (lbl) lbl.textContent = '…';
|
||||
toggleService(be, turnOn)
|
||||
.then(function() { return waitForService(be, turnOn, 40); })
|
||||
.then(function() {
|
||||
busy = false;
|
||||
return refresh(true);
|
||||
})
|
||||
.catch(function(e) {
|
||||
busy = false;
|
||||
lastError = _('Toggle failed: %s').format(e.message || e);
|
||||
return refresh(true);
|
||||
});
|
||||
});
|
||||
|
||||
/* line 1: badge + toggle (always aligned, never wraps); line 2: meta */
|
||||
const row = E('div', { 'class': 'dd-status-row' }, [
|
||||
badge,
|
||||
E('span', { 'class': 'dd-grow' }),
|
||||
swErr,
|
||||
E('span', { 'class': 'dd-switch-wrap' }, [
|
||||
E('span', { 'class': 'dd-switch-label' }, state.running ? 'ON' : 'OFF'),
|
||||
sw
|
||||
])
|
||||
]);
|
||||
body.appendChild(row);
|
||||
if (meta.length)
|
||||
body.appendChild(E('div', { 'class': 'dd-status-meta' }, meta));
|
||||
|
||||
const actions = [];
|
||||
if (be.hasWebUI && state.running) {
|
||||
const port = (listenAddr || be.defaultListen).split(':').slice(-1)[0];
|
||||
actions.push(E('a', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'href': 'http://%s:%s'.format(window.location.hostname, port),
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener'
|
||||
}, _('Open WebUI')));
|
||||
}
|
||||
if (state.running) {
|
||||
const restart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Restart'));
|
||||
restart.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
restart.disabled = true;
|
||||
lastError = '';
|
||||
swErr.style.display = 'none';
|
||||
restartService(be)
|
||||
.then(function() {
|
||||
busy = false;
|
||||
return refresh(true);
|
||||
})
|
||||
.catch(function(e) {
|
||||
busy = false;
|
||||
lastError = _('Restart failed: %s').format(e.message || e);
|
||||
return refresh(true);
|
||||
});
|
||||
});
|
||||
actions.push(restart);
|
||||
}
|
||||
if (state.running && be.name === 'dae') {
|
||||
const hot = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Hot Reload'));
|
||||
hot.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
hot.disabled = true;
|
||||
execInit(be, 'hot_reload').finally(function() { hot.disabled = false; });
|
||||
});
|
||||
actions.push(hot);
|
||||
}
|
||||
if (state.running && be.name === 'dae') {
|
||||
const ckBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Test YouTube'));
|
||||
const probeVisible = probeState.running || probeState.visibleUntil > Date.now();
|
||||
const ckRes = E('span', {
|
||||
'class': 'dd-meta' + (probeState.kind ? ' ' + probeState.kind : ''),
|
||||
style: 'margin-left:8px;' + (probeVisible ? '' : 'display:none')
|
||||
}, probeState.text);
|
||||
ckBtn.disabled = probeState.running;
|
||||
ckBtn.addEventListener('click', function(ev) {
|
||||
ev.preventDefault();
|
||||
if (probeState.running)
|
||||
return;
|
||||
if (probeState.hideTimer) {
|
||||
clearTimeout(probeState.hideTimer);
|
||||
probeState.hideTimer = null;
|
||||
}
|
||||
probeState.running = true;
|
||||
probeState.text = _('Testing…');
|
||||
probeState.kind = '';
|
||||
probeState.visibleUntil = 0;
|
||||
ckBtn.disabled = true;
|
||||
ckRes.style.display = 'inline';
|
||||
ckRes.classList.remove('dd-ok', 'dd-err');
|
||||
ckRes.textContent = probeState.text;
|
||||
/* actually probe YouTube through dae's transparent proxy */
|
||||
fs.exec('/usr/share/luci-app-daede/proxy-check.sh', []).then(function(r) {
|
||||
const out = (r && r.stdout) || '';
|
||||
const ok = /\bok=1\b/.test(out);
|
||||
const code = (out.match(/code=(\S+)/) || [])[1] || '000';
|
||||
const ms = parseInt((out.match(/ms=(\d+)/) || [])[1] || '0');
|
||||
if (ok) {
|
||||
probeState.kind = 'dd-ok';
|
||||
probeState.text = _('YouTube reachable · %d ms').format(ms);
|
||||
} else {
|
||||
probeState.kind = 'dd-err';
|
||||
probeState.text = (code === '000')
|
||||
? _('YouTube unreachable (timeout)')
|
||||
: _('YouTube unreachable (HTTP %s)').format(code);
|
||||
}
|
||||
}).catch(function() {
|
||||
probeState.kind = 'dd-err';
|
||||
probeState.text = _('YouTube unreachable (timeout)');
|
||||
}).finally(function() {
|
||||
probeState.running = false;
|
||||
probeState.visibleUntil = Date.now() + 8000;
|
||||
refresh(true);
|
||||
probeState.hideTimer = setTimeout(function() {
|
||||
probeState.text = '';
|
||||
probeState.kind = '';
|
||||
probeState.visibleUntil = 0;
|
||||
probeState.hideTimer = null;
|
||||
refresh(true);
|
||||
}, 8000);
|
||||
});
|
||||
});
|
||||
actions.push(ckBtn);
|
||||
actions.push(ckRes);
|
||||
}
|
||||
if (actions.length)
|
||||
body.appendChild(E('div', { 'class': 'dd-actions' }, actions));
|
||||
};
|
||||
|
||||
const refresh = function(force) {
|
||||
if (busy && !force)
|
||||
return Promise.resolve();
|
||||
const generation = refreshGeneration;
|
||||
return backend.serviceStatus(be.name).then(function(state) {
|
||||
if (generation !== refreshGeneration || (busy && !force)) return;
|
||||
render(state);
|
||||
});
|
||||
};
|
||||
|
||||
poll.add(refresh);
|
||||
refresh();
|
||||
card._ddCleanup = function() {
|
||||
poll.remove(refresh);
|
||||
if (probeState.hideTimer)
|
||||
clearTimeout(probeState.hideTimer);
|
||||
};
|
||||
return card;
|
||||
}
|
||||
|
||||
function wrapSettingsCard(title, descr, mapPromise, advTitle, advFields) {
|
||||
return mapPromise.then(function(node) {
|
||||
const advBody = E('div', { 'class': 'dd-adv-body' });
|
||||
const advWrap = E('div', { 'class': 'dd-adv dd-closed' }, [
|
||||
E('div', { 'class': 'dd-adv-bar' }, [
|
||||
E('span', {}, advTitle),
|
||||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||||
]),
|
||||
advBody
|
||||
]);
|
||||
advWrap.firstChild.addEventListener('click', function() {
|
||||
advWrap.classList.toggle('dd-closed');
|
||||
});
|
||||
|
||||
(advFields || []).forEach(function(name) {
|
||||
const row = node.querySelector('[data-name="' + name + '"]');
|
||||
if (row) advBody.appendChild(row);
|
||||
});
|
||||
|
||||
const host = node.querySelector('.cbi-section') || node;
|
||||
if (advBody.firstChild) host.appendChild(advWrap);
|
||||
|
||||
const children = [ E('h4', { 'class': 'dd-card-title' }, title) ];
|
||||
if (descr) children.push(E('div', { 'class': 'dd-settings-descr' }, descr));
|
||||
children.push(node);
|
||||
return E('div', { 'class': 'dd-card dd-settings-card' }, children);
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
renderStatusCard: renderStatusCard,
|
||||
|
||||
renderBackendSwitcher: renderBackendSwitcher,
|
||||
|
||||
wrapSettingsCard: wrapSettingsCard
|
||||
|
||||
});
|
||||
@@ -0,0 +1,899 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Collecting data…"
|
||||
msgstr ""
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr ""
|
||||
|
||||
msgid "STOPPED"
|
||||
msgstr ""
|
||||
|
||||
msgid "Backend"
|
||||
msgstr ""
|
||||
|
||||
msgid "Listen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle service"
|
||||
msgstr ""
|
||||
|
||||
msgid "Open WebUI"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restart"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hot Reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active backend"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is not installed"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is not installed. Install it first, then switch backend."
|
||||
msgstr ""
|
||||
|
||||
msgid "Stopping current backend…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching to %s…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switched to %s. Click start when you want it to run."
|
||||
msgstr ""
|
||||
|
||||
msgid "Backend switched to %s. Click start to run it."
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to switch backend: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action \"%s\" error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action \"%s\" failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Action \"%s\" succeeded."
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Service did not start in time."
|
||||
msgstr ""
|
||||
|
||||
msgid "Service did not stop in time."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment."
|
||||
msgstr ""
|
||||
|
||||
msgid "daede Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Listen Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Max Log Size (MB)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rotate the log file once it grows past this many megabytes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Max Log Backups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Number of rotated log files to keep."
|
||||
msgstr ""
|
||||
|
||||
msgid "daed Username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Used by LuCI to access daed (subscription updates and import). If daed is not initialized yet, this account is used to create the daed admin."
|
||||
msgstr ""
|
||||
|
||||
msgid "daed Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable subscription auto-update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update Cycle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update Time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update subscriptions now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updating daed subscriptions…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscription update failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log Advanced Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "dae Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "eBPF-based Linux high-performance transparent proxy solution."
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration File"
|
||||
msgstr ""
|
||||
|
||||
msgid "dae Configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Text-only mode."
|
||||
msgstr ""
|
||||
|
||||
msgid "dae has no built-in WebUI. Edit the config DSL below by hand (subscriptions, nodes, routing, DNS). \"Initialize from example\" loads the bundled template — replace the placeholder subscription URL with your own before saving. For point-and-click management, switch backend to daed."
|
||||
msgstr ""
|
||||
|
||||
msgid "dae config file is empty. Click \"Initialize from example\" to start, or paste your config here."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save and Hot Reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Initialize from example"
|
||||
msgstr ""
|
||||
|
||||
msgid "+ Insert Block…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Insert config block at cursor"
|
||||
msgstr ""
|
||||
|
||||
msgid "lines"
|
||||
msgstr ""
|
||||
|
||||
msgid "⚠ %d placeholder"
|
||||
msgstr ""
|
||||
|
||||
msgid "⚠ 还差订阅链接才能跑:检测到 %d 行占位 URL"
|
||||
msgstr ""
|
||||
|
||||
msgid " / "
|
||||
msgstr ""
|
||||
|
||||
msgid "这些 "
|
||||
msgstr ""
|
||||
|
||||
msgid " 都是 dae 官方模板留的假地址,不是真订阅。把其中任意一行的 URL 换成你机场的订阅链接(删掉多余行),保存即可跑通。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Placeholder lines (click line number to jump):"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jump to line"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validating…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validate failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saving…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reload failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validated · saved · hot-reloaded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading example…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Example loaded"
|
||||
msgstr ""
|
||||
|
||||
msgid "Init failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Selected backend is not installed. Install dae or daed from the package feed first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log file does not exist yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Error reading log: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log file cleared."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to clear log: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Truncate %s log on the router? This cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Data Updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package Updates"
|
||||
msgstr ""
|
||||
|
||||
msgid "System Health"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Upgrade"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clean"
|
||||
msgstr ""
|
||||
|
||||
msgid "missing — click Update to fetch"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s download started — will refresh in 1–3 min."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s update already running."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s update failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s update error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s binary"
|
||||
msgstr ""
|
||||
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
msgid "installed"
|
||||
msgstr ""
|
||||
|
||||
msgid "latest"
|
||||
msgstr ""
|
||||
|
||||
msgid "up to date"
|
||||
msgstr ""
|
||||
|
||||
msgid "not installed via package manager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run \"apk upgrade %s\" now? This may restart the active backend."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade started — see log below."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade already running."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s upgrade error: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Probing…"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s process"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kernel BTF"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebUI/API"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hot reload"
|
||||
msgstr ""
|
||||
|
||||
msgid "netns daens"
|
||||
msgstr ""
|
||||
|
||||
msgid "pgrep reports the backend is running"
|
||||
msgstr ""
|
||||
|
||||
msgid "backend process is not running"
|
||||
msgstr ""
|
||||
|
||||
msgid "service is running, expected on port %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "service is stopped"
|
||||
msgstr ""
|
||||
|
||||
msgid "not available — eBPF needs CONFIG_DEBUG_INFO_BTF or vmlinux-btf"
|
||||
msgstr ""
|
||||
|
||||
msgid "/etc/init.d/dae hot_reload is available"
|
||||
msgstr ""
|
||||
|
||||
msgid "netns daens removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "exists, may block daed start"
|
||||
msgstr ""
|
||||
|
||||
msgid "clean"
|
||||
msgstr ""
|
||||
|
||||
msgid "%s Realtime Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto-scroll"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pause"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter (substring)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stop daed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stop daed now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "daed stopped"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to stop daed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "One node share link per line — ss, vmess, vless, trojan, tuic, hysteria2, socks5."
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "⚠ Still needs a subscription link: %d placeholder URL line(s) detected"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "These "
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid " are placeholder addresses from dae's template, not real subscriptions. Replace the URL on any one line with your airport subscription link (delete the spare lines) and save."
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Manual nodes"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Update all subscriptions"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Advanced settings"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "auto-generated"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Save and Apply"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Save config"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Save and Start"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Saved · applied"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Saved"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Saved · started"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Starting…"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Name is required"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Group name must be unique"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Updating…"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Subscriptions updated (dae reloaded)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscriptions updated and applied"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Save manual config"
|
||||
msgstr ""
|
||||
|
||||
#: dae.js
|
||||
msgid "Start failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Subscription Converter"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Convert Clash YAML into share links, preview the result, then import selected nodes into dae or daed. Inputs and credentials are not saved."
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "1. Input Clash YAML"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetch and parse"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Subscription User-Agent"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Auto User-Agent (recommended)"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Browser User-Agent"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fetcher returned an invalid response"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to read subscription data"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Or paste Clash YAML here"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Parse pasted YAML"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "2. Conversion Preview"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Select compatible new nodes"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Clear selection"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Filter name or protocol"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "3. Import Selected Nodes"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Target backend"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import selected nodes (%d)"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Detected %d · compatible %d · duplicates %d · incompatible %d · ignored info %d"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Ready to import"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Already exists, skipped"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Sign in to daed"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "The password is not saved. Sign-in is remembered in this browser for up to 30 days."
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Initialize daed"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Initialize and import"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "daed has not been initialized yet. The account below will be created as the daed admin (password needs at least 6 characters with both letters and numbers), then used to import."
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "daed is not initialized yet. Open the daed web panel (port 2023) to create an admin account, then import again."
|
||||
msgstr ""
|
||||
|
||||
msgid "3. Import Subscription"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import selected nodes as one subscription and add it to the default proxy group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscription name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a subscription name to import the selected nodes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Subscription \"%s\" is available on daed. Select nodes above to update it."
|
||||
msgstr ""
|
||||
|
||||
msgid "Update subscription \"%s\" with %d selected nodes in default group \"proxy\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Import %d selected nodes as subscription \"%s\" into default group \"proxy\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Update subscription (%d)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import subscription (%d)"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Sign in and import"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
#: converter.js
|
||||
msgid "YAML parser failed to load"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "daed returned HTTP %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import cancelled"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import selected nodes"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "No YAML parsed yet"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "YAML parse failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "No top-level proxies list was found"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "The server returned a Base64/share-link subscription, not Clash YAML. Try another subscription User-Agent or add the URL directly as a native subscription."
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Conversion preview is ready"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Conversion completed, but no supported nodes were found"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Unsupported protocol: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Enter an HTTP or HTTPS subscription URL"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetching subscription…"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetch failed"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Fetch failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Paste Clash YAML first"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "or"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Importing selected nodes…"
|
||||
msgstr ""
|
||||
|
||||
#: converter.js
|
||||
msgid "Import failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import complete: added %d, duplicates %d, failed %d"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create new airport"
|
||||
msgstr ""
|
||||
|
||||
msgid "Existing airport"
|
||||
msgstr ""
|
||||
|
||||
msgid "Airport name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync airport"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync airport (%d nodes)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Already exists, will be reused"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter an airport name"
|
||||
msgstr ""
|
||||
|
||||
msgid "No usable nodes were imported"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronizing airport…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync complete: added %d, reused %d, failed %d"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "This will delete the old nodes in \"%s\" and import %d new nodes. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid "3. Sync Airport Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "The selected nodes are kept together as one airport group. Re-syncing an existing airport safely replaces its old managed nodes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Detected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compatible"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duplicates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incompatible"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ignored info"
|
||||
msgstr ""
|
||||
|
||||
msgid "No nodes to preview yet. Fetch or paste Clash YAML above."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import node group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import node group (%d)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update node group (%d)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a group name to import the selected nodes."
|
||||
msgstr ""
|
||||
|
||||
msgid "dae group names may only contain letters, numbers, underscores, and hyphens, and must start with a letter or underscore."
|
||||
msgstr ""
|
||||
|
||||
msgid "Update node group \"%s\": replace it with %d selected nodes on %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Import %d selected nodes into group \"%s\" on %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter a group name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Replace node group \"%s\" with %d selected nodes?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Importing node group…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Node group imported: added %d, reused %d, failed %d"
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed with warning: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Managed subscription no longer exists in daed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Node group import failed: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "3. Import Node Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import the selected nodes as one named group, so large airport node lists stay easy to manage."
|
||||
msgstr ""
|
||||
|
||||
msgid "Node group \"%s\" is available on %s. Select nodes above to update it."
|
||||
msgstr ""
|
||||
|
||||
msgid "Config Backup"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "dae + daed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back up / restore the whole daede config (kernels excluded)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import overwrites the current daede config and restarts the backend. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Data Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto-update"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
|
||||
msgid "Weekly"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo data source saved."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "latest version unknown"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete the daens netns?"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"%s is running. Deleting the daens netns now will break its networking until "
|
||||
"you restart it. Continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DNS response TTL"
|
||||
msgstr ""
|
||||
|
||||
msgid "TTL returned to LAN DNS clients. 0 keeps dae default behavior."
|
||||
msgstr ""
|
||||
|
||||
msgid "Log level"
|
||||
msgstr ""
|
||||
|
||||
msgid "Debug and trace show per-connection routing details, but increase CPU usage and log volume. Enable them only while troubleshooting."
|
||||
msgstr ""
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,2 @@
|
||||
config daede 'config'
|
||||
option active_backend 'daed'
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/bin/sh
|
||||
# Run once on every (re-)install/upgrade of luci-app-daede: migrate legacy
|
||||
# config, drop stale files from the old luci-app-daed package, and seed the
|
||||
# default dae form sections. The netns cleanup that used to be patched into
|
||||
# daed's init from here now lives directly in daed's own stop_service().
|
||||
|
||||
set -e
|
||||
|
||||
# luci-app-daede owns its own LuCI menu and view path. Remove stale files from
|
||||
# the official luci-app-daed package and earlier daede builds that reused
|
||||
# /view/daed.
|
||||
rm -f /usr/share/luci/menu.d/luci-app-daed.json
|
||||
rm -f /usr/share/rpcd/acl.d/luci-app-daed.json
|
||||
rm -rf /usr/share/luci-app-daed
|
||||
rm -rf /www/luci-static/resources/view/daed
|
||||
rm -f /usr/lib/lua/luci/i18n/daed.zh-cn.lmo
|
||||
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
|
||||
rm -rf /tmp/luci-modulecache/* 2>/dev/null || true
|
||||
|
||||
if [ -f /etc/config/daed_luci ]; then
|
||||
old_backend="$(uci -q get daed_luci.config.active_backend || true)"
|
||||
if ! uci -q get daede.config >/dev/null 2>&1; then
|
||||
touch /etc/config/daede
|
||||
uci -q set daede.config=daede
|
||||
case "$old_backend" in
|
||||
dae|daed) uci -q set daede.config.active_backend="$old_backend" ;;
|
||||
esac
|
||||
uci -q commit daede
|
||||
elif ! uci -q get daede.config.active_backend >/dev/null 2>&1; then
|
||||
case "$old_backend" in
|
||||
dae|daed)
|
||||
uci -q set daede.config.active_backend="$old_backend"
|
||||
uci -q commit daede
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
rm -f /etc/config/daed_luci
|
||||
fi
|
||||
|
||||
if ! uci -q get daede.config >/dev/null 2>&1; then
|
||||
touch /etc/config/daede
|
||||
uci -q set daede.config=daede
|
||||
if [ -f /etc/init.d/daed ]; then
|
||||
uci -q set daede.config.active_backend='daed'
|
||||
else
|
||||
uci -q set daede.config.active_backend='dae'
|
||||
fi
|
||||
uci -q commit daede
|
||||
fi
|
||||
|
||||
# Remove --disable-timestamp from dae init so log lines carry timestamps
|
||||
DAE_INIT="/etc/init.d/dae"
|
||||
if [ -f "$DAE_INIT" ]; then
|
||||
sed -i 's/--disable-timestamp//' "$DAE_INIT"
|
||||
sed -i '/procd_append_param command *$/d' "$DAE_INIT"
|
||||
sed -i 's|procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray".*|procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"|' "$DAE_INIT"
|
||||
fi
|
||||
|
||||
# dae expects geoip.dat / geosite.dat under /usr/share/dae/;
|
||||
# v2ray-geoip installs them to /usr/share/v2ray/. Create symlinks.
|
||||
if [ -f /usr/share/v2ray/geoip.dat ] && [ ! -f /usr/share/dae/geoip.dat ]; then
|
||||
mkdir -p /usr/share/dae
|
||||
ln -sf /usr/share/v2ray/geoip.dat /usr/share/dae/geoip.dat
|
||||
fi
|
||||
if [ -f /usr/share/v2ray/geosite.dat ] && [ ! -f /usr/share/dae/geosite.dat ]; then
|
||||
mkdir -p /usr/share/dae
|
||||
ln -sf /usr/share/v2ray/geosite.dat /usr/share/dae/geosite.dat
|
||||
fi
|
||||
if [ -f /usr/share/v2ray/geoip.dat ] && [ ! -f /usr/share/daed/geoip.dat ]; then
|
||||
mkdir -p /usr/share/daed
|
||||
ln -sf /usr/share/v2ray/geoip.dat /usr/share/daed/geoip.dat
|
||||
fi
|
||||
if [ -f /usr/share/v2ray/geosite.dat ] && [ ! -f /usr/share/daed/geosite.dat ]; then
|
||||
mkdir -p /usr/share/daed
|
||||
ln -sf /usr/share/v2ray/geosite.dat /usr/share/daed/geosite.dat
|
||||
fi
|
||||
|
||||
# Seed default dae form sections (group/routing/dns) for the LuCI editor.
|
||||
# Idempotent: only add what's missing. Lives in the `dae` UCI package,
|
||||
# consumed by gen-dae-config.sh.
|
||||
if [ -f /etc/config/dae ]; then
|
||||
/usr/share/luci-app-daede/config-defaults.sh
|
||||
# Migrate legacy group filters to the unified source / name_filter fields,
|
||||
# then drop the old keys so the form shows real, editable values (the old
|
||||
# fields used to re-populate the new ones and looked un-deletable).
|
||||
# `uci get` joins a list into one space-separated line; the unquoted $() word-
|
||||
# splits it back into items. dae tags/keywords are whitespace-free, so this is
|
||||
# the intended split (do NOT set IFS=newline here — it would merge the items).
|
||||
i=0
|
||||
while uci -q get "dae.@group[$i]" >/dev/null 2>&1; do
|
||||
if [ -z "$(uci -q get dae.@group[$i].source)" ]; then
|
||||
for v in $(uci -q get dae.@group[$i].filter_sub 2>/dev/null); do
|
||||
uci -q add_list dae.@group[$i].source="$v"
|
||||
done
|
||||
fi
|
||||
if [ -z "$(uci -q get dae.@group[$i].name_filter)" ]; then
|
||||
for v in $(uci -q get dae.@group[$i].filter_node 2>/dev/null); do
|
||||
uci -q add_list dae.@group[$i].name_filter="$v"
|
||||
done
|
||||
fi
|
||||
uci -q delete dae.@group[$i].filter_sub 2>/dev/null
|
||||
uci -q delete dae.@group[$i].filter_node 2>/dev/null
|
||||
i=$((i+1))
|
||||
done
|
||||
uci -q commit dae
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/bin/sh
|
||||
# Whole-plugin backup/import/reset. Only fixed configuration paths are managed.
|
||||
set -eu
|
||||
umask 077
|
||||
|
||||
ACTION="${1:-}"
|
||||
UPLOAD="${2:-/tmp/daede-import.b64}"
|
||||
LOG=/tmp/luci-app-daede.backup.log
|
||||
LOCK=/tmp/luci-app-daede.config.lock
|
||||
SHARE=/usr/share/luci-app-daede
|
||||
MAX_TAR=184320 # Base64 must fit inside the ubus reply (~240 KiB).
|
||||
MAX_RAW=16777216
|
||||
FILES="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db etc/daed/wing.db-wal etc/daed/wing.db-shm"
|
||||
WORK=""
|
||||
SNAPSHOT=0
|
||||
MUTATING=0
|
||||
KEEP=0
|
||||
RESTART=""
|
||||
|
||||
fail() { echo "$*" >&2; exit 1; }
|
||||
b64enc() { ucode -e 'let f=require("fs"); print(b64enc(f.open(ARGV[0],"r").read("all")));' -- "$1"; }
|
||||
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); if(!o || o.write(r)!=length(r))exit(1); o.close();' -- "$1" "$2"; }
|
||||
# Do not merge pending CLI UCI deltas into snapshots/defaults.
|
||||
config() { /sbin/uci -c /etc/config -t "$WORK/uci" "$@"; }
|
||||
|
||||
paths() {
|
||||
printf '%s\n' $FILES
|
||||
for sub in "$1"/etc/dae/subscriptions/*.sub; do
|
||||
[ -e "$sub" ] || [ -L "$sub" ] || continue
|
||||
name="${sub##*/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'unexpected subscription filename' ;; esac
|
||||
printf 'etc/dae/subscriptions/%s\n' "$name"
|
||||
done
|
||||
for sub in "$1"/etc/daed/daede-sub-*; do
|
||||
[ -e "$sub" ] || [ -L "$sub" ] || continue
|
||||
name="${sub##*/daede-sub-}"
|
||||
case "$name" in ''|*[!A-Za-z0-9]*) fail 'unexpected daed subscription filename' ;; esac
|
||||
printf 'etc/daed/daede-sub-%s\n' "$name"
|
||||
done
|
||||
}
|
||||
check_paths() {
|
||||
for dir in /etc/config /etc/dae /etc/daed /etc/dae/subscriptions; do
|
||||
[ ! -L "$dir" ] || fail "refusing symlink: $dir"
|
||||
done
|
||||
paths / > "$WORK/paths"
|
||||
while IFS= read -r p; do
|
||||
[ ! -L "/$p" ] || fail "refusing symlink: /$p"
|
||||
[ ! -e "/$p" ] || [ -f "/$p" ] || fail "not a regular file: /$p"
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
cleanup_stages() {
|
||||
for stage in /etc/daed/.daede-sub-stage-*; do
|
||||
[ -e "$stage" ] || [ -L "$stage" ] || continue
|
||||
[ ! -L "$stage" ] || fail "refusing symlink: $stage"
|
||||
[ -f "$stage" ] || fail "not a regular file: $stage"
|
||||
rm -f "$stage" || fail "failed to remove stale snapshot stage: $stage"
|
||||
done
|
||||
}
|
||||
snapshot() {
|
||||
mkdir -p "$WORK/before"
|
||||
while IFS= read -r p; do
|
||||
[ -f "/$p" ] || continue
|
||||
mkdir -p "$WORK/before/${p%/*}"
|
||||
cp -p "/$p" "$WORK/before/$p"
|
||||
done < "$WORK/paths"
|
||||
SNAPSHOT=1
|
||||
}
|
||||
stop_backends() {
|
||||
for svc in dae daed; do
|
||||
[ -x "/etc/init.d/$svc" ] || continue
|
||||
if /etc/init.d/"$svc" running >/dev/null 2>&1; then RESTART="$RESTART $svc"; fi
|
||||
/etc/init.d/"$svc" stop >/dev/null 2>&1 || fail "failed to stop $svc; configuration unchanged"
|
||||
done
|
||||
# procd termination can be asynchronous. Never copy a live SQLite database.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if ! pidof dae daed daed-guard >/dev/null 2>&1; then return; fi
|
||||
sleep 1
|
||||
done
|
||||
fail 'backend still running; configuration unchanged'
|
||||
}
|
||||
disable_backends() {
|
||||
for svc in dae daed; do
|
||||
if [ -f "/etc/config/$svc" ]; then
|
||||
config -q set "$svc.config=$svc" || return 1
|
||||
config -q set "$svc.config.enabled=0" || return 1
|
||||
config -q commit "$svc" || return 1
|
||||
fi
|
||||
if [ -x "/etc/init.d/$svc" ]; then
|
||||
/etc/init.d/"$svc" disable >/dev/null 2>&1 || return 1
|
||||
/etc/init.d/"$svc" stop >/dev/null 2>&1 || return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
restore_snapshot() {
|
||||
# Remove files created by the failed operation as well as original files.
|
||||
paths / > "$WORK/current" || return 1
|
||||
while IFS= read -r p; do rm -f "/$p" || return 1; done < "$WORK/current"
|
||||
while IFS= read -r p; do
|
||||
[ -f "$WORK/before/$p" ] || continue
|
||||
mkdir -p "/${p%/*}" || return 1
|
||||
cp -p "$WORK/before/$p" "/$p" || return 1
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
sync_cron() {
|
||||
geo=disable; sub=disable
|
||||
[ "$(config -q get daede.config.geo_auto || :)" != 1 ] || geo=enable
|
||||
[ "$(config -q get daed.config.subscribe_auto_update || :)" != 1 ] || sub=enable
|
||||
"$SHARE/geo-cron.sh" "$geo" >/dev/null 2>&1
|
||||
"$SHARE/daed-sub-cron.sh" "$sub" >/dev/null 2>&1
|
||||
}
|
||||
finish() {
|
||||
rc=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
if [ "$rc" -ne 0 ] && [ "$MUTATING" = 1 ]; then
|
||||
disable_backends || true
|
||||
if [ "$SNAPSHOT" = 1 ] && ! restore_snapshot; then
|
||||
KEEP=1
|
||||
echo "rollback failed; root-only recovery files retained at $WORK" >&2
|
||||
else
|
||||
echo 'previous configuration restored; backends remain disabled' >&2
|
||||
fi
|
||||
disable_backends || { KEEP=1; echo 'failed to disable backend; check service state' >&2; }
|
||||
sync_cron || true
|
||||
fi
|
||||
if [ "$ACTION" = export ]; then
|
||||
for svc in $RESTART; do
|
||||
/etc/init.d/"$svc" start >/dev/null 2>&1 || { rc=1; echo "failed to resume $svc" >&2; }
|
||||
done
|
||||
fi
|
||||
if [ "$ACTION" != export ]; then
|
||||
if [ "$rc" = 0 ]; then echo '✓ 完成'; else echo '✗ 失败'; fi
|
||||
fi
|
||||
[ "$KEEP" = 1 ] || rm -rf "$WORK"
|
||||
rmdir "$LOCK"
|
||||
exit "$rc"
|
||||
}
|
||||
validate_archive() {
|
||||
[ "$(wc -c < "$WORK/upload.b64")" -le 245760 ] || fail 'upload too large'
|
||||
b64dec "$WORK/upload.b64" "$WORK/import.gz" || fail 'invalid base64 upload'
|
||||
[ "$(wc -c < "$WORK/import.gz")" -le "$MAX_TAR" ] || fail 'archive too large'
|
||||
gzip -t "$WORK/import.gz" 2>/dev/null || fail 'invalid gzip archive'
|
||||
gzip -dc "$WORK/import.gz" | head -c 16777217 > "$WORK/import.tar"
|
||||
[ "$(wc -c < "$WORK/import.tar")" -le "$MAX_RAW" ] || fail 'unpacked backup exceeds 16 MiB'
|
||||
tar -tf "$WORK/import.tar" > "$WORK/entries" 2>/dev/null || fail 'invalid tar archive'
|
||||
[ -s "$WORK/entries" ] || fail 'empty archive'
|
||||
# Reject links, directories, devices and duplicate entries before extraction.
|
||||
tar -tvf "$WORK/import.tar" > "$WORK/types" 2>/dev/null || fail 'invalid tar headers'
|
||||
if grep -qv '^-' "$WORK/types"; then fail 'only regular files are allowed'; fi
|
||||
[ -z "$(sort "$WORK/entries" | uniq -d)" ] || fail 'duplicate archive entry'
|
||||
while IFS= read -r p; do
|
||||
case "$p" in
|
||||
etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config.dae|etc/daed/wing.db|etc/daed/wing.db-wal|etc/daed/wing.db-shm) ;;
|
||||
etc/dae/subscriptions/*.sub)
|
||||
name="${p#etc/dae/subscriptions/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'invalid subscription path' ;; esac ;;
|
||||
etc/daed/daede-sub-*)
|
||||
name="${p#etc/daed/daede-sub-}"
|
||||
case "$name" in ''|*[!A-Za-z0-9]*) fail 'invalid daed subscription path' ;; esac ;;
|
||||
*) fail 'unexpected archive entry' ;;
|
||||
esac
|
||||
done < "$WORK/entries"
|
||||
mkdir -p "$WORK/after"
|
||||
tar -xf "$WORK/import.tar" -C "$WORK/after"
|
||||
found=0
|
||||
for svc in dae daed daede; do
|
||||
[ -f "$WORK/after/etc/config/$svc" ] || continue
|
||||
found=1
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q export "$svc" >/dev/null || fail "invalid $svc configuration"
|
||||
done
|
||||
[ "$found" = 1 ] || fail 'backup has no UCI configuration'
|
||||
if [ -f "$WORK/after/etc/daed/wing.db-wal" ]; then
|
||||
[ -f "$WORK/after/etc/daed/wing.db" ] || fail 'database WAL has no database'
|
||||
fi
|
||||
ab="$(/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q get daede.config.active_backend || echo dae)"
|
||||
case "$ab" in dae|daed) ;; *) fail 'invalid backend in backup' ;; esac
|
||||
[ -x "/etc/init.d/$ab" ] || fail "backup backend $ab is not installed"
|
||||
}
|
||||
run_action() {
|
||||
trap finish EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
check_paths
|
||||
if [ "$ACTION" = import ]; then validate_archive; fi
|
||||
if [ "$ACTION" = reset ]; then
|
||||
mkdir -p "$WORK/after/etc/config"
|
||||
for svc in dae daed daede; do
|
||||
cp "$SHARE/defaults/$svc" "$WORK/after/etc/config/$svc"
|
||||
done
|
||||
"$SHARE/config-defaults.sh" "$WORK/after/etc/config"
|
||||
if [ ! -x /etc/init.d/daed ]; then
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" set daede.config.active_backend=dae
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" commit daede
|
||||
fi
|
||||
fi
|
||||
stop_backends
|
||||
[ "$ACTION" = reset ] && cleanup_stages
|
||||
snapshot
|
||||
if [ "$ACTION" = export ]; then
|
||||
paths "$WORK/before" > "$WORK/candidates"
|
||||
: > "$WORK/entries"
|
||||
while IFS= read -r p; do [ ! -f "$WORK/before/$p" ] || echo "$p" >> "$WORK/entries"; done < "$WORK/candidates"
|
||||
[ -s "$WORK/entries" ] || fail 'no configuration to export'
|
||||
tar -czf "$WORK/export.gz" -C "$WORK/before" -T "$WORK/entries"
|
||||
[ "$(wc -c < "$WORK/export.gz")" -le "$MAX_TAR" ] || fail 'config too large; use System Backup instead'
|
||||
b64enc "$WORK/export.gz"
|
||||
return
|
||||
fi
|
||||
MUTATING=1
|
||||
disable_backends
|
||||
# Old backups may omit an uninstalled backend. Keep its UCI file, but remove
|
||||
# all managed data first so stale WAL/SHM and local subscriptions cannot leak in.
|
||||
while IFS= read -r p; do
|
||||
case "$p" in etc/config/*) continue ;; esac
|
||||
rm -f "/$p"
|
||||
done < "$WORK/paths"
|
||||
paths "$WORK/after" > "$WORK/replacement"
|
||||
while IFS= read -r p; do
|
||||
[ -f "$WORK/after/$p" ] || continue
|
||||
mkdir -p "/${p%/*}"
|
||||
cp "$WORK/after/$p" "/$p"
|
||||
chmod 600 "/$p"
|
||||
done < "$WORK/replacement"
|
||||
if [ "$ACTION" = reset ]; then
|
||||
disable_backends
|
||||
sync_cron
|
||||
echo 'All defaults restored. Backends are disabled; configure before starting manually.'
|
||||
else
|
||||
# Preserve the imported active backend's enabled flag. The other backend
|
||||
# stays disabled even when the archive was made on a dual-backend router.
|
||||
for svc in dae daed; do
|
||||
[ "$svc" = "$ab" ] || [ ! -f "/etc/config/$svc" ] || { config -q set "$svc.config.enabled=0"; config -q commit "$svc"; }
|
||||
done
|
||||
sync_cron
|
||||
if [ "$(config -q get "$ab.config.enabled" || :)" = 1 ]; then
|
||||
/etc/init.d/"$ab" enable
|
||||
/etc/init.d/"$ab" start
|
||||
/etc/init.d/"$ab" running >/dev/null 2>&1 || fail 'restored backend failed to start'
|
||||
else
|
||||
/etc/init.d/"$ab" disable
|
||||
fi
|
||||
echo 'Backup restored. Active backend follows the saved enabled setting.'
|
||||
fi
|
||||
}
|
||||
|
||||
case "$ACTION" in export|import|reset) ;; *) fail "usage: $0 export|import [upload]|reset" ;; esac
|
||||
# An atomic lock covers the complete operation, including background execution.
|
||||
mkdir "$LOCK" 2>/dev/null || fail 'another configuration operation is running'
|
||||
WORK="$(mktemp -d /tmp/daede-config.XXXXXX)" || { rmdir "$LOCK"; exit 1; }
|
||||
mkdir -p "$WORK/uci"
|
||||
if [ "$ACTION" = import ]; then
|
||||
case "$UPLOAD" in
|
||||
/tmp/daede-import.b64) ;;
|
||||
/tmp/daede-import.*.b64)
|
||||
token="${UPLOAD#/tmp/daede-import.}"; token="${token%.b64}"
|
||||
case "$token" in ''|*[!a-f0-9]*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;; esac ;;
|
||||
*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;;
|
||||
esac
|
||||
if [ ! -f "$UPLOAD" ] || [ -L "$UPLOAD" ]; then rm -rf "$WORK"; rmdir "$LOCK"; fail 'upload missing or unsafe'; fi
|
||||
mv "$UPLOAD" "$WORK/upload.b64" || { rm -rf "$WORK"; rmdir "$LOCK"; exit 1; }
|
||||
fi
|
||||
if [ "$ACTION" = export ]; then
|
||||
run_action
|
||||
else
|
||||
# Truncate before returning, so the frontend cannot mistake an earlier job
|
||||
# for this one. The child owns the lock and all cleanup from here on.
|
||||
: > "$LOG"
|
||||
(run_action) </dev/null >"$LOG" 2>&1 &
|
||||
echo "started in background, see $LOG"
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
# Shared installation/reset defaults. Optional directory is an isolated UCI tree.
|
||||
set -eu
|
||||
if [ "$#" -gt 0 ]; then
|
||||
config_dir="$1"
|
||||
mkdir -p "$config_dir/.uci"
|
||||
uci() { /sbin/uci -c "$config_dir" -t "$config_dir/.uci" "$@"; }
|
||||
fi
|
||||
|
||||
if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then
|
||||
uci -q set dae.config.lan_interface='br-lan'
|
||||
fi
|
||||
if ! uci -q show dae | grep -q "=group$"; then
|
||||
g="$(uci add dae group)"
|
||||
uci -q set "dae.$g.name=proxy"
|
||||
uci -q set "dae.$g.policy=min_moving_avg"
|
||||
fi
|
||||
if ! uci -q get dae.routing >/dev/null 2>&1; then
|
||||
uci -q set dae.routing=routing
|
||||
uci -q set dae.routing.private_direct=1
|
||||
uci -q set dae.routing.cn_direct=1
|
||||
uci -q set dae.routing.block_ads=0
|
||||
uci -q set dae.routing.fallback=proxy
|
||||
fi
|
||||
if ! uci -q get dae.dns >/dev/null 2>&1; then
|
||||
uci -q set dae.dns=dns
|
||||
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
|
||||
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
|
||||
uci -q set dae.dns.response_ttl='0'
|
||||
fi
|
||||
uci -q commit dae
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
# Manage daed subscription auto-update cron entry.
|
||||
|
||||
ACTION="$1"
|
||||
CRONTAB="/etc/crontabs/root"
|
||||
TAG="# luci-app-daede daed-sub-update"
|
||||
SCRIPT="/usr/share/luci-app-daede/daed-sub-update.sh"
|
||||
|
||||
clean() {
|
||||
[ -f "$CRONTAB" ] || return 0
|
||||
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
|
||||
mv "$CRONTAB.tmp" "$CRONTAB"
|
||||
}
|
||||
|
||||
clean
|
||||
|
||||
if [ "$ACTION" = "enable" ] && [ "$(uci -q get daed.config.subscribe_auto_update)" = "1" ]; then
|
||||
hour="$(uci -q get daed.config.subscribe_update_hour)"
|
||||
case "$hour" in
|
||||
''|*[!0-9]*) hour=4 ;;
|
||||
esac
|
||||
[ "$hour" -ge 0 ] 2>/dev/null && [ "$hour" -le 23 ] 2>/dev/null || hour=4
|
||||
|
||||
case "$(uci -q get daed.config.subscribe_update_cycle)" in
|
||||
weekly) sched="17 $hour * * 0" ;;
|
||||
*) sched="17 $hour * * *" ;;
|
||||
esac
|
||||
|
||||
mkdir -p /etc/crontabs
|
||||
{
|
||||
echo "$TAG"
|
||||
echo "$sched $SCRIPT >/dev/null 2>&1"
|
||||
} >> "$CRONTAB"
|
||||
/etc/init.d/cron enable >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
/etc/init.d/cron restart >/dev/null 2>&1
|
||||
exit 0
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/bin/sh
|
||||
# Update all daed subscriptions through dae-wing GraphQL.
|
||||
|
||||
LOG="/tmp/luci-app-daede.daed-sub-update.log"
|
||||
LOCK="/tmp/luci-app-daede.daed-sub-update.lock"
|
||||
|
||||
log() {
|
||||
echo "$(date '+%F %T') $*" >> "$LOG"
|
||||
}
|
||||
|
||||
fail() {
|
||||
local code="$1"; shift
|
||||
log "$*"
|
||||
echo "$*" >&2
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
[ -n "$TMPDIR" ] && rm -rf "$TMPDIR"
|
||||
rmdir "$LOCK" 2>/dev/null
|
||||
}
|
||||
|
||||
if ! mkdir "$LOCK" 2>/dev/null; then
|
||||
fail 1 "daed subscription update already running"
|
||||
fi
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
TMPDIR="$(mktemp -d /tmp/daed-sub-update.XXXXXX)" || exit 1
|
||||
|
||||
port="$(uci -q get daed.config.listen_addr | sed -n 's/.*:\([0-9]\{1,5\}\)$/\1/p')"
|
||||
[ -n "$port" ] || port=2023
|
||||
endpoint="http://127.0.0.1:$port/graphql"
|
||||
|
||||
username="$(uci -q get daed.config.dashboard_username)"
|
||||
password="$(uci -q get daed.config.dashboard_password)"
|
||||
|
||||
if [ -z "$username" ] || [ -z "$password" ]; then
|
||||
fail 2 "missing daed dashboard username or password"
|
||||
fi
|
||||
|
||||
if ! /bin/pidof daed >/dev/null 2>&1; then
|
||||
fail 3 "daed is not running"
|
||||
fi
|
||||
|
||||
post_graphql() {
|
||||
local body="$1" outfile="$2" auth="$3"
|
||||
if [ -n "$auth" ]; then
|
||||
uclient-fetch -q -O "$outfile" \
|
||||
--header="Content-Type: application/json" \
|
||||
--header="Authorization: Bearer $auth" \
|
||||
--post-file="$body" \
|
||||
"$endpoint" 2>>"$LOG"
|
||||
else
|
||||
uclient-fetch -q -O "$outfile" \
|
||||
--header="Content-Type: application/json" \
|
||||
--post-file="$body" \
|
||||
"$endpoint" 2>>"$LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
login_body="$TMPDIR/login.json"
|
||||
login_resp="$TMPDIR/login.out"
|
||||
printf '{"query":"query Login($username:String!,$password:String!){token(username:$username,password:$password)}","variables":{"username":"%s","password":"%s"}}' \
|
||||
"$(json_escape "$username")" "$(json_escape "$password")" > "$login_body"
|
||||
|
||||
log "starting daed subscription update"
|
||||
if ! post_graphql "$login_body" "$login_resp"; then
|
||||
fail 4 "login request failed"
|
||||
fi
|
||||
|
||||
token="$(sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$login_resp")"
|
||||
if [ -z "$token" ]; then
|
||||
fail 5 "login failed: $(cat "$login_resp" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
list_body="$TMPDIR/list.json"
|
||||
list_resp="$TMPDIR/list.out"
|
||||
printf '{"query":"query Subscriptions{subscriptions{id tag link}}","variables":{}}' > "$list_body"
|
||||
|
||||
if ! post_graphql "$list_body" "$list_resp" "$token"; then
|
||||
fail 6 "subscription list request failed"
|
||||
fi
|
||||
if grep -q '"errors"' "$list_resp"; then
|
||||
fail 7 "subscription list failed: $(cat "$list_resp" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
sed 's/"id"/\
|
||||
"id"/g' "$list_resp" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' > "$TMPDIR/ids"
|
||||
|
||||
count="$(grep -c . "$TMPDIR/ids" 2>/dev/null || echo 0)"
|
||||
if [ "${count:-0}" -eq 0 ]; then
|
||||
log "no daed subscriptions to update"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ok=0
|
||||
fail=0
|
||||
while IFS= read -r id || [ -n "$id" ]; do
|
||||
[ -n "$id" ] || continue
|
||||
update_body="$TMPDIR/update.json"
|
||||
update_resp="$TMPDIR/update.out"
|
||||
printf '{"query":"mutation Update($id:ID!){updateSubscription(id:$id){id tag status}}","variables":{"id":"%s"}}' \
|
||||
"$(json_escape "$id")" > "$update_body"
|
||||
if post_graphql "$update_body" "$update_resp" "$token" && ! grep -q '"errors"' "$update_resp"; then
|
||||
ok=$((ok + 1))
|
||||
log "updated subscription $id"
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
log "failed subscription $id: $(cat "$update_resp" 2>/dev/null)"
|
||||
fi
|
||||
done < "$TMPDIR/ids"
|
||||
|
||||
log "finished daed subscription update: ok=$ok fail=$fail total=$count"
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$count" -gt 0 ] && [ "$ok" -gt 0 ] && /bin/pidof daed >/dev/null 2>&1; then
|
||||
state_body="$TMPDIR/state.json"
|
||||
state_resp="$TMPDIR/state.out"
|
||||
printf '%s' '{"query":"query General{general{dae{running modified}}}","variables":{}}' > "$state_body"
|
||||
if ! post_graphql "$state_body" "$state_resp" "$token" || grep -q '"errors"' "$state_resp"; then
|
||||
fail 8 "failed to query daed runtime state: $(cat "$state_resp" 2>/dev/null)"
|
||||
fi
|
||||
if ! grep -q '"running"[[:space:]]*:[[:space:]]*true' "$state_resp"; then
|
||||
log "daed proxy is stopped; skipping runtime apply"
|
||||
exit 0
|
||||
fi
|
||||
if ! grep -q '"modified"[[:space:]]*:[[:space:]]*true' "$state_resp"; then
|
||||
log "daed runtime is already up to date"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
run_body="$TMPDIR/run.json"
|
||||
run_resp="$TMPDIR/run.out"
|
||||
printf '{"query":"mutation Run($dry:Boolean!){run(dry:$dry)}","variables":{"dry":false}}' > "$run_body"
|
||||
log "applying updated subscriptions"
|
||||
if ! post_graphql "$run_body" "$run_resp" "$token" || grep -q '"errors"' "$run_resp"; then
|
||||
fail 9 "failed to apply updated subscriptions: $(cat "$run_resp" 2>/dev/null)"
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -eu
|
||||
|
||||
CHUNK_BYTES=16384
|
||||
ACTION="${1:-}"
|
||||
|
||||
case "$ACTION" in
|
||||
chunk)
|
||||
TOKEN="${2:-}"
|
||||
INDEX="${3:-}"
|
||||
case "$TOKEN" in *[!A-Za-z0-9]*|'') exit 2 ;; esac
|
||||
case "$INDEX" in *[!0-9]*|'') exit 2 ;; esac
|
||||
FILE="/tmp/daede-clash-yaml-result.$TOKEN"
|
||||
[ -f "$FILE" ] || exit 4
|
||||
dd if="$FILE" bs="$CHUNK_BYTES" skip="$INDEX" count=1 2>/dev/null | hexdump -ve '1/1 "%02x"'
|
||||
exit 0
|
||||
;;
|
||||
cleanup)
|
||||
TOKEN="${2:-}"
|
||||
case "$TOKEN" in *[!A-Za-z0-9]*|'') exit 2 ;; esac
|
||||
rm -f "/tmp/daede-clash-yaml-result.$TOKEN"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
URL="${1:-}"
|
||||
UA_MODE="${2:-auto}"
|
||||
OUTPUT_MODE="${3:-stdout}"
|
||||
MAX_BYTES=5242880
|
||||
FETCH_BIN="${DAEDE_FETCH_BIN:-uclient-fetch}"
|
||||
|
||||
case "$URL" in
|
||||
http://*|https://*) ;;
|
||||
*)
|
||||
echo "Subscription URL must use HTTP or HTTPS" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP="$(mktemp /tmp/daede-clash-yaml.XXXXXX)"
|
||||
FIRST="$(mktemp /tmp/daede-clash-yaml-first.XXXXXX)"
|
||||
ERR="$(mktemp /tmp/daede-clash-yaml-error.XXXXXX)"
|
||||
trap 'rm -f "$TMP" "$FIRST" "$ERR"' EXIT INT TERM
|
||||
chmod 600 "$TMP"
|
||||
chmod 600 "$FIRST"
|
||||
chmod 600 "$ERR"
|
||||
|
||||
case "$UA_MODE" in
|
||||
auto|browser|ClashMeta|clash-verge/v2.4.2|ClashForWindows/0.20.39|Clash) ;;
|
||||
*)
|
||||
echo "Unsupported subscription User-Agent" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$OUTPUT_MODE" in
|
||||
stdout|handle) ;;
|
||||
*)
|
||||
echo "Unsupported output mode" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
LAST_ERROR="Failed to fetch subscription"
|
||||
|
||||
emit_result() {
|
||||
local source="$1" result token size
|
||||
if [ "$OUTPUT_MODE" = "stdout" ]; then
|
||||
cat "$source"
|
||||
return
|
||||
fi
|
||||
|
||||
# Keep each RPC response small. The browser reads this root-only temporary
|
||||
# file through the chunk action and removes it in a finally handler.
|
||||
find /tmp -maxdepth 1 -name 'daede-clash-yaml-result.*' -mmin +10 -delete 2>/dev/null || true
|
||||
result="$(mktemp /tmp/daede-clash-yaml-result.XXXXXX)"
|
||||
chmod 600 "$result"
|
||||
cp "$source" "$result"
|
||||
token="${result##*.}"
|
||||
size="$(wc -c <"$result" | tr -d ' ')"
|
||||
printf '%s\t%s\n' "$token" "$size"
|
||||
}
|
||||
|
||||
fetch_one() {
|
||||
local ua="$1" size response_error
|
||||
if [ "$ua" = "browser" ]; then
|
||||
ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
|
||||
fi
|
||||
: >"$TMP"
|
||||
: >"$ERR"
|
||||
if ! "$FETCH_BIN" -q -T 20 -U "$ua" -O "$TMP" "$URL" 2>"$ERR"; then
|
||||
response_error="$(head -c 512 "$TMP" | tr '\r\n\t' ' ')"
|
||||
LAST_ERROR="${response_error:-$(cat "$ERR")}"
|
||||
[ -n "$LAST_ERROR" ] || LAST_ERROR="Failed to fetch subscription"
|
||||
return 1
|
||||
fi
|
||||
|
||||
size="$(wc -c <"$TMP" | tr -d ' ')"
|
||||
if [ "$size" -eq 0 ]; then
|
||||
LAST_ERROR="Subscription response is empty"
|
||||
return 1
|
||||
fi
|
||||
if [ "$size" -gt "$MAX_BYTES" ]; then
|
||||
LAST_ERROR="Subscription response is too large (maximum 5 MiB)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "$UA_MODE" = "auto" ]; then
|
||||
for ua in ClashMeta clash-verge/v2.4.2 ClashForWindows/0.20.39 Clash browser; do
|
||||
if fetch_one "$ua"; then
|
||||
[ -s "$FIRST" ] || cp "$TMP" "$FIRST"
|
||||
if grep -Eq '^[[:space:]]*proxies[[:space:]]*:' "$TMP"; then
|
||||
emit_result "$TMP"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ -s "$FIRST" ]; then
|
||||
emit_result "$FIRST"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
if fetch_one "$UA_MODE"; then
|
||||
emit_result "$TMP"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$LAST_ERROR" >&2
|
||||
exit 3
|
||||
@@ -0,0 +1,422 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# gen-dae-config.sh - build /etc/dae/config.dae from the dae UCI form sections,
|
||||
# validate with `dae validate`, then overwrite + hot reload only on success.
|
||||
#
|
||||
# generate read UCI -> render config.dae -> validate -> overwrite + hot reload
|
||||
# import parse existing config.dae subscription{}/node{} -> fill UCI form
|
||||
#
|
||||
# UCI model (package dae):
|
||||
# config subscription tag/url, enabled
|
||||
# config node tag/link, enabled
|
||||
# config group name/policy, list source, list name_filter
|
||||
# (old list filter_sub/filter_node still read as fallback)
|
||||
# config routing 'routing' private_direct/cn_direct/block_ads/fallback, list custom
|
||||
# config dns 'dns' cn_upstream/fallback_upstream/response_ttl
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
CONFIG_DAE="/etc/dae/config.dae"
|
||||
TMP_GEN="/tmp/dae-gen.dae"
|
||||
DAE_BIN="/usr/bin/dae"
|
||||
DAE_INITD="/etc/init.d/dae"
|
||||
SUBS_DIR="/etc/dae/subscriptions"
|
||||
SUB_STAGE="/tmp/daede-sub.txt"
|
||||
|
||||
# dae single-quoted strings have no escaping; strip quotes/newlines defensively.
|
||||
sanitize() {
|
||||
printf '%s' "$1" | tr -d "'" | tr -d '\n\r'
|
||||
}
|
||||
|
||||
# dae subscription/node tags are bareword identifiers: only [A-Za-z0-9_] are
|
||||
# valid tokens, so a CJK/spaced tag (e.g. 白月光) makes the parser choke. Map
|
||||
# any such tag to a stable ASCII slug; pure-word tags pass through unchanged.
|
||||
# Deterministic on the input, so a group's source reference resolves to the
|
||||
# same slug as the subscription it points at. Empty stays empty (anonymous).
|
||||
dae_tag() {
|
||||
[ -n "$1" ] || return 0
|
||||
case "$1" in
|
||||
*[!A-Za-z0-9_]*) printf 's_%s' "$(printf '%s' "$1" | md5sum | cut -c1-10)" ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ===================== generate =====================
|
||||
|
||||
SUB_BUF=""
|
||||
NODE_BUF=""
|
||||
GROUP_BUF=""
|
||||
FIRST_GROUP=""
|
||||
|
||||
emit_sub() {
|
||||
local s="$1" tag url enabled
|
||||
config_get tag "$s" tag ""
|
||||
config_get url "$s" url ""
|
||||
config_get_bool enabled "$s" enabled 1
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
url="$(sanitize "$url")"
|
||||
[ -n "$url" ] || return 0
|
||||
tag="$(dae_tag "$(sanitize "$tag")")"
|
||||
if [ -n "$tag" ]; then
|
||||
SUB_BUF="${SUB_BUF} ${tag}: '${url}'
|
||||
"
|
||||
else
|
||||
SUB_BUF="${SUB_BUF} '${url}'
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
emit_node() {
|
||||
local s="$1" tag link enabled
|
||||
config_get tag "$s" tag ""
|
||||
config_get link "$s" link ""
|
||||
config_get_bool enabled "$s" enabled 1
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
link="$(sanitize "$link")"
|
||||
[ -n "$link" ] || return 0
|
||||
tag="$(dae_tag "$(sanitize "$tag")")"
|
||||
if [ -n "$tag" ]; then
|
||||
NODE_BUF="${NODE_BUF} ${tag}: '${link}'
|
||||
"
|
||||
else
|
||||
NODE_BUF="${NODE_BUF} '${link}'
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
emit_group() {
|
||||
local s="$1" name policy
|
||||
config_get name "$s" name ""
|
||||
config_get policy "$s" policy "min_moving_avg"
|
||||
name="$(sanitize "$name")"
|
||||
[ -n "$name" ] || return 0
|
||||
# dae fatals on duplicate outbound names; keep the first, skip later dupes
|
||||
case " $GROUP_NAMES_SEEN " in *" $name "*) return 0 ;; esac
|
||||
GROUP_NAMES_SEEN="$GROUP_NAMES_SEEN $name"
|
||||
[ -n "$FIRST_GROUP" ] || FIRST_GROUP="$name"
|
||||
|
||||
GROUP_BUF="${GROUP_BUF} ${name} {
|
||||
"
|
||||
# Build the source: 'source' entries that match a subscription tag become
|
||||
# subtag(), the rest (node tags / keywords) become name(). Old filter_sub /
|
||||
# filter_node configs still work as a fallback when 'source' is unset.
|
||||
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
|
||||
# source: new field, else old filter_sub (decoupled from the name filter so
|
||||
# editing one never silently drops the other)
|
||||
config_list_foreach "$s" source _collect_source
|
||||
[ -z "$SUBS_ACC" ] && [ -z "$NODES_ACC" ] && config_list_foreach "$s" filter_sub _collect_source
|
||||
# optional name filter narrows the subscription pool only (AND); hand-picked
|
||||
# nodes are explicit choices and stay in unconditionally
|
||||
config_list_foreach "$s" name_filter _collect_kw
|
||||
[ -z "$KW_ACC" ] && config_list_foreach "$s" filter_node _collect_kw
|
||||
local andclause=""
|
||||
[ -n "$KW_ACC" ] && andclause=" && name(${KW_ACC})"
|
||||
|
||||
local emitted=0
|
||||
# multiple filter lines = OR, so subtag and name lines union the sources
|
||||
if [ -n "$SUBS_ACC" ]; then
|
||||
GROUP_BUF="${GROUP_BUF} filter: subtag(${SUBS_ACC})${andclause}
|
||||
"
|
||||
emitted=1
|
||||
fi
|
||||
if [ -n "$NODES_ACC" ]; then
|
||||
GROUP_BUF="${GROUP_BUF} filter: name(${NODES_ACC})
|
||||
"
|
||||
emitted=1
|
||||
fi
|
||||
# name filter with no source = filter the whole pool by name
|
||||
if [ "$emitted" = 0 ] && [ -n "$KW_ACC" ]; then
|
||||
GROUP_BUF="${GROUP_BUF} filter: name(${KW_ACC})
|
||||
"
|
||||
fi
|
||||
GROUP_BUF="${GROUP_BUF} policy: ${policy}
|
||||
}
|
||||
"
|
||||
SUBS_ACC=""; NODES_ACC=""; KW_ACC=""
|
||||
}
|
||||
|
||||
SUBS_ACC=""
|
||||
NODES_ACC=""
|
||||
KW_ACC=""
|
||||
# Subscription tags, space-padded for membership tests (" tag " match).
|
||||
SUB_TAGS=""
|
||||
collect_subtag() {
|
||||
local t; config_get t "$1" tag ""
|
||||
t="$(dae_tag "$(sanitize "$t")")"
|
||||
[ -n "$t" ] && SUB_TAGS="${SUB_TAGS} ${t} "
|
||||
}
|
||||
is_subtag() {
|
||||
case "$SUB_TAGS" in *" $1 "*) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
# dae filter args are bare only for plain identifiers; anything with regex
|
||||
# chars / CJK / spaces must be wrapped as regex: '...' or the lexer chokes.
|
||||
filter_arg() {
|
||||
case "$1" in
|
||||
*[!A-Za-z0-9_]*) printf "regex: '%s'" "$1" ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
# name-filter args do substring matching: regex metachars -> regex:, else keyword:.
|
||||
# Intentionally stricter than filter_arg: a plain CJK keyword here stays a
|
||||
# substring `keyword:` match, whereas filter_arg wraps any non-word char as regex.
|
||||
kw_arg() {
|
||||
if printf '%s' "$1" | grep -q '[][|().*+?^$\\]'; then
|
||||
printf "regex: '%s'" "$1"
|
||||
else
|
||||
printf "keyword: '%s'" "$1"
|
||||
fi
|
||||
}
|
||||
# classify a source entry: subscription tag -> subtag bucket, else name bucket
|
||||
_collect_source() {
|
||||
local v="$(sanitize "$1")"
|
||||
[ -n "$v" ] || return 0
|
||||
# subscription refs resolve to the (ASCII) dae tag; node-name refs keep the
|
||||
# original text (matched as a quoted regex against node names)
|
||||
local dt="$(dae_tag "$v")"
|
||||
if is_subtag "$dt"; then
|
||||
SUBS_ACC="${SUBS_ACC:+$SUBS_ACC, }$(filter_arg "$dt")"
|
||||
else
|
||||
NODES_ACC="${NODES_ACC:+$NODES_ACC, }$(filter_arg "$v")"
|
||||
fi
|
||||
}
|
||||
_collect_kw() {
|
||||
local v="$(sanitize "$1")"
|
||||
[ -n "$v" ] || return 0
|
||||
KW_ACC="${KW_ACC:+$KW_ACC, }$(kw_arg "$v")"
|
||||
}
|
||||
|
||||
generate() {
|
||||
config_load dae
|
||||
|
||||
# global overridable knobs (fall back to defaults)
|
||||
local dial_mode log_level wan_interface lan_interface
|
||||
config_get dial_mode config dial_mode "domain"
|
||||
config_get log_level config log_level "info"
|
||||
config_get wan_interface config wan_interface "auto"
|
||||
config_get lan_interface config lan_interface "br-lan"
|
||||
|
||||
SUB_BUF=""; NODE_BUF=""; GROUP_BUF=""; FIRST_GROUP=""; GROUP_NAMES_SEEN=""
|
||||
SUB_TAGS=""
|
||||
config_foreach collect_subtag subscription
|
||||
config_foreach emit_sub subscription
|
||||
config_foreach emit_node node
|
||||
config_foreach emit_group group
|
||||
|
||||
# fall back to a default group when none defined
|
||||
if [ -z "$GROUP_BUF" ]; then
|
||||
GROUP_BUF=" proxy {
|
||||
policy: min_moving_avg
|
||||
}
|
||||
"
|
||||
FIRST_GROUP="proxy"
|
||||
fi
|
||||
|
||||
# routing / dns singletons
|
||||
local private_direct cn_direct block_ads fallback cn_up fb_up response_ttl
|
||||
config_get_bool private_direct routing private_direct 1
|
||||
config_get_bool cn_direct routing cn_direct 1
|
||||
config_get_bool block_ads routing block_ads 0
|
||||
config_get fallback routing fallback "$FIRST_GROUP"
|
||||
[ -n "$fallback" ] || fallback="$FIRST_GROUP"
|
||||
fallback="$(sanitize "$fallback")"
|
||||
config_get cn_up dns cn_upstream "udp://dns.alidns.com:53"
|
||||
config_get fb_up dns fallback_upstream "tcp+udp://dns.google:53"
|
||||
config_get response_ttl dns response_ttl "0"
|
||||
cn_up="$(sanitize "$cn_up")"
|
||||
fb_up="$(sanitize "$fb_up")"
|
||||
case "$response_ttl" in
|
||||
''|*[!0-9]*) response_ttl=0 ;;
|
||||
esac
|
||||
|
||||
{
|
||||
echo "# 本配置由 luci-app-daede 表单自动生成,再次保存表单会覆盖你的手动修改。"
|
||||
echo "global {"
|
||||
echo " tproxy_port: 12345"
|
||||
echo " tproxy_port_protect: true"
|
||||
echo " so_mark_from_dae: 0"
|
||||
echo " log_level: ${log_level}"
|
||||
[ -n "$lan_interface" ] && echo " lan_interface: ${lan_interface}"
|
||||
echo " wan_interface: ${wan_interface}"
|
||||
echo " auto_config_kernel_parameter: true"
|
||||
echo " dial_mode: ${dial_mode}"
|
||||
echo " tcp_check_url: 'http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111'"
|
||||
echo " udp_check_dns: 'dns.google:53,8.8.8.8,2001:4860:4860::8888'"
|
||||
echo " check_interval: 30s"
|
||||
echo " check_tolerance: 50ms"
|
||||
echo "}"
|
||||
echo ""
|
||||
|
||||
if [ -n "$SUB_BUF" ]; then
|
||||
echo "subscription {"
|
||||
printf '%s' "$SUB_BUF"
|
||||
echo "}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -n "$NODE_BUF" ]; then
|
||||
echo "node {"
|
||||
printf '%s' "$NODE_BUF"
|
||||
echo "}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "dns {"
|
||||
echo " ipversion_prefer: 4"
|
||||
[ "$response_ttl" -gt 0 ] && echo " response_ttl: ${response_ttl}"
|
||||
echo " upstream {"
|
||||
echo " cndns: '${cn_up}'"
|
||||
echo " fallbackdns: '${fb_up}'"
|
||||
echo " }"
|
||||
echo " routing {"
|
||||
echo " request {"
|
||||
[ "$block_ads" = "1" ] && echo " qname(geosite:category-ads-all) -> reject"
|
||||
echo " qname(geosite:cn) -> cndns"
|
||||
echo " fallback: fallbackdns"
|
||||
echo " }"
|
||||
echo " }"
|
||||
echo "}"
|
||||
echo ""
|
||||
|
||||
echo "group {"
|
||||
printf '%s' "$GROUP_BUF"
|
||||
echo "}"
|
||||
echo ""
|
||||
|
||||
echo "routing {"
|
||||
echo " pname(NetworkManager) -> direct"
|
||||
# multicast / broadcast direct (geoip:private doesn't cover these)
|
||||
echo " dip(224.0.0.0/3) -> direct"
|
||||
echo " dip(255.255.255.255/32) -> direct"
|
||||
echo " dip('ff00::/8') -> direct"
|
||||
[ "$private_direct" = "1" ] && echo " dip(geoip:private) -> direct"
|
||||
# NTP time sync direct
|
||||
echo " l4proto(udp) && dport(123) -> direct"
|
||||
# OS connectivity checks direct (avoid captive-portal false positives)
|
||||
echo " domain(connectivitycheck.gstatic.com) -> direct"
|
||||
echo " domain(msftconnecttest.com) -> direct"
|
||||
if [ "$cn_direct" = "1" ]; then
|
||||
echo " dip(geoip:cn) -> direct"
|
||||
echo " domain(geosite:cn) -> direct"
|
||||
fi
|
||||
[ "$block_ads" = "1" ] && echo " domain(geosite:category-ads-all) -> block"
|
||||
config_list_foreach routing custom _emit_custom
|
||||
echo " fallback: ${fallback}"
|
||||
echo "}"
|
||||
} > "$TMP_GEN"
|
||||
chmod 600 "$TMP_GEN" # dae rejects world/group-accessible config files
|
||||
|
||||
# overwrite live config only if validation passes
|
||||
local out
|
||||
out="$("$DAE_BIN" validate -c "$TMP_GEN" 2>&1)"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "validate failed: $(echo "$out" | head -1)" >&2
|
||||
rm -f "$TMP_GEN"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p /etc/dae
|
||||
cat "$TMP_GEN" > "$CONFIG_DAE"
|
||||
chmod 600 "$CONFIG_DAE"
|
||||
rm -f "$TMP_GEN"
|
||||
|
||||
if /usr/bin/pgrep -x dae >/dev/null 2>&1; then
|
||||
"$DAE_INITD" hot_reload >/dev/null 2>&1
|
||||
fi
|
||||
echo "ok"
|
||||
return 0
|
||||
}
|
||||
|
||||
_emit_custom() {
|
||||
local line="$(printf '%s' "$1" | tr -d '\n\r')"
|
||||
[ -n "$line" ] || return 0
|
||||
echo " ${line}"
|
||||
}
|
||||
|
||||
# ===================== import =====================
|
||||
# Fill the UCI form from an existing config.dae subscription{}/node{} block.
|
||||
# Idempotent: wipe same-type sections first, then rebuild from file.
|
||||
|
||||
import_block() {
|
||||
# $1=block name (subscription|node) $2=uci section type $3=link/url field
|
||||
local block="$1" stype="$2" field="$3"
|
||||
awk -v blk="$block" '
|
||||
$0 ~ "^[[:space:]]*"blk"[[:space:]]*\\{" { inb=1; next }
|
||||
inb && /^[[:space:]]*\}/ { inb=0 }
|
||||
inb { print }
|
||||
' "$CONFIG_DAE" 2>/dev/null | while IFS= read -r raw; do
|
||||
# drop comment, trim
|
||||
line="$(printf '%s' "$raw" | sed 's/#.*$//' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
||||
[ -n "$line" ] || continue
|
||||
tag=""; val=""
|
||||
case "$line" in
|
||||
\'*|\"*)
|
||||
# starts with a quote -> untagged link
|
||||
val="$line"
|
||||
;;
|
||||
[A-Za-z0-9_]*:*)
|
||||
# identifier ':' link -> tagged (split on first colon)
|
||||
tag="$(printf '%s' "$line" | sed "s/[[:space:]]*:.*//")"
|
||||
val="$(printf '%s' "$line" | sed "s/^[^:]*:[[:space:]]*//")"
|
||||
;;
|
||||
*)
|
||||
val="$line"
|
||||
;;
|
||||
esac
|
||||
# strip surrounding quotes
|
||||
val="$(printf '%s' "$val" | sed "s/^['\"]//; s/['\"]$//")"
|
||||
[ -n "$val" ] || continue
|
||||
# skip dae example placeholders (ss://LINK, server-ip:port, localhost:1080 ...)
|
||||
case "$val" in
|
||||
*://LINK|*LINK*|*server-ip*|*://localhost:*|*example.com*) continue ;;
|
||||
esac
|
||||
sid="$(uci add dae "$stype")"
|
||||
[ -n "$tag" ] && uci set "dae.$sid.tag=$tag"
|
||||
uci set "dae.$sid.$field=$val"
|
||||
uci set "dae.$sid.enabled=1"
|
||||
done
|
||||
}
|
||||
|
||||
do_import() {
|
||||
[ -f "$CONFIG_DAE" ] || { echo "no config.dae" >&2; return 1; }
|
||||
while uci -q delete dae.@subscription[0] >/dev/null 2>&1; do :; done
|
||||
while uci -q delete dae.@node[0] >/dev/null 2>&1; do :; done
|
||||
import_block subscription subscription url
|
||||
import_block node node link
|
||||
uci commit dae
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
# Converter writes a batch of share links to a local file that dae reads as a
|
||||
# file:// subscription, so a converted airport is one subscription instead of
|
||||
# hundreds of manual nodes. $1=id (airport id), reads links from SUB_STAGE.
|
||||
# id is restricted to [A-Za-z0-9_] to keep the path inside SUBS_DIR.
|
||||
do_write_sub() {
|
||||
local id="$1"
|
||||
case "$id" in ''|*[!A-Za-z0-9_]*) echo "bad id" >&2; return 2 ;; esac
|
||||
[ -f "$SUB_STAGE" ] || { echo "no staged data" >&2; return 1; }
|
||||
mkdir -p "$SUBS_DIR"
|
||||
# dae rejects subscription/config files readable by group/other (needs <=0640)
|
||||
cp "$SUB_STAGE" "$SUBS_DIR/$id.sub.tmp" || { echo "write failed" >&2; return 1; }
|
||||
chmod 0600 "$SUBS_DIR/$id.sub.tmp"
|
||||
mv "$SUBS_DIR/$id.sub.tmp" "$SUBS_DIR/$id.sub"
|
||||
rm -f "$SUB_STAGE"
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
do_delete_sub() {
|
||||
local id="$1"
|
||||
case "$id" in ''|*[!A-Za-z0-9_]*) return 0 ;; esac
|
||||
rm -f "$SUBS_DIR/$id.sub"
|
||||
echo "ok"
|
||||
}
|
||||
|
||||
# ===================== entry =====================
|
||||
|
||||
case "$1" in
|
||||
generate) generate ;;
|
||||
import) do_import ;;
|
||||
write-sub) do_write_sub "$2" ;;
|
||||
delete-sub) do_delete_sub "$2" ;;
|
||||
*) echo "usage: $0 {generate|import|write-sub <id>|delete-sub <id>}" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# geo-cron.sh enable|disable
|
||||
# Manage the geo auto-update crontab entry. enable reads the schedule from
|
||||
# daede.config.geo_auto_freq (daily|weekly); both actions are idempotent.
|
||||
|
||||
ACTION="$1"
|
||||
CRONTAB="/etc/crontabs/root"
|
||||
TAG="# luci-app-daede geo-update"
|
||||
SCRIPT="/usr/share/luci-app-daede/update-geo.sh"
|
||||
|
||||
# Drop any previous daede geo entry (tag line + its command line).
|
||||
clean() {
|
||||
[ -f "$CRONTAB" ] || return 0
|
||||
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
|
||||
mv "$CRONTAB.tmp" "$CRONTAB"
|
||||
}
|
||||
|
||||
clean
|
||||
|
||||
if [ "$ACTION" = "enable" ]; then
|
||||
case "$(uci -q get daede.config.geo_auto_freq)" in
|
||||
weekly) sched="17 4 * * 0" ;; # Sunday 04:17
|
||||
*) sched="17 4 * * *" ;; # every day 04:17
|
||||
esac
|
||||
mkdir -p /etc/crontabs
|
||||
{
|
||||
echo "$TAG"
|
||||
echo "$sched $SCRIPT geoip; $SCRIPT geosite"
|
||||
} >> "$CRONTAB"
|
||||
/etc/init.d/cron enable >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
/etc/init.d/cron restart >/dev/null 2>&1
|
||||
exit 0
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# pkg-info.sh <package>
|
||||
# Prints "<installed>\t<latest>" for the named package, where either field is
|
||||
# empty when unknown. Used by the Updates view to decide whether to enable
|
||||
# the [Upgrade] button.
|
||||
|
||||
PKG="$1"
|
||||
case "$PKG" in
|
||||
dae|daed|luci-app-daede) ;;
|
||||
*) echo "" ; exit 64 ;;
|
||||
esac
|
||||
|
||||
installed=""
|
||||
latest=""
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
if apk info -e "$PKG" >/dev/null 2>&1; then
|
||||
installed=$(apk list -I "$PKG" 2>/dev/null | awk -v p="$PKG" '
|
||||
$1 ~ "^" p "-" {
|
||||
sub("^" p "-", "", $1);
|
||||
print $1;
|
||||
exit
|
||||
}
|
||||
')
|
||||
fi
|
||||
# apk search treats ^ and $ as literal chars (not regex anchors), so the old
|
||||
# `apk search "^pkg$"` matched nothing. Enumerate the exact-name package
|
||||
# across all feeds via `apk list` and take the highest version. Restricting
|
||||
# to "<pkg>-<digit>" avoids sibling names like <pkg>-geoip / luci-app-<pkg>.
|
||||
latest=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '
|
||||
$1 ~ "^" p "-[0-9]" {
|
||||
v = $1; sub("^" p "-", "", v);
|
||||
print v
|
||||
}
|
||||
' | sort -V | tail -1)
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
installed=$(opkg status "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
|
||||
latest=$(opkg info "$PKG" 2>/dev/null | awk -F': ' '$1=="Version"{print $2; exit}')
|
||||
fi
|
||||
|
||||
case "$PKG" in
|
||||
dae|daed)
|
||||
case "$installed" in
|
||||
20[0-9][0-9].[0-9]*) ;;
|
||||
*) installed="" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\t%s\n' "$installed" "$latest"
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# proxy-check.sh - test whether dae's transparent proxy can actually reach a
|
||||
# blocked site (default YouTube). dae proxies the router's own WAN-bound traffic
|
||||
# (wan_interface), so a plain request from here is routed through dae just like a
|
||||
# LAN client would be. Prints one line: ok=<0|1> code=<http> ms=<n>
|
||||
|
||||
URL="${1:-https://www.youtube.com/generate_204}"
|
||||
|
||||
code=000
|
||||
ms=0
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
out="$(curl -4 -sL --max-time 8 --connect-timeout 4 \
|
||||
-o /dev/null -w '%{http_code} %{time_total}' "$URL" 2>/dev/null)"
|
||||
code="$(printf '%s' "$out" | awk '{print $1}')"
|
||||
t="$(printf '%s' "$out" | awk '{print $2}')"
|
||||
[ -n "$code" ] || code=000
|
||||
ms="$(awk -v x="${t:-0}" 'BEGIN{printf "%d", x*1000}')"
|
||||
else
|
||||
# busybox wget has no status/timing output — success is exit 0
|
||||
if wget -q -T 8 -O /dev/null "$URL" 2>/dev/null; then code=204; else code=000; fi
|
||||
fi
|
||||
|
||||
ok=0
|
||||
case "$code" in 200|204|301|302) ok=1 ;; esac
|
||||
|
||||
echo "ok=$ok code=$code ms=$ms"
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Background apk/opkg update for the Updates view. Returns at once; throttled.
|
||||
|
||||
LOCK=/tmp/luci-app-daede.idx.lock
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
[ "$(( $(date +%s) - mtime ))" -lt 90 ] && exit 0
|
||||
fi
|
||||
: > "$LOCK"
|
||||
|
||||
(
|
||||
# -n: skip if a package op holds the shared apk lock (no need to refresh then)
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
flock -n /tmp/luci-app-daede.apk.lock apk update
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
opkg update
|
||||
fi
|
||||
) >/dev/null 2>&1 </dev/null &
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
# Publish converter snapshots without exposing a partially written final file.
|
||||
|
||||
set -eu
|
||||
|
||||
DIR=/etc/daed
|
||||
PREFIX=daede-sub
|
||||
STAGE_PREFIX=.daede-sub-stage
|
||||
|
||||
fail() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
valid_token() {
|
||||
[ "$#" -eq 1 ] || return 1
|
||||
case "$1" in
|
||||
''|*[!A-Za-z0-9]*) return 1 ;;
|
||||
esac
|
||||
[ "${#1}" -le 64 ]
|
||||
}
|
||||
|
||||
stage_path() { printf '%s/%s-%s\n' "$DIR" "$STAGE_PREFIX" "$1"; }
|
||||
final_path() { printf '%s/%s-%s\n' "$DIR" "$PREFIX" "$1"; }
|
||||
|
||||
regular_file() {
|
||||
[ -f "$1" ] && [ ! -L "$1" ]
|
||||
}
|
||||
|
||||
safe_dir() {
|
||||
[ -d "$DIR" ] && [ ! -L "$DIR" ] || fail "invalid snapshot directory"
|
||||
}
|
||||
|
||||
publish() {
|
||||
[ "$#" -eq 1 ] || fail 'publish expects one token'
|
||||
valid_token "$1" || fail 'invalid snapshot token'
|
||||
safe_dir
|
||||
stage=$(stage_path "$1")
|
||||
final=$(final_path "$1")
|
||||
regular_file "$stage" || fail 'snapshot stage is not a regular file'
|
||||
[ ! -e "$final" ] && [ ! -L "$final" ] || fail 'snapshot already exists'
|
||||
chmod 600 "$stage" || fail 'cannot secure snapshot stage'
|
||||
mv -f "$stage" "$final" || fail 'cannot publish snapshot'
|
||||
}
|
||||
|
||||
replace() {
|
||||
[ "$#" -eq 2 ] || fail 'replace expects existing and stage tokens'
|
||||
valid_token "$1" || fail 'invalid existing snapshot token'
|
||||
valid_token "$2" || fail 'invalid stage snapshot token'
|
||||
safe_dir
|
||||
[ "$1" != "$2" ] || fail 'existing and stage tokens must differ'
|
||||
stage=$(stage_path "$2")
|
||||
final=$(final_path "$1")
|
||||
regular_file "$stage" || fail 'snapshot stage is not a regular file'
|
||||
if [ -e "$final" ] || [ -L "$final" ]; then
|
||||
regular_file "$final" || fail 'existing snapshot is not a regular file'
|
||||
fi
|
||||
chmod 600 "$stage" || fail 'cannot secure snapshot stage'
|
||||
mv -f "$stage" "$final" || fail 'cannot replace snapshot'
|
||||
}
|
||||
|
||||
discard() {
|
||||
[ "$#" -eq 1 ] || fail 'discard expects one token'
|
||||
valid_token "$1" || fail 'invalid snapshot token'
|
||||
safe_dir
|
||||
stage=$(stage_path "$1")
|
||||
if [ -L "$stage" ]; then
|
||||
fail 'refusing symlink snapshot stage'
|
||||
fi
|
||||
if [ -e "$stage" ]; then
|
||||
[ -f "$stage" ] || fail 'snapshot stage is not a regular file'
|
||||
rm -f "$stage" || fail 'cannot discard snapshot stage'
|
||||
fi
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
publish) shift; publish "$@" ;;
|
||||
replace) shift; replace "$@" ;;
|
||||
discard) shift; discard "$@" ;;
|
||||
*) fail 'unknown snapshot action' ;;
|
||||
esac
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
# update-geo.sh <geoip|geosite>
|
||||
# Fork-and-detach so the LuCI RPC call returns immediately. The diagnostics
|
||||
# card polls the .dat file's mtime/size to detect completion. A lock file
|
||||
# in /tmp prevents concurrent runs.
|
||||
|
||||
TYPE="$1"
|
||||
# Default source: Loyalsoldier/v2ray-rules-dat — daily-built, China-optimized,
|
||||
# ships both geoip.dat and geosite.dat. Users can override per-type via
|
||||
# daede.config.geoip_url / geosite_url (empty falls back to the default below).
|
||||
case "$TYPE" in
|
||||
geoip)
|
||||
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
|
||||
URL="$(uci -q get daede.config.geoip_url)"
|
||||
DEST="/usr/share/v2ray/geoip.dat"
|
||||
;;
|
||||
geosite)
|
||||
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
|
||||
URL="$(uci -q get daede.config.geosite_url)"
|
||||
DEST="/usr/share/v2ray/geosite.dat"
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 <geoip|geosite>" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
[ -n "$URL" ] || URL="$DEF_URL"
|
||||
|
||||
LOCK="/tmp/luci-app-daede.${TYPE}.lock"
|
||||
LOG="/tmp/luci-app-daede.${TYPE}.log"
|
||||
RLOCK="/tmp/luci-app-daede.geo-reload.lock"
|
||||
|
||||
schedule_backend_reload() {
|
||||
if [ -d "$RLOCK" ]; then
|
||||
rmt=$(date -r "$RLOCK" +%s 2>/dev/null || echo 0)
|
||||
[ $(( $(date +%s) - rmt )) -gt 300 ] && rmdir "$RLOCK" 2>/dev/null
|
||||
fi
|
||||
mkdir "$RLOCK" 2>/dev/null || return 0
|
||||
(
|
||||
sleep 25
|
||||
ab="$(uci -q get daede.config.active_backend 2>/dev/null || echo daed)"
|
||||
if [ "$ab" = dae ]; then
|
||||
bin=dae; act=hot_reload
|
||||
else
|
||||
bin=daed; act=reload
|
||||
fi
|
||||
if /bin/pidof "$bin" >/dev/null 2>&1; then
|
||||
if "/etc/init.d/$bin" "$act" >/dev/null 2>&1; then
|
||||
echo "$(date '+%F %T') reloaded $bin"
|
||||
else
|
||||
echo "$(date '+%F %T') $bin reload failed"
|
||||
fi
|
||||
else
|
||||
echo "$(date '+%F %T') $bin not running; skip reload"
|
||||
fi
|
||||
rmdir "$RLOCK" 2>/dev/null
|
||||
) </dev/null >>"$LOG" 2>&1 &
|
||||
}
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
# Lock is fresh (< 5 min)? Refuse. Stale? Remove and proceed.
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
age=$(( $(date +%s) - mtime ))
|
||||
if [ "$age" -lt 300 ]; then
|
||||
echo "${TYPE} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
|
||||
exit 75
|
||||
fi
|
||||
rm -f "$LOCK"
|
||||
fi
|
||||
|
||||
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
|
||||
echo "${TYPE} update already in progress" >&2
|
||||
exit 75
|
||||
fi
|
||||
|
||||
# Spawn detached worker — parent returns immediately to LuCI.
|
||||
(
|
||||
exec >"$LOG" 2>&1
|
||||
trap 'rm -f "$LOCK"' EXIT INT TERM
|
||||
|
||||
rc=0
|
||||
TMP="${DEST}.new"
|
||||
mkdir -p "$(dirname "$DEST")"
|
||||
echo "$(date '+%F %T') begin: $URL"
|
||||
|
||||
if ! curl -fsSL --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
|
||||
echo "$(date '+%F %T') download failed"
|
||||
rm -f "$TMP"
|
||||
rc=1
|
||||
else
|
||||
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
|
||||
magic=$(hexdump -n 1 -e '1/1 "%02x"' "$TMP" 2>/dev/null)
|
||||
if [ "$size" -lt 1024 ]; then
|
||||
echo "$(date '+%F %T') file too small ($size bytes)"
|
||||
rm -f "$TMP"
|
||||
rc=2
|
||||
elif [ "$magic" != "0a" ]; then
|
||||
echo "$(date '+%F %T') invalid geodata format"
|
||||
rm -f "$TMP"
|
||||
rc=3
|
||||
else
|
||||
mv "$TMP" "$DEST"
|
||||
echo "$(date '+%F %T') updated $DEST ($size bytes)"
|
||||
schedule_backend_reload
|
||||
fi
|
||||
fi
|
||||
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
|
||||
echo "started in background, see $LOG"
|
||||
exit 0
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/sh
|
||||
# update-pkg.sh <dae|daed|luci-app-daede>
|
||||
# Refresh package indexes and upgrade the named package via apk (25.12+) or
|
||||
# opkg (24.10). Forks the work to background so the LuCI RPC call returns
|
||||
# immediately; the result is streamed to /tmp/luci-app-daede.pkg.<name>.log.
|
||||
|
||||
PKG="$1"
|
||||
case "$PKG" in
|
||||
dae|daed|luci-app-daede) ;;
|
||||
*)
|
||||
echo "usage: $0 <dae|daed|luci-app-daede>" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
# run from a /tmp copy so upgrading luci-app-daede (which replaces this script)
|
||||
# can't corrupt the in-flight upgrade
|
||||
case "$0" in
|
||||
/tmp/.daede-upd-*) ;;
|
||||
*)
|
||||
_self="/tmp/.daede-upd-$$"
|
||||
cp "$0" "$_self" 2>/dev/null && exec sh "$_self" "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
LOCK="/tmp/luci-app-daede.pkg-${PKG}.lock"
|
||||
LOG="/tmp/luci-app-daede.pkg-${PKG}.log"
|
||||
|
||||
if [ -f "$LOCK" ]; then
|
||||
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
|
||||
age=$(( $(date +%s) - mtime ))
|
||||
if [ "$age" -lt 300 ]; then
|
||||
echo "${PKG} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
|
||||
exit 75
|
||||
fi
|
||||
rm -f "$LOCK"
|
||||
fi
|
||||
|
||||
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
|
||||
echo "${PKG} update already in progress" >&2
|
||||
exit 75
|
||||
fi
|
||||
|
||||
(
|
||||
exec >"$LOG" 2>&1
|
||||
trap 'rm -f "$LOCK"; [ "${0#/tmp/.daede-upd-}" != "$0" ] && rm -f "$0"' EXIT INT TERM
|
||||
|
||||
echo "$(date '+%F %T') begin upgrade: $PKG"
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
# shared apk lock with the bg index refresh (avoid "Unable to lock database")
|
||||
(
|
||||
flock 9
|
||||
apk update 2>&1
|
||||
ver=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '$1 ~ "^" p "-[0-9]" { v=$1; sub("^" p "-", "", v); print v }' | sort -V | tail -1)
|
||||
if [ -n "$ver" ]; then
|
||||
constraint="$PKG=$ver"
|
||||
else
|
||||
echo "result: 软件源中没有找到 $PKG,请检查网络或软件源配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- apk add -s $constraint ---"
|
||||
if ! apk add -s "$constraint" 2>&1; then
|
||||
echo "note: apk 预检失败,通常是系统上其它软件包的问题,继续尝试升级"
|
||||
fi
|
||||
echo "--- apk add $constraint ---"
|
||||
apk add "$constraint" 2>&1
|
||||
exit 0
|
||||
) 9>/tmp/luci-app-daede.apk.lock
|
||||
rc=$?
|
||||
if [ "$rc" != 0 ]; then
|
||||
:
|
||||
elif ! apk list --installed 2>/dev/null | grep -q "^${PKG}-"; then
|
||||
echo "result: $PKG is not installed"
|
||||
rc=1
|
||||
elif apk list -u 2>/dev/null | grep -q "^${PKG}-"; then
|
||||
echo "result: $PKG still has a pending upgrade"
|
||||
rc=1
|
||||
else
|
||||
echo "result: $PKG is at the latest available version"
|
||||
rc=0
|
||||
fi
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
echo "--- opkg update ---"
|
||||
opkg update 2>&1
|
||||
echo "--- opkg upgrade $PKG ---"
|
||||
opkg upgrade "$PKG" 2>&1
|
||||
rc=$?
|
||||
else
|
||||
echo "no package manager found"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
|
||||
|
||||
# luci-app-daede upgrade replaces ACL JSON — reload rpcd so changes apply.
|
||||
if [ "$PKG" = "luci-app-daede" ] && [ "$rc" = "0" ]; then
|
||||
echo "reloading rpcd to pick up new ACL"
|
||||
/etc/init.d/rpcd reload 2>&1
|
||||
fi
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
|
||||
echo "started in background, see $LOG"
|
||||
exit 0
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"admin/services/daede": {
|
||||
"title": "daede",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-daede" ]
|
||||
}
|
||||
},
|
||||
"admin/services/daede/config": {
|
||||
"title": "Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/config"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/converter": {
|
||||
"title": "Subscription Converter",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/converter"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/updates": {
|
||||
"title": "Updates",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/updates"
|
||||
}
|
||||
},
|
||||
"admin/services/daede/log": {
|
||||
"title": "Log",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "daede/log"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"luci-app-daede": {
|
||||
"description": "Grant access to dae/daed configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/init.d/dae": [ "read" ],
|
||||
"/etc/init.d/daed": [ "read" ],
|
||||
"/etc/config/daede": [ "read" ],
|
||||
"/etc/dae/config.dae": [ "read" ],
|
||||
"/etc/dae/example.dae": [ "read" ],
|
||||
"/var/log/dae/dae.log": [ "read" ],
|
||||
"/var/log/daed/daed.log": [ "read" ],
|
||||
"/usr/share/v2ray/geoip.dat": [ "read" ],
|
||||
"/usr/share/v2ray/geosite.dat": [ "read" ],
|
||||
"/sys/kernel/btf/vmlinux": [ "read" ],
|
||||
"/usr/lib/debug/boot/vmlinux": [ "read" ],
|
||||
"/run/netns/daens": [ "read" ],
|
||||
"/tmp/luci-app-daede.geoip.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.geosite.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.pkg-dae.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.pkg-daed.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.pkg-luci-app-daede.log": [ "read" ],
|
||||
"/tmp/luci-app-daede.backup.log": [ "read" ],
|
||||
"/bin/pidof dae": [ "exec" ],
|
||||
"/bin/pidof daed": [ "exec" ],
|
||||
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/bin/dae --version": [ "exec" ],
|
||||
"/bin/rm -f /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "dae", "daed", "daede" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/etc/dae/config.dae": [ "write" ],
|
||||
"/tmp/dae-validate.dae": [ "write" ],
|
||||
"/tmp/daede-import.b64": [ "write" ],
|
||||
"/tmp/daede-sub.txt": [ "write" ],
|
||||
"/etc/daed/.daede-sub-stage-*": [ "write" ],
|
||||
"/etc/daed/daede-sub-*": [ "write" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh import": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/snapshot-file.sh publish *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/snapshot-file.sh replace * *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/snapshot-file.sh discard *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh write-sub *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh delete-sub *": [ "exec" ],
|
||||
"/etc/config/daede": [ "write" ],
|
||||
"/var/log/dae/dae.log": [ "write" ],
|
||||
"/var/log/daed/daed.log": [ "write" ],
|
||||
"/etc/init.d/dae start": [ "exec" ],
|
||||
"/etc/init.d/dae stop": [ "exec" ],
|
||||
"/etc/init.d/dae restart": [ "exec" ],
|
||||
"/etc/init.d/dae enable": [ "exec" ],
|
||||
"/etc/init.d/dae disable": [ "exec" ],
|
||||
"/etc/init.d/dae hot_reload": [ "exec" ],
|
||||
"/etc/init.d/daed start": [ "exec" ],
|
||||
"/etc/init.d/daed stop": [ "exec" ],
|
||||
"/etc/init.d/daed restart": [ "exec" ],
|
||||
"/etc/init.d/daed enable": [ "exec" ],
|
||||
"/etc/init.d/daed disable": [ "exec" ],
|
||||
"/sbin/uci set dae.config.enabled=*": [ "exec" ],
|
||||
"/sbin/uci set daed.config.enabled=*": [ "exec" ],
|
||||
"/sbin/uci set daede.config=daede": [ "exec" ],
|
||||
"/sbin/uci set daede.config.active_backend=*": [ "exec" ],
|
||||
"/sbin/uci commit dae": [ "exec" ],
|
||||
"/sbin/uci commit daed": [ "exec" ],
|
||||
"/sbin/uci commit daede": [ "exec" ],
|
||||
"/sbin/ip netns del daens": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-geo.sh geoip": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-geo.sh geosite": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/geo-cron.sh enable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/geo-cron.sh disable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-pkg.sh dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-pkg.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/update-pkg.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/fetch-clash-yaml.sh *": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh generate": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-update.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh enable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh reset": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh import /tmp/daede-import.*.b64": [ "exec" ],
|
||||
"/tmp/daede-import.*.b64": [ "write" ]
|
||||
},
|
||||
"uci": [ "dae", "daed", "daede" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
# daede-graphql - same-origin GraphQL relay to local daed.
|
||||
#
|
||||
# HTTPS LuCI pages can't fetch daed's plain-HTTP :2023 (mixed content),
|
||||
# so the converter posts here and we relay to 127.0.0.1 over loopback.
|
||||
# daed still checks its own bearer token; no extra exposure. See #11.
|
||||
|
||||
reply() { printf 'Status: %s\r\nContent-Type: application/json\r\n\r\n%s\n' "$1" "$2"; exit 0; }
|
||||
fail() { reply '502 Bad Gateway' '{"errors":[{"message":"daed relay failed"}]}'; }
|
||||
|
||||
[ "$REQUEST_METHOD" = "POST" ] || reply '405 Method Not Allowed' '{"errors":[{"message":"POST only"}]}'
|
||||
|
||||
# daed listen port from uci, default 2023
|
||||
port=$(uci -q get daed.config.listen_addr | sed -n 's/.*:\([0-9]\{1,5\}\)$/\1/p')
|
||||
[ -n "$port" ] || port=2023
|
||||
|
||||
len=${CONTENT_LENGTH:-0}
|
||||
[ "$len" -gt 0 ] 2>/dev/null || fail
|
||||
|
||||
body=$(mktemp /tmp/daede-gql.XXXXXX) || fail
|
||||
head -c "$len" > "$body"
|
||||
|
||||
if [ -n "$HTTP_AUTHORIZATION" ]; then
|
||||
resp=$(uclient-fetch -q -O - \
|
||||
--header="Content-Type: ${CONTENT_TYPE:-application/json}" \
|
||||
--header="Authorization: $HTTP_AUTHORIZATION" \
|
||||
--post-file="$body" \
|
||||
"http://127.0.0.1:$port/graphql" 2>/dev/null)
|
||||
else
|
||||
resp=$(uclient-fetch -q -O - \
|
||||
--header="Content-Type: ${CONTENT_TYPE:-application/json}" \
|
||||
--post-file="$body" \
|
||||
"http://127.0.0.1:$port/graphql" 2>/dev/null)
|
||||
fi
|
||||
rc=$?
|
||||
rm -f "$body"
|
||||
|
||||
[ "$rc" = 0 ] || fail
|
||||
reply '200 OK' "$resp"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# daede-sub - serve a converter's staged node list to the local daed only.
|
||||
#
|
||||
# daed (dae-wing) can only build a subscription by HTTP-fetching a link, and it
|
||||
# does not read file:// like dae core. So the converter stores the base64 share
|
||||
# links in /etc/daed/daede-sub-<token> and points daed's importSubscription at
|
||||
# http://127.0.0.1/cgi-bin/daede-sub?t=<token>. This CGI is the read side.
|
||||
#
|
||||
# Locked to loopback (REMOTE_ADDR), so even though uhttpd listens on the LAN no
|
||||
# remote client can pull the links. The snapshot remains available for daed's
|
||||
# own refresh action and is included in daede configuration backups.
|
||||
|
||||
deny() { printf 'Status: 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nforbidden\n'; exit 0; }
|
||||
|
||||
[ "$REMOTE_ADDR" = "127.0.0.1" ] || deny
|
||||
|
||||
# token: only [A-Za-z0-9] so the path can't escape /tmp
|
||||
t=$(printf '%s' "$QUERY_STRING" | sed -n 's/^.*\bt=\([A-Za-z0-9]\{1,64\}\).*$/\1/p')
|
||||
[ -n "$t" ] || deny
|
||||
|
||||
f="/etc/daed/daede-sub-$t"
|
||||
[ -f "$f" ] || deny
|
||||
|
||||
printf 'Content-Type: text/plain\r\n\r\n'
|
||||
cat "$f"
|
||||
Reference in new issue
Block a user