🔥 Sync 2026-06-19 21:14:10

This commit is contained in:
github-actions[bot] 2026-06-19 21:14:10 +08:00
parent ce48633b46
commit d92edece61
17 changed files with 533 additions and 111 deletions

View File

@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=gecoosac
PKG_VERSION:=2.2.20251015
PKG_RELEASE:=9
PKG_RELEASE:=10
ifeq ($(ARCH),aarch64)
PKG_ARCH:=ac_linux_arm64

View File

@ -53,11 +53,41 @@ strip_trailing_slashes() {
printf '%s\n' "$path"
}
normalize_path() {
local path="$1"
local old_ifs part normalized parent
is_abs_path "$path" || return 1
normalized="/"
old_ifs="$IFS"
IFS=/
set -- $path
IFS="$old_ifs"
for part in "$@"; do
case "$part" in
""|.) ;;
..)
if [ "$normalized" != "/" ]; then
parent="${normalized%/*}"
[ -n "$parent" ] || parent="/"
normalized="$parent"
fi
;;
*) normalized="${normalized%/}/$part" ;;
esac
done
printf '%s\n' "$normalized"
}
is_safe_upload_dir() {
local path
is_abs_path "$1" || return 1
path="$(strip_trailing_slashes "$1")"
path="$(normalize_path "$1")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
case "$path" in
*/gecoosac/upload) return 0 ;;
@ -66,6 +96,47 @@ is_safe_upload_dir() {
return 1
}
is_path_in_dir() {
local path root
path="$(normalize_path "$1")" || return 1
root="$(normalize_path "$2")" || return 1
[ "$root" != "/" ] || return 1
[ "$path" = "$root" ] && return 0
[ "${path#"$root"/}" != "$path" ]
}
is_safe_db_dir() {
local path upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
*) return 1 ;;
esac
is_path_in_dir "$path" "$upload_root" && return 1
return 0
}
is_safe_pid_dir() {
local path upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
case "$path" in
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
*) return 1 ;;
esac
is_path_in_dir "$path" "$upload_root" && return 1
return 0
}
is_port() {
case "$1" in
""|*[!0-9]*) return 1 ;;
@ -139,6 +210,18 @@ cert_matches_san() {
return 0
}
cert_matches_key() {
local cert_file="$1"
local key_file="$2"
local cert_pub key_pub
[ -s "$cert_file" ] && [ -s "$key_file" ] || return 1
cert_pub="$(openssl x509 -in "$cert_file" -noout -pubkey 2>/dev/null)" || return 1
key_pub="$(openssl pkey -in "$key_file" -pubout 2>/dev/null)" || return 1
[ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]
}
generate_default_cert() {
local cert_host cert_ip cert_cn cert_san tmp_crt tmp_key
@ -155,7 +238,7 @@ generate_default_cert() {
cert_ip="$(uci -q get network.lan.ipaddr)"
is_ipv4 "$cert_ip" || cert_ip=""
if [ -s "$DEFAULT_KEY_FILE" ] && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip"; then
if [ -s "$DEFAULT_KEY_FILE" ] && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
return 0
fi
@ -184,7 +267,7 @@ generate_default_cert() {
-addext "extendedKeyUsage=serverAuth" \
-addext "subjectAltName=$cert_san"
if [ -s "$tmp_crt" ] && [ -s "$tmp_key" ] && cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip"; then
if [ -s "$tmp_crt" ] && [ -s "$tmp_key" ] && cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip" && cert_matches_key "$tmp_crt" "$tmp_key"; then
mv "$tmp_key" "$DEFAULT_KEY_FILE"
mv "$tmp_crt" "$DEFAULT_CRT_FILE"
chmod 600 "$DEFAULT_KEY_FILE"
@ -199,14 +282,22 @@ generate_default_cert() {
normalize_conf() {
if is_safe_upload_dir "$upload_dir"; then
upload_dir="$(strip_trailing_slashes "$upload_dir")/"
upload_dir="$(normalize_path "$upload_dir")/"
else
upload_dir="$DEFAULT_UPLOAD_DIR"
fi
is_abs_path "$db_dir" || db_dir="$DEFAULT_DB_DIR"
if is_safe_db_dir "$db_dir" "$upload_dir"; then
db_dir="$(normalize_path "$db_dir")/"
else
db_dir="$DEFAULT_DB_DIR"
fi
is_abs_path "$crt_file" || crt_file="$DEFAULT_CRT_FILE"
is_abs_path "$key_file" || key_file="$DEFAULT_KEY_FILE"
is_abs_path "$piddir" && [ "$piddir" != "/" ] || piddir="$DEFAULT_PID_DIR"
if is_safe_pid_dir "$piddir" "$upload_dir"; then
piddir="$(normalize_path "$piddir")/"
else
piddir="$DEFAULT_PID_DIR"
fi
is_port "$port" || port="60650"
is_port "$m_port" || m_port="8080"
@ -223,7 +314,10 @@ normalize_conf() {
}
ensure_dirs() {
mkdir -p "$upload_dir" "$db_dir" "$piddir" /etc/gecoosac/tls
if ! mkdir -p "$upload_dir" "$db_dir" "$piddir" /etc/gecoosac/tls; then
logger -t gecoosac "failed to create runtime directories"
return 1
fi
}
start_service() {
@ -236,7 +330,12 @@ start_service() {
return 1
fi
ensure_dirs
if [ "$isonlyoneprot" = "0" ] && [ "$port" = "$m_port" ]; then
logger -t gecoosac "interface port and management port must be different"
return 1
fi
ensure_dirs || return 1
if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
@ -247,6 +346,11 @@ start_service() {
logger -t gecoosac "HTTPS is enabled but certificate or key file is missing"
return 1
fi
if ! cert_matches_key "$crt_file" "$key_file"; then
logger -t gecoosac "HTTPS certificate and key do not match"
return 1
fi
fi
procd_open_instance gecoosac

View File

@ -1,9 +1,11 @@
#!/bin/sh
changed=0
DEFAULT_DB_DIR=/etc/gecoosac/
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload/
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
DEFAULT_PID_DIR=/var/run/
OLD_CRT_FILE=/etc/gecoosac/tls/1.crt
OLD_KEY_FILE=/etc/gecoosac/tls/1.key
@ -32,6 +34,34 @@ strip_trailing_slashes() {
printf '%s\n' "$path"
}
normalize_path() {
local path="$1"
local old_ifs part normalized parent
is_abs_path "$path" || return 1
normalized="/"
old_ifs="$IFS"
IFS=/
set -- $path
IFS="$old_ifs"
for part in "$@"; do
case "$part" in
""|.) ;;
..)
if [ "$normalized" != "/" ]; then
parent="${normalized%/*}"
[ -n "$parent" ] || parent="/"
normalized="$parent"
fi
;;
*) normalized="${normalized%/}/$part" ;;
esac
done
printf '%s\n' "$normalized"
}
is_abs_path() {
case "$1" in
/*) return 0 ;;
@ -42,8 +72,10 @@ is_abs_path() {
is_safe_upload_dir() {
local path
is_abs_path "$1" || return 1
path="$(strip_trailing_slashes "$1")"
path="$(normalize_path "$1")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
case "$path" in
*/gecoosac/upload) return 0 ;;
@ -52,6 +84,47 @@ is_safe_upload_dir() {
return 1
}
is_path_in_dir() {
local path root
path="$(normalize_path "$1")" || return 1
root="$(normalize_path "$2")" || return 1
[ "$root" != "/" ] || return 1
[ "$path" = "$root" ] && return 0
[ "${path#"$root"/}" != "$path" ]
}
is_safe_db_dir() {
local path upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
*) return 1 ;;
esac
is_path_in_dir "$path" "$upload_root" && return 1
return 0
}
is_safe_pid_dir() {
local path upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
case "$path" in
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
*) return 1 ;;
esac
is_path_in_dir "$path" "$upload_root" && return 1
return 0
}
normalize_upload_dir() {
local upload_dir normalized
@ -59,7 +132,7 @@ normalize_upload_dir() {
[ -n "$upload_dir" ] || return 0
if is_safe_upload_dir "$upload_dir"; then
normalized="$(strip_trailing_slashes "$upload_dir")/"
normalized="$(normalize_path "$upload_dir")/"
else
normalized="$DEFAULT_UPLOAD_DIR"
fi
@ -69,6 +142,28 @@ normalize_upload_dir() {
changed=1
}
normalize_dir_option() {
local option="$1"
local default="$2"
local validator="$3"
local value normalized upload_dir
value="$(uci -q get "gecoosac.config.${option}")"
[ -n "$value" ] || return 0
upload_dir="$(uci -q get gecoosac.config.upload_dir)"
[ -n "$upload_dir" ] || upload_dir="$DEFAULT_UPLOAD_DIR"
if "$validator" "$value" "$upload_dir"; then
normalized="$(normalize_path "$value")/"
else
normalized="$default"
fi
[ "$value" = "$normalized" ] && return 0
uci -q set "gecoosac.config.${option}=${normalized}"
changed=1
}
migrate_legacy_cert_paths() {
local crt_file key_file
local migrate
@ -95,6 +190,8 @@ migrate_legacy_cert_paths() {
ensure_section
migrate_legacy_cert_paths
normalize_upload_dir
normalize_dir_option db_dir "$DEFAULT_DB_DIR" is_safe_db_dir
normalize_dir_option piddir "$DEFAULT_PID_DIR" is_safe_pid_dir
set_default enabled 0
set_default port 60650
set_default isonlyoneprot 1
@ -103,8 +200,8 @@ set_default https 0
set_default crt_file "$DEFAULT_CRT_FILE"
set_default key_file "$DEFAULT_KEY_FILE"
set_default upload_dir "$DEFAULT_UPLOAD_DIR"
set_default db_dir /etc/gecoosac/
set_default piddir /var/run/
set_default db_dir "$DEFAULT_DB_DIR"
set_default piddir "$DEFAULT_PID_DIR"
set_default lang zh
set_default debug 0
# Preserve the previous package behavior for upgraded configs that never had showtip.

View File

@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.13.0
PKG_RELEASE:=17
PKG_VERSION:=1.14.0
PKG_RELEASE:=18
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@ -77,10 +77,9 @@ define Package/$(PKG_NAME)/install
$(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/refresh-index.sh $(1)/usr/share/luci-app-daede/refresh-index.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/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_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

View File

@ -60,7 +60,7 @@ const CSS = [
'.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}',
/* 手风琴折叠组 —— 与 config.js dd-adv 同构 */
/* 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}',
@ -131,6 +131,8 @@ function stamp() {
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() {}),
@ -212,14 +214,29 @@ return view.extend({
logPane.classList.add('show');
return;
}
const bin = atob(res.stdout.trim());
let bin;
try {
bin = atob(res.stdout.trim());
} catch (e) {
logPane.textContent = _('Export failed: invalid base64 data from server');
logPane.classList.add('show');
return;
}
if (!bin || bin.length === 0) {
logPane.textContent = _('Export failed: empty archive');
logPane.classList.add('show');
return;
}
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
const url = URL.createObjectURL(new Blob([arr], { 'type': 'application/gzip' }));
const a = E('a', { 'href': url, 'download': 'daede-config-' + stamp() + '.tar.gz' });
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(url); }, 1000);
}).catch(function() {}).finally(function() {
}).catch(function(e) {
logPane.textContent = _('Export failed') + ': ' + (e ? (e.message || String(e)) : _('script not found'));
logPane.classList.add('show');
}).finally(function() {
exportBtn.disabled = false; exportBtn.textContent = orig;
});
});
@ -236,7 +253,10 @@ return view.extend({
const b64 = String(e.target.result).split(',')[1] || '';
fs.write('/tmp/daede-import.b64', b64).then(function() {
return runJob('config-backup.sh', 'import', importBtn, '/tmp/luci-app-daede.backup.log');
}).catch(function() {}).finally(function() { fileInput.value = ''; });
}).catch(function(e) {
logPane.textContent = _('Import failed') + ': ' + (e ? (e.message || String(e)) : _('unknown error'));
logPane.classList.add('show');
}).finally(function() { fileInput.value = ''; });
};
reader.readAsDataURL(file);
});
@ -377,21 +397,6 @@ return view.extend({
poll.add(refresh);
refresh();
// Start the potentially slow index refresh without holding a LuCI RPC
// request open, then re-probe promptly when its status changes to done.
const waitIndexRefresh = function(tries) {
return L.resolveDefault(fs.read_direct('/tmp/luci-app-daede.idx.status', 'text'), '').then(function(status) {
if (String(status).trim() === 'done')
return refresh();
if (tries <= 0)
return;
return new Promise(function(resolve) { setTimeout(resolve, 1000); })
.then(function() { return waitIndexRefresh(tries - 1); });
});
};
fs.exec('/usr/share/luci-app-daede/refresh-index.sh', [])
.then(function() { return waitIndexRefresh(30); })
.catch(function() {});
// === Geo data source (preset / custom URL + auto-update) ===
const geoSettings = (function() {

View File

@ -1,28 +1,21 @@
#!/bin/sh
# Refresh the apk/opkg index for the Updates view without holding the LuCI RPC
# request open. The view watches STATUS and re-probes as soon as it reads done.
# Background apk/opkg update for the Updates view. Returns at once; throttled.
LOCK=/tmp/luci-app-daede.idx.lock
STATUS=/tmp/luci-app-daede.idx.status
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
if [ "$(( $(date +%s) - mtime ))" -lt 90 ]; then
[ -f "$STATUS" ] || printf '%s\n' done > "$STATUS"
exit 0
fi
[ "$(( $(date +%s) - mtime ))" -lt 90 ] && exit 0
fi
: > "$LOCK"
printf '%s\n' running > "$STATUS"
# -n: skip if a package op holds the shared apk lock (no need to refresh then)
(
# -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
printf '%s\n' done > "$STATUS"
) >/dev/null 2>&1 </dev/null &
exit 0

View File

@ -20,7 +20,6 @@
"/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" ],
"/tmp/luci-app-daede.idx.status": [ "read" ],
"/bin/pidof dae": [ "exec" ],
"/bin/pidof daed": [ "exec" ],
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],

View File

@ -6,11 +6,11 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-gecoosac
PKG_VERSION:=2.3
PKG_RELEASE:=10
PKG_VERSION:=2.2
PKG_RELEASE:=11
LUCI_TITLE:=LuCI Support for gecoosac
LUCI_DEPENDS:=+gecoosac
LUCI_DEPENDS:=+luci-base +gecoosac
LUCI_PKGARCH:=all
PKG_LICENSE:=AGPL-3.0-only

View File

@ -11,11 +11,15 @@ const DEFAULT_DB_DIR = '/etc/gecoosac/';
const DEFAULT_CRT_FILE = '/etc/gecoosac/tls/gecoosac.crt';
const DEFAULT_KEY_FILE = '/etc/gecoosac/tls/gecoosac.key';
const DEFAULT_PID_DIR = '/var/run/';
const CONFIG_BACKUP_DIR = '/etc/gecoosac';
const DB_DIR_PREFIXES = [ '/etc/gecoosac', '/tmp/gecoosac', '/var/lib/gecoosac' ];
const PID_DIR_PREFIXES = [ '/var/run', '/tmp/gecoosac' ];
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name' ],
let statusPollRegistered = false;
const callServiceStatus = rpc.declare({
object: 'luci.gecoosac',
method: 'status',
expect: { '': {} }
});
@ -39,16 +43,74 @@ function stripTrailingSlashes(value) {
return path;
}
function validUploadDir(value) {
const path = stripTrailingSlashes(value);
function normalizePath(value) {
const path = String(value || '');
return path.charAt(0) === '/' && path.endsWith('/gecoosac/upload');
if (path.charAt(0) !== '/')
return null;
const parts = [];
const segments = path.split('/');
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (!segment || segment === '.')
continue;
if (segment === '..') {
if (parts.length > 0)
parts.pop();
continue;
}
parts.push(segment);
}
return '/' + parts.join('/');
}
function validUploadDir(value) {
const path = normalizePath(value);
return path !== null && path.endsWith('/gecoosac/upload') && !pathInDir(path, CONFIG_BACKUP_DIR);
}
function validPathPrefix(value, prefixes) {
const path = normalizePath(value);
if (path === null)
return false;
for (let i = 0; i < prefixes.length; i++)
if (path === prefixes[i] || path.indexOf(prefixes[i] + '/') === 0)
return true;
return false;
}
function pathInDir(value, dir) {
const path = normalizePath(value);
const root = normalizePath(dir);
return path !== null && root !== null && root !== '/' && (path === root || path.indexOf(root + '/') === 0);
}
function validDbDir(value, uploadDir) {
return validPathPrefix(value, DB_DIR_PREFIXES) && !pathInDir(value, uploadDir || DEFAULT_UPLOAD_DIR);
}
function validPidDir(value, uploadDir) {
return validPathPrefix(value, PID_DIR_PREFIXES) && !pathInDir(value, uploadDir || DEFAULT_UPLOAD_DIR);
}
function serviceRunning(status) {
const service = status && status.gecoosac;
const instances = service && service.instances;
if (status && status.running === true)
return true;
if (!instances)
return false;
@ -121,23 +183,26 @@ return view.extend({
load() {
return Promise.all([
uci.load('gecoosac'),
L.resolveDefault(callServiceList('gecoosac'), {})
L.resolveDefault(callServiceStatus(), {})
]);
},
render(data) {
let m, s, o;
let m, s, o, uploadDirOption;
m = new form.Map('gecoosac', _('Gecoos AC'),
_('Batch management Gecoos AP, Default password: admin') + '<br />' +
_('The current AC version %s, only supports AP 7.6 and above.').format('2.2'));
_('Supports Gecoos AP firmware 7.6 and above.') + '<br />' +
_('The initial password is admin; change it immediately after first login.'));
s = m.section(form.TypedSection, 'gecoosac');
s.anonymous = true;
s.render = function() {
poll.add(function() {
return L.resolveDefault(callServiceList('gecoosac'), {}).then(updateStatus);
}, 3);
if (!statusPollRegistered) {
poll.add(function() {
return L.resolveDefault(callServiceStatus(), {}).then(updateStatus);
}, 3);
statusPollRegistered = true;
}
return E('fieldset', { 'class': 'cbi-section' }, [
E('style', {}, [
@ -174,7 +239,7 @@ return view.extend({
o.depends('isonlyoneprot', '0');
o = s.option(form.Flag, 'https', _('Enable HTTPS service'),
_('Default certificate files are generated when HTTPS starts; custom paths must point to readable files.'));
_('Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key.'));
o.default = '0';
o.depends('isonlyoneprot', '0');
@ -191,7 +256,8 @@ return view.extend({
o.depends({ isonlyoneprot: '0', https: '1' });
o = s.option(form.Value, 'upload_dir', _('Upload dir path'),
_('Upload AP upgrade firmware here. For safe cleanup, use an absolute path ending with /gecoosac/upload, for example /tmp/gecoosac/upload/.'));
_('Upload AP upgrade firmware here. Use an absolute path ending with /gecoosac/upload, for example /tmp/gecoosac/upload/. Do not place it under /etc/gecoosac because that directory is backed up during sysupgrade.'));
uploadDirOption = o;
o.placeholder = DEFAULT_UPLOAD_DIR;
o.default = DEFAULT_UPLOAD_DIR;
o.datatype = 'directory';
@ -199,20 +265,42 @@ return view.extend({
o.validate = function(section_id, value) {
return validUploadDir(value)
? true
: _('Upload directory must be an absolute path ending with /gecoosac/upload.');
: _('Upload directory must be an absolute path ending with /gecoosac/upload and must not be under /etc/gecoosac.');
};
o = s.option(form.Value, 'db_dir', _('Database dir path'), _('The path to store the config database'));
o = s.option(form.Value, 'db_dir', _('Database dir path'),
_('Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory.'));
o.placeholder = DEFAULT_DB_DIR;
o.default = DEFAULT_DB_DIR;
o.datatype = 'directory';
o.rmempty = false;
o.validate = function(section_id, value) {
const uploadDir = uploadDirOption.formvalue(section_id) || DEFAULT_UPLOAD_DIR;
o = s.option(form.Value, 'piddir', _('PID dir path'), _('The path to store the AC program pid file'));
if (!validPathPrefix(value, DB_DIR_PREFIXES))
return _('Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac.');
return validDbDir(value, uploadDir)
? true
: _('Database directory must not be the upload directory or inside it.');
};
o = s.option(form.Value, 'piddir', _('PID dir path'),
_('Store the AC program pid file under /var/run or /tmp/gecoosac. Do not place it inside the upload directory.'));
o.placeholder = DEFAULT_PID_DIR;
o.default = DEFAULT_PID_DIR;
o.datatype = 'directory';
o.rmempty = false;
o.validate = function(section_id, value) {
const uploadDir = uploadDirOption.formvalue(section_id) || DEFAULT_UPLOAD_DIR;
if (!validPathPrefix(value, PID_DIR_PREFIXES))
return _('PID directory must be under /var/run or /tmp/gecoosac.');
return validPidDir(value, uploadDir)
? true
: _('PID directory must not be the upload directory or inside it.');
};
o = s.option(form.ListValue, 'lang', _('Language'));
o.value('zh', _('Chinese'));
@ -234,16 +322,16 @@ return view.extend({
o.rmempty = false;
o = s.option(form.Button, '_clear_upload', _('Clear Upload Directory'),
_('Only files under the configured Gecoos upload directory will be removed.'));
_('Only files under the saved Gecoos upload directory will be removed. Save and Apply before clearing a newly edited path.'));
o.inputstyle = 'remove';
o.inputtitle = _('Clear');
o.onclick = function() {
if (!confirm(_('Really clear the configured upload directory?')))
if (!confirm(_('Really clear the saved upload directory?')))
return Promise.resolve();
return callClearUpload().then(function(res) {
if (res && res.result === true)
ui.addNotification(null, E('p', {}, _('Upload directory cleared')));
ui.addNotification(null, E('p', {}, _('Saved upload directory cleared')));
else
ui.addNotification(null, E('p', {}, clearUploadError(res)), 'danger');
});

View File

@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-gecoosac\n"
"PO-Revision-Date: 2026-06-12 00:00+0800\n"
"PO-Revision-Date: 2026-06-19 00:00+0800\n"
"Last-Translator: Automatically generated\n"
"Language-Team: Chinese\n"
"Language: zh_CN\n"
@ -9,17 +9,17 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Default certificate files are generated when HTTPS starts; custom paths must point to readable files."
msgstr "默认证书文件会在 HTTPS 启动时生成;自定义路径必须指向可读取的文件。"
msgid "Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key."
msgstr "默认证书文件会在 HTTPS 启动时生成;自定义路径必须指向可读取且相互匹配的证书和私钥。"
msgid "Gecoos AC"
msgstr "集客AC控制器"
msgid "Batch management Gecoos AP, Default password: admin"
msgstr "批量集中管理集客 AP默认密码admin"
msgid "Supports Gecoos AP firmware 7.6 and above."
msgstr "支持集客 AP 7.6 及以上版本固件。"
msgid "The current AC version %s, only supports AP 7.6 and above."
msgstr "当前 AC 版本 %s ,仅支持 AP 7.6 及以上版本。"
msgid "The initial password is admin; change it immediately after first login."
msgstr "初始密码为 admin首次登录后请立即修改。"
msgid "Global Settings"
msgstr "全局设置"
@ -51,23 +51,35 @@ msgstr "指定 key 证书文件"
msgid "Upload dir path"
msgstr "上传目录"
msgid "Upload AP upgrade firmware here. For safe cleanup, use an absolute path ending with /gecoosac/upload, for example /tmp/gecoosac/upload/."
msgstr "AP 升级固件会上传到此目录。为确保安全清理,请使用以 /gecoosac/upload 结尾的绝对路径,例如 /tmp/gecoosac/upload/。"
msgid "Upload AP upgrade firmware here. Use an absolute path ending with /gecoosac/upload, for example /tmp/gecoosac/upload/. Do not place it under /etc/gecoosac because that directory is backed up during sysupgrade."
msgstr "AP 升级固件会上传到此目录。请使用以 /gecoosac/upload 结尾的绝对路径,例如 /tmp/gecoosac/upload/。不要放在 /etc/gecoosac 下,因为该目录会在 sysupgrade 时被备份。"
msgid "Upload directory must be an absolute path ending with /gecoosac/upload."
msgstr "上传目录必须是以 /gecoosac/upload 结尾的绝对路径。"
msgid "Upload directory must be an absolute path ending with /gecoosac/upload and must not be under /etc/gecoosac."
msgstr "上传目录必须是以 /gecoosac/upload 结尾的绝对路径,且不能位于 /etc/gecoosac 下。"
msgid "Database dir path"
msgstr "数据库目录"
msgid "The path to store the config database"
msgstr "存放配置数据库的路径"
msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory."
msgstr "将配置数据库存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。不要放在上传目录内。"
msgid "Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac."
msgstr "数据库目录必须位于 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。"
msgid "Database directory must not be the upload directory or inside it."
msgstr "数据库目录不能是上传目录,也不能位于上传目录内。"
msgid "PID dir path"
msgstr "PID 目录"
msgid "The path to store the AC program pid file"
msgstr "存放 AC 程序 PID 文件的路径"
msgid "Store the AC program pid file under /var/run or /tmp/gecoosac. Do not place it inside the upload directory."
msgstr "将 AC 程序 PID 文件存放在 /var/run 或 /tmp/gecoosac 下。不要放在上传目录内。"
msgid "PID directory must be under /var/run or /tmp/gecoosac."
msgstr "PID 目录必须位于 /var/run 或 /tmp/gecoosac 下。"
msgid "PID directory must not be the upload directory or inside it."
msgstr "PID 目录不能是上传目录,也不能位于上传目录内。"
msgid "Language"
msgstr "语言"
@ -105,11 +117,11 @@ msgstr "清理上传目录"
msgid "Clear"
msgstr "清理"
msgid "Only files under the configured Gecoos upload directory will be removed."
msgstr "只会删除已配置的集客 AC 上传目录中的文件。"
msgid "Only files under the saved Gecoos upload directory will be removed. Save and Apply before clearing a newly edited path."
msgstr "只会删除已保存的集客 AC 上传目录中的文件。若刚修改路径,请先保存并应用后再清理。"
msgid "Really clear the configured upload directory?"
msgstr "确定要清理已配置的上传目录吗?"
msgid "Really clear the saved upload directory?"
msgstr "确定要清理已保存的上传目录吗?"
msgid "Expecting an absolute path"
msgstr "请输入绝对路径"
@ -120,8 +132,11 @@ msgstr "只能清理集客 AC 上传目录"
msgid "Upload directory was not cleared"
msgstr "上传目录未清理"
msgid "Upload directory cleared"
msgstr "上传目录已清理"
msgid "Saved upload directory cleared"
msgstr "已保存的上传目录已清理"
msgid "Unable to remove upload directory contents"
msgstr "无法删除上传目录内容"
msgid "Upload directory contains configured runtime paths"
msgstr "上传目录包含已配置的运行目录"

View File

@ -27,10 +27,45 @@ strip_trailing_slashes() {
printf '%s\n' "$path"
}
normalize_path() {
local path="$1"
local old_ifs part normalized parent
case "$path" in
/*) ;;
*) return 1 ;;
esac
normalized="/"
old_ifs="$IFS"
IFS=/
set -- $path
IFS="$old_ifs"
for part in "$@"; do
case "$part" in
""|.) ;;
..)
if [ "$normalized" != "/" ]; then
parent="${normalized%/*}"
[ -n "$parent" ] || parent="/"
normalized="$parent"
fi
;;
*) normalized="${normalized%/}/$part" ;;
esac
done
printf '%s\n' "$normalized"
}
safe_upload_path() {
local path
path="$(strip_trailing_slashes "$1")"
path="$(normalize_path "$1")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
[ "$path" = "/tmp/gecoosac/upload" ] && return 0
case "$path" in
@ -40,6 +75,60 @@ safe_upload_path() {
return 1
}
path_in_dir() {
local path root
path="$(normalize_path "$1")" || return 1
root="$(normalize_path "$2")" || return 1
[ "$root" != "/" ] || return 1
[ "$path" = "$root" ] && return 0
[ "${path#"$root"/}" != "$path" ]
}
configured_path_in_upload() {
local option="$1"
local upload_path="$2"
local path real_path
path="$(uci -q get "gecoosac.config.${option}")"
[ -n "$path" ] || return 1
case "$path" in
/*) ;;
*) return 1 ;;
esac
real_path="$(readlink -f "$path" 2>/dev/null)"
[ -n "$real_path" ] || real_path="$(normalize_path "$path")" || return 1
path_in_dir "$real_path" "$upload_path"
}
service_status() {
local data instances instance running state
running=0
data="$(ubus call service list '{"name":"gecoosac"}' 2>/dev/null)"
if json_load "$data" 2>/dev/null && json_select gecoosac 2>/dev/null && json_select instances 2>/dev/null; then
json_get_keys instances
for instance in $instances; do
json_select "$instance" 2>/dev/null || continue
json_get_var state running
json_select ..
if [ "$state" = "1" ]; then
running=1
break
fi
done
fi
json_init
json_add_boolean running "$running"
json_dump
json_cleanup
}
clear_upload() {
local path real entry
@ -51,16 +140,21 @@ clear_upload() {
*) json_result 0 "Expecting an absolute path"; return ;;
esac
path="$(strip_trailing_slashes "$path")"
path="$(normalize_path "$path")"
real="$(readlink -f "$path" 2>/dev/null)"
[ -n "$real" ] || real="$path"
real="$(strip_trailing_slashes "$real")"
real="$(normalize_path "$real")" || { json_result 0 "Expecting an absolute path"; return; }
if ! safe_upload_path "$real"; then
json_result 0 "Only Gecoos upload directories can be cleared" "$real"
return
fi
if configured_path_in_upload db_dir "$real" || configured_path_in_upload piddir "$real"; then
json_result 0 "Upload directory contains configured runtime paths" "$real"
return
fi
[ -d "$real" ] || { json_result 1 "" "$real"; return; }
for entry in "$real"/* "$real"/.[!.]* "$real"/..?*; do
@ -77,10 +171,11 @@ clear_upload() {
case "$1" in
list)
printf '{ "clear_upload": {} }'
printf '{ "clear_upload": {}, "status": {} }'
;;
call)
case "$2" in
status) service_status ;;
clear_upload) clear_upload ;;
*) json_result 0 "Unknown method" ;;
esac

View File

@ -3,7 +3,7 @@
"description": "Grant access for luci-app-gecoosac",
"read": {
"ubus": {
"service": [ "list" ]
"luci.gecoosac": [ "status" ]
},
"uci": [ "gecoosac" ]
},

View File

@ -0,0 +1,25 @@
## What's Changed
First stable release of Aurora, centered on a navigation overhaul.
### New Features
- **Sidebar Navigation Mode**: Added a configurable sidebar navigation layout. Initial design contributed by @chillykidd.
- **Three-Column Mega-Menu**: Redesigned top navigation dropdown with Tabler icons, an anchor column, and a device summary, with smooth hover animations.
- **Redesigned Mobile Navigation**: Reworked to align with the desktop navigation styling for a more consistent look.
### Improvements
- GPU-friendly animations with explicit transition properties.
- Compressed the logo for faster loading, and updated the Shortcuts icon.
### Bug Fixes
- Fixed dropdown and mega-menu stacking order.
- Fixed `.hidden` not always hiding elements regardless of specificity.
- Fixed the notification close button not dismissing alert messages.
- Applied button shape styles to `input[type=button/submit/reset]`.
### Other Changes
- For remaining changes, see the [full changelog](https://github.com/eamonxg/luci-theme-aurora/compare/v0.12.0...v1.0.0)

View File

@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.12.15
PKG_RELEASE:=32
PKG_VERSION:=1.0.0
PKG_RELEASE:=33
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=

View File

@ -24,8 +24,9 @@
- **Modern**: Modern, content-first UI design with a clean layout and elegant animations.
- **Mobile-friendly**: Optimized for mobile interactions and display, supporting both smartphones and tablets.
- **Theme Switcher**: Built-in theme switcher with seamless switching between Auto (system), Light, and Dark modes.
- **Floating Toolbar**: Clickable button icons for quick access to frequently used pages
- **Customizable**: The [luci-app-aurora-config](https://github.com/eamonxg/luci-app-aurora-config) plugin includes multiple builtin theme presets you can switch between, and lets you customize colors, the navigation style, the theme logo, and the floating toolbar (add or edit frequently used pages).
- **Floating Toolbar**: Clickable button icons for quick access to frequently used pages.
- **Installable (PWA)**: Ships a web app manifest and app icons, so LuCI can be installed to your home screen and launched like a native app.
- **Customizable**: The [luci-app-aurora-config](https://github.com/eamonxg/luci-app-aurora-config) plugin includes multiple builtin theme presets you can switch between, and lets you customize Light/Dark color tokens, the navigation layout (Mega Menu, Dropdown, Sidebar), layout density, typography, branding (logo, favicons, login background), and the floating toolbar (add or edit frequently used pages).
## Preview
@ -59,12 +60,12 @@ OpenWrt 25.12+ and snapshots use `apk`; other versions use `opkg`:
- **opkg** (OpenWrt < 25.12):
```sh
cd /tmp && uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_0.12.0-r20260531_all.ipk && opkg install luci-theme-aurora.ipk
cd /tmp && uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_1.0.0-r20260619_all.ipk && opkg install luci-theme-aurora.ipk
```
- **apk** (OpenWrt 25.12+ and snapshots):
```sh
cd /tmp && uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-0.12.0-r20260531.apk && apk add --allow-untrusted luci-theme-aurora.apk
cd /tmp && uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-1.0.0-r20260619.apk && apk add --allow-untrusted luci-theme-aurora.apk
```
## Contributing

View File

@ -25,7 +25,8 @@
- **移动端友好**:针对移动端的交互和显示进行了优化,适配手机和平板设备。
- **主题切换**:内置主题切换器,支持在自动(跟随系统)、浅色和深色模式之间无缝切换。
- **悬浮工具栏**:提供可点击的图标按钮,用于快速访问常用页面。
- **高度可定制**[luci-app-aurora-config](https://github.com/eamonxg/luci-app-aurora-config) 插件内置多套主题预设,可自由切换;同时还支持自定义颜色、导航子菜单样式、主题 Logo以及添加或编辑悬浮工具栏中的常用页面。
- **可安装PWA**:内置 Web 应用清单manifest与应用图标可将 LuCI 安装到主屏幕,像原生应用一样启动。
- **高度可定制**[luci-app-aurora-config](https://github.com/eamonxg/luci-app-aurora-config) 插件内置多套主题预设,可自由切换;同时还支持自定义浅色/深色色彩令牌、导航布局Mega Menu、下拉菜单、侧边栏、布局间距、字体排版、品牌标识Logo、favicon、登录背景以及添加或编辑悬浮工具栏中的常用页面。
## 预览
@ -59,13 +60,13 @@ OpenWrt 25.12+ 和 Snapshot 版本使用 `apk`;其他版本使用 `opkg`
- **opkg** (OpenWrt < 25.12):
```sh
cd /tmp && uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_0.12.0-r20260531_all.ipk && opkg install luci-theme-aurora.ipk
cd /tmp && uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_1.0.0-r20260619_all.ipk && opkg install luci-theme-aurora.ipk
```
- **apk** (OpenWrt 25.12+ 及 snapshots):
```sh
cd /tmp && uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-0.12.0-r20260531.apk && apk add --allow-untrusted luci-theme-aurora.apk
cd /tmp && uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-1.0.0-r20260619.apk && apk add --allow-untrusted luci-theme-aurora.apk
```
## 加入贡献

View File

@ -8,12 +8,12 @@ PKG_NAME:=alwaysonline
PKG_UPSTREAM_VERSION:=1.2.1
PKG_UPSTREAM_GITHASH:=206292ca68e4d6c81b215163a7bbd0cd2eb36860
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))
PKG_RELEASE:=6
PKG_RELEASE:=7
UCI_VERSION:=0.2025.01.25
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/Jamesits/alwaysonline.git
PKG_SOURCE_VERSION:=eaba4daa34d1a759f1bd6a18e20b00928e09bbe8
PKG_SOURCE_VERSION:=5d66f9b28a2054f4b73fd447d531e10f82af416d
PKG_MIRROR_HASH:=skip
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION)