mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Thu Mar 26 20:20:16 CET 2026
This commit is contained in:
@@ -1310,3 +1310,4 @@ Update On Sun Mar 22 19:50:55 CET 2026
|
||||
Update On Mon Mar 23 20:06:04 CET 2026
|
||||
Update On Tue Mar 24 20:16:12 CET 2026
|
||||
Update On Wed Mar 25 20:07:22 CET 2026
|
||||
Update On Thu Mar 26 20:20:07 CET 2026
|
||||
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
From f4de14a515221e27c0d79446b423849a6546e3a6 Mon Sep 17 00:00:00 2001
|
||||
From: Brad Fitzpatrick <bradfitz@golang.org>
|
||||
Date: Tue, 24 Mar 2026 23:02:09 +0000
|
||||
Subject: [PATCH] runtime: use uname version check for 64-bit time on 32-bit
|
||||
arch codepaths
|
||||
|
||||
The previous fallback-on-ENOSYS logic causes issues on forks of Linux.
|
||||
|
||||
Android: #77621 (CL 750040 added a workaround with a TODO,
|
||||
this fixes that TODO)
|
||||
Causes the OS to terminate the program when running on Android
|
||||
versions <=10 since the seccomp jail does not know about the 64-bit
|
||||
time syscall and is configured to terminate the program on any
|
||||
unknown syscall.
|
||||
|
||||
Synology's Linux: #77930
|
||||
On old versions of Synology's Linux they added custom vendor syscalls
|
||||
without adding a gap in the syscall numbers, that means when we call
|
||||
the newer Linux syscall which was added later, Synology's Linux
|
||||
interprets it as a completely different vendor syscall.
|
||||
|
||||
Originally by Jorropo in CL 751340.
|
||||
|
||||
Fixes golang/go#77930
|
||||
Updates tailscale/go#162
|
||||
Originally https://go-review.googlesource.com/c/go/+/758902/2
|
||||
|
||||
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
|
||||
Change-Id: I90e15495d9249fd7f6e112f9e3ae8ad1322f56e0
|
||||
---
|
||||
.../runtime/syscall/linux/defs_linux_386.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_amd64.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_arm.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_arm64.go | 1 +
|
||||
.../syscall/linux/defs_linux_loong64.go | 1 +
|
||||
.../syscall/linux/defs_linux_mips64x.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_mipsx.go | 1 +
|
||||
.../syscall/linux/defs_linux_ppc64x.go | 1 +
|
||||
.../syscall/linux/defs_linux_riscv64.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_s390x.go | 1 +
|
||||
.../runtime/syscall/linux/syscall_linux.go | 14 +++++
|
||||
src/runtime/os_linux.go | 62 +++++++++++++++++++
|
||||
src/runtime/os_linux32.go | 28 +++------
|
||||
src/runtime/os_linux64.go | 2 +
|
||||
14 files changed, 97 insertions(+), 19 deletions(-)
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_386.go b/src/internal/runtime/syscall/linux/defs_linux_386.go
|
||||
index 7fdf5d3f8062fa..4e8e645dc49a66 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_386.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_386.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 295
|
||||
SYS_PREAD64 = 180
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_amd64.go b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
|
||||
index 2c8676e6e9b4d9..fa764d9ccd9b8e 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_amd64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 257
|
||||
SYS_PREAD64 = 17
|
||||
SYS_READ = 0
|
||||
+ SYS_UNAME = 63
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm.go b/src/internal/runtime/syscall/linux/defs_linux_arm.go
|
||||
index a0b395d6762734..cef556d5f6f986 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_arm.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 322
|
||||
SYS_PREAD64 = 180
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm64.go b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
|
||||
index 223dce0c5b4281..eabddbac1bc063 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_arm64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 56
|
||||
SYS_PREAD64 = 67
|
||||
SYS_READ = 63
|
||||
+ SYS_UNAME = 160
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_loong64.go b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
|
||||
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_loong64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 56
|
||||
SYS_PREAD64 = 67
|
||||
SYS_READ = 63
|
||||
+ SYS_UNAME = 160
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
|
||||
index 84b760dc1b5545..b5794e5002af5e 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
SYS_OPENAT = 5247
|
||||
SYS_PREAD64 = 5016
|
||||
SYS_READ = 5000
|
||||
+ SYS_UNAME = 5061
|
||||
|
||||
EFD_NONBLOCK = 0x80
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
|
||||
index a9be21414c26f9..1fb4d919d1a318 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
SYS_OPENAT = 4288
|
||||
SYS_PREAD64 = 4200
|
||||
SYS_READ = 4003
|
||||
+ SYS_UNAME = 4122
|
||||
|
||||
EFD_NONBLOCK = 0x80
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
|
||||
index 63f4e5d7864de4..ee93ad345b810f 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
SYS_OPENAT = 286
|
||||
SYS_PREAD64 = 179
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
|
||||
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 56
|
||||
SYS_PREAD64 = 67
|
||||
SYS_READ = 63
|
||||
+ SYS_UNAME = 160
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_s390x.go b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
|
||||
index 52945db0e5b72f..da11c704081abc 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_s390x.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 288
|
||||
SYS_PREAD64 = 180
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/syscall_linux.go b/src/internal/runtime/syscall/linux/syscall_linux.go
|
||||
index 8201e7d1907444..b64f511b03c947 100644
|
||||
--- a/src/internal/runtime/syscall/linux/syscall_linux.go
|
||||
+++ b/src/internal/runtime/syscall/linux/syscall_linux.go
|
||||
@@ -86,3 +86,17 @@ func Pread(fd int, p []byte, offset int64) (n int, errno uintptr) {
|
||||
}
|
||||
return int(r1), e
|
||||
}
|
||||
+
|
||||
+type Utsname struct {
|
||||
+ Sysname [65]byte
|
||||
+ Nodename [65]byte
|
||||
+ Release [65]byte
|
||||
+ Version [65]byte
|
||||
+ Machine [65]byte
|
||||
+ Domainname [65]byte
|
||||
+}
|
||||
+
|
||||
+func Uname(buf *Utsname) (errno uintptr) {
|
||||
+ _, _, e := Syscall6(SYS_UNAME, uintptr(unsafe.Pointer(buf)), 0, 0, 0, 0, 0)
|
||||
+ return e
|
||||
+}
|
||||
diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go
|
||||
index 7e6af22d48a764..493567b5303673 100644
|
||||
--- a/src/runtime/os_linux.go
|
||||
+++ b/src/runtime/os_linux.go
|
||||
@@ -354,6 +354,7 @@ func osinit() {
|
||||
numCPUStartup = getCPUCount()
|
||||
physHugePageSize = getHugePageSize()
|
||||
vgetrandomInit()
|
||||
+ configure64bitsTimeOn32BitsArchitectures()
|
||||
}
|
||||
|
||||
var urandom_dev = []byte("/dev/urandom\x00")
|
||||
@@ -935,3 +936,64 @@ func mprotect(addr unsafe.Pointer, n uintptr, prot int32) (ret int32, errno int3
|
||||
r, _, err := linux.Syscall6(linux.SYS_MPROTECT, uintptr(addr), n, uintptr(prot), 0, 0, 0)
|
||||
return int32(r), int32(err)
|
||||
}
|
||||
+
|
||||
+type kernelVersion struct {
|
||||
+ major int
|
||||
+ minor int
|
||||
+}
|
||||
+
|
||||
+// getKernelVersion returns major and minor kernel version numbers
|
||||
+// parsed from the uname release field.
|
||||
+func getKernelVersion() kernelVersion {
|
||||
+ var buf linux.Utsname
|
||||
+ if e := linux.Uname(&buf); e != 0 {
|
||||
+ throw("uname failed")
|
||||
+ }
|
||||
+
|
||||
+ rel := gostringnocopy(&buf.Release[0])
|
||||
+ major, minor, _, ok := parseRelease(rel)
|
||||
+ if !ok {
|
||||
+ throw("failed to parse kernel version from uname")
|
||||
+ }
|
||||
+ return kernelVersion{major: major, minor: minor}
|
||||
+}
|
||||
+
|
||||
+// parseRelease parses a dot-separated version number. It follows the
|
||||
+// semver syntax, but allows the minor and patch versions to be
|
||||
+// elided.
|
||||
+func parseRelease(rel string) (major, minor, patch int, ok bool) {
|
||||
+ // Strip anything after a dash or plus.
|
||||
+ for i := 0; i < len(rel); i++ {
|
||||
+ if rel[i] == '-' || rel[i] == '+' {
|
||||
+ rel = rel[:i]
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ next := func() (int, bool) {
|
||||
+ for i := 0; i < len(rel); i++ {
|
||||
+ if rel[i] == '.' {
|
||||
+ ver, err := strconv.Atoi(rel[:i])
|
||||
+ rel = rel[i+1:]
|
||||
+ return ver, err == nil
|
||||
+ }
|
||||
+ }
|
||||
+ ver, err := strconv.Atoi(rel)
|
||||
+ rel = ""
|
||||
+ return ver, err == nil
|
||||
+ }
|
||||
+ if major, ok = next(); !ok || rel == "" {
|
||||
+ return
|
||||
+ }
|
||||
+ if minor, ok = next(); !ok || rel == "" {
|
||||
+ return
|
||||
+ }
|
||||
+ patch, ok = next()
|
||||
+ return
|
||||
+}
|
||||
+
|
||||
+// GE checks if the running kernel version
|
||||
+// is greater than or equal to the provided version.
|
||||
+func (kv kernelVersion) GE(x, y int) bool {
|
||||
+ return kv.major > x || (kv.major == x && kv.minor >= y)
|
||||
+}
|
||||
diff --git a/src/runtime/os_linux32.go b/src/runtime/os_linux32.go
|
||||
index 16de6fb350f624..02cb18f32d57d2 100644
|
||||
--- a/src/runtime/os_linux32.go
|
||||
+++ b/src/runtime/os_linux32.go
|
||||
@@ -7,27 +7,25 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
- "internal/runtime/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
+func configure64bitsTimeOn32BitsArchitectures() {
|
||||
+ use64bitsTimeOn32bits = getKernelVersion().GE(5, 1)
|
||||
+}
|
||||
+
|
||||
//go:noescape
|
||||
func futex_time32(addr unsafe.Pointer, op int32, val uint32, ts *timespec32, addr2 unsafe.Pointer, val3 uint32) int32
|
||||
|
||||
//go:noescape
|
||||
func futex_time64(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
|
||||
|
||||
-var isFutexTime32bitOnly atomic.Bool
|
||||
+var use64bitsTimeOn32bits bool
|
||||
|
||||
//go:nosplit
|
||||
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 {
|
||||
- if !isFutexTime32bitOnly.Load() {
|
||||
- ret := futex_time64(addr, op, val, ts, addr2, val3)
|
||||
- // futex_time64 is only supported on Linux 5.0+
|
||||
- if ret != -_ENOSYS {
|
||||
- return ret
|
||||
- }
|
||||
- isFutexTime32bitOnly.Store(true)
|
||||
+ if use64bitsTimeOn32bits {
|
||||
+ return futex_time64(addr, op, val, ts, addr2, val3)
|
||||
}
|
||||
// Downgrade ts.
|
||||
var ts32 timespec32
|
||||
@@ -45,17 +43,10 @@ func timer_settime32(timerid int32, flags int32, new, old *itimerspec32) int32
|
||||
//go:noescape
|
||||
func timer_settime64(timerid int32, flags int32, new, old *itimerspec) int32
|
||||
|
||||
-var isSetTime32bitOnly atomic.Bool
|
||||
-
|
||||
//go:nosplit
|
||||
func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
|
||||
- if !isSetTime32bitOnly.Load() {
|
||||
- ret := timer_settime64(timerid, flags, new, old)
|
||||
- // timer_settime64 is only supported on Linux 5.0+
|
||||
- if ret != -_ENOSYS {
|
||||
- return ret
|
||||
- }
|
||||
- isSetTime32bitOnly.Store(true)
|
||||
+ if use64bitsTimeOn32bits {
|
||||
+ return timer_settime64(timerid, flags, new, old)
|
||||
}
|
||||
|
||||
var newts, oldts itimerspec32
|
||||
@@ -73,6 +64,5 @@ func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
|
||||
old32 = &oldts
|
||||
}
|
||||
|
||||
- // Fall back to 32-bit timer
|
||||
return timer_settime32(timerid, flags, new32, old32)
|
||||
}
|
||||
diff --git a/src/runtime/os_linux64.go b/src/runtime/os_linux64.go
|
||||
index 7b70d80fbe5a89..f9571dd7586614 100644
|
||||
--- a/src/runtime/os_linux64.go
|
||||
+++ b/src/runtime/os_linux64.go
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
+func configure64bitsTimeOn32BitsArchitectures() {}
|
||||
+
|
||||
//go:noescape
|
||||
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
|
||||
|
||||
+54
-42
@@ -1,4 +1,8 @@
|
||||
Subject: [PATCH] internal/poll: move rsan to heap on windows
|
||||
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
|
||||
From: database64128 <free122448@hotmail.com>
|
||||
Date: Mon, 9 Mar 2026 16:25:16 +0800
|
||||
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
|
||||
windows
|
||||
|
||||
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
|
||||
the memory pointed to by lpFromlen must remain available during the
|
||||
@@ -14,20 +18,27 @@ with rsa in the same sync.Pool. The new wsaRsa struct is still in the
|
||||
same size class, so no additional overhead is introduced by this
|
||||
change.
|
||||
|
||||
Fixes #77975.
|
||||
Fixes #78041.
|
||||
|
||||
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
|
||||
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
|
||||
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
|
||||
Reviewed-by: Cherry Mui <cherryyz@google.com>
|
||||
Commit-Queue: Cherry Mui <cherryyz@google.com>
|
||||
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
|
||||
Reviewed-by: Damien Neil <dneil@google.com>
|
||||
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
|
||||
Reviewed-by: Mark Freeman <markfreeman@google.com>
|
||||
---
|
||||
Index: src/internal/poll/fd_windows.go
|
||||
IDEA additional info:
|
||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||
<+>UTF-8
|
||||
===================================================================
|
||||
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
--- a/src/internal/poll/fd_windows.go (revision 8149d992682ce76c6af804b507878e19fc966f7b)
|
||||
+++ b/src/internal/poll/fd_windows.go (date 1773058735706)
|
||||
@@ -149,7 +149,7 @@
|
||||
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
|
||||
1 file changed, 59 insertions(+), 35 deletions(-)
|
||||
|
||||
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
index 2ba967f990982f..26319548e3c310 100644
|
||||
--- a/src/internal/poll/fd_windows.go
|
||||
+++ b/src/internal/poll/fd_windows.go
|
||||
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
|
||||
|
||||
// newWSAMsg creates a new WSAMsg with the provided parameters.
|
||||
// Use [freeWSAMsg] to free it.
|
||||
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
|
||||
@@ -35,8 +46,8 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
// The returned object can't be allocated in the stack because it is accessed asynchronously
|
||||
// by Windows in between several system calls. If the stack frame is moved while that happens,
|
||||
// then Windows may access invalid memory.
|
||||
@@ -164,33 +164,45 @@
|
||||
Buf: unsafe.SliceData(oob),
|
||||
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
|
||||
}
|
||||
}
|
||||
msg.Flags = uint32(flags)
|
||||
- if unconnected {
|
||||
@@ -48,7 +59,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
|
||||
func freeWSAMsg(msg *windows.WSAMsg) {
|
||||
// Clear pointers to buffers so they can be released by garbage collector.
|
||||
+ msg.Name = nil
|
||||
@@ -65,7 +76,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
- }
|
||||
wsaMsgPool.Put(msg)
|
||||
}
|
||||
|
||||
|
||||
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
|
||||
+//
|
||||
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
|
||||
@@ -81,20 +92,21 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ return new(wsaRsa)
|
||||
},
|
||||
}
|
||||
+
|
||||
|
||||
+func newWSARsa() *wsaRsa {
|
||||
+ rsa := wsaRsaPool.Get().(*wsaRsa)
|
||||
+ rsa.name = syscall.RawSockaddrAny{}
|
||||
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
|
||||
+ return rsa
|
||||
+}
|
||||
|
||||
+
|
||||
var operationPool = sync.Pool{
|
||||
New: func() any {
|
||||
@@ -737,19 +749,18 @@
|
||||
|
||||
return new(operation)
|
||||
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
|
||||
|
||||
fd.pin('r', &buf[0])
|
||||
|
||||
|
||||
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
|
||||
+ rsa := newWSARsa()
|
||||
defer wsaRsaPool.Put(rsa)
|
||||
@@ -113,11 +125,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ sa, _ := rsa.name.Sockaddr()
|
||||
return n, sa, nil
|
||||
}
|
||||
|
||||
@@ -768,19 +779,18 @@
|
||||
|
||||
|
||||
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
|
||||
|
||||
fd.pin('r', &buf[0])
|
||||
|
||||
|
||||
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
|
||||
+ rsa := newWSARsa()
|
||||
defer wsaRsaPool.Put(rsa)
|
||||
@@ -136,11 +148,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ rawToSockaddrInet4(&rsa.name, sa4)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -799,19 +809,18 @@
|
||||
|
||||
|
||||
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
|
||||
|
||||
fd.pin('r', &buf[0])
|
||||
|
||||
|
||||
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
|
||||
+ rsa := newWSARsa()
|
||||
defer wsaRsaPool.Put(rsa)
|
||||
@@ -159,11 +171,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ rawToSockaddrInet6(&rsa.name, sa6)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -1371,7 +1380,9 @@
|
||||
|
||||
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
|
||||
p = p[:maxRW]
|
||||
}
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, flags, true)
|
||||
+ rsa := newWSARsa()
|
||||
+ defer wsaRsaPool.Put(rsa)
|
||||
@@ -171,10 +183,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
|
||||
@@ -1396,7 +1407,9 @@
|
||||
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
|
||||
p = p[:maxRW]
|
||||
}
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, flags, true)
|
||||
+ rsa := newWSARsa()
|
||||
+ defer wsaRsaPool.Put(rsa)
|
||||
@@ -182,10 +194,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
|
||||
@@ -1420,7 +1433,9 @@
|
||||
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
|
||||
p = p[:maxRW]
|
||||
}
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, flags, true)
|
||||
+ rsa := newWSARsa()
|
||||
+ defer wsaRsaPool.Put(rsa)
|
||||
@@ -193,10 +205,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
|
||||
@@ -1444,15 +1459,18 @@
|
||||
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
|
||||
}
|
||||
defer fd.writeUnlock()
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, 0, sa != nil)
|
||||
- defer freeWSAMsg(msg)
|
||||
+ var rsa *wsaRsa
|
||||
@@ -215,10 +227,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
|
||||
return qty, err
|
||||
@@ -1471,11 +1489,14 @@
|
||||
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
|
||||
}
|
||||
defer fd.writeUnlock()
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, 0, sa != nil)
|
||||
- defer freeWSAMsg(msg)
|
||||
+ var rsa *wsaRsa
|
||||
@@ -233,10 +245,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
|
||||
return qty, err
|
||||
@@ -1494,11 +1515,14 @@
|
||||
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
|
||||
}
|
||||
defer fd.writeUnlock()
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, 0, sa != nil)
|
||||
- defer freeWSAMsg(msg)
|
||||
+ var rsa *wsaRsa
|
||||
@@ -250,4 +262,4 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
|
||||
return qty, err
|
||||
return qty, err
|
||||
+13
-7
@@ -173,14 +173,13 @@ jobs:
|
||||
|
||||
- name: Set up Go1.26 loongarch abi1
|
||||
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
||||
run: |
|
||||
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0/go1.26.0.linux-amd64-abi1.tar.gz
|
||||
mkdir -p $HOME/usr/local
|
||||
tar zxf go1.26.0.linux-amd64-abi1.tar.gz -C $HOME/usr/local
|
||||
echo "$HOME/usr/local/go/bin" >> $GITHUB_PATH
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-download-base-url: 'https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0'
|
||||
go-version: 1.26.0
|
||||
|
||||
# TODO: remove after issue77731 merged, see: https://github.com/golang/go/issues/77731
|
||||
- name: Apply issue77731 for Golang1.26
|
||||
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
|
||||
- name: Fix issue77731 for Golang1.26
|
||||
if: ${{ matrix.jobs.goversion == '' }}
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
@@ -193,6 +192,13 @@ jobs:
|
||||
cd $(go env GOROOT)
|
||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
|
||||
|
||||
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
|
||||
- name: Fix issue77930 for Golang1.26
|
||||
if: ${{ matrix.jobs.goversion == '' }}
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77930.patch
|
||||
|
||||
# this patch file only works on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode"
|
||||
@@ -61,10 +64,29 @@ type inetDiagResponse struct {
|
||||
|
||||
func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) {
|
||||
uid, inode, err := resolveSocketByNetlink(network, ip, srcPort)
|
||||
if runtime.GOOS == "android" {
|
||||
// on Android (especially recent releases), netlink INET_DIAG can fail or return UID 0 / empty process info for some apps
|
||||
// so trying fallback to resolve /proc/net/{tcp,tcp6,udp,udp6}
|
||||
if err != nil {
|
||||
uid, inode, err = resolveSocketByProcFS(network, ip, srcPort)
|
||||
} else if uid == 0 {
|
||||
pUID, pInode, pErr := resolveSocketByProcFS(network, ip, srcPort)
|
||||
if pErr == nil && pUID != 0 {
|
||||
uid, inode, err = pUID, pInode, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
pp, err := resolveProcessNameByProcSearch(inode, uid)
|
||||
if runtime.GOOS == "android" {
|
||||
// if inode-based /proc/<pid>/fd resolution fails but UID is known,
|
||||
// fall back to resolving the process/package name by UID (typical on Android where all app processes share one UID).
|
||||
if err != nil && uid != 0 {
|
||||
pp, err = resolveProcessNameByUID(uid)
|
||||
}
|
||||
}
|
||||
return uid, pp, err
|
||||
}
|
||||
|
||||
@@ -180,6 +202,47 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
||||
}
|
||||
|
||||
// resolveProcessNameByUID returns a process name for any process with uid.
|
||||
// On Android all processes of one app share the same UID; used when inode
|
||||
// lookup fails (socket closed / TIME_WAIT).
|
||||
func resolveProcessNameByUID(uid uint32) (string, error) {
|
||||
files, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if !f.IsDir() || !isPid(f.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
|
||||
processPath := filepath.Join("/proc", f.Name())
|
||||
if runtime.GOOS == "android" {
|
||||
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if name := splitCmdline(cmdline); name != "" {
|
||||
return name, nil
|
||||
}
|
||||
} else {
|
||||
if exe, err := os.Readlink(filepath.Join(processPath, "exe")); err == nil {
|
||||
return exe, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no process found with uid %d", uid)
|
||||
}
|
||||
|
||||
func splitCmdline(cmdline []byte) string {
|
||||
cmdline = bytes.Trim(cmdline, " ")
|
||||
|
||||
@@ -198,3 +261,196 @@ func isPid(s string) bool {
|
||||
return !unicode.IsDigit(r)
|
||||
}) == -1
|
||||
}
|
||||
|
||||
// resolveSocketByProcFS finds UID and inode from /proc/net/{tcp,tcp6,udp,udp6}.
|
||||
// In TUN mode metadata sourceIP is often the gateway (e.g. fake-ip range), not
|
||||
// the socket's real local address; we match by local port first and prefer
|
||||
// exact IP+port when it matches.
|
||||
func resolveSocketByProcFS(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) {
|
||||
var proto string
|
||||
switch {
|
||||
case strings.HasPrefix(network, "tcp"):
|
||||
proto = "tcp"
|
||||
case strings.HasPrefix(network, "udp"):
|
||||
proto = "udp"
|
||||
default:
|
||||
return 0, 0, ErrInvalidNetwork
|
||||
}
|
||||
|
||||
targetPort := uint16(srcPort)
|
||||
unmapped := ip.Unmap()
|
||||
files := []string{"/proc/net/" + proto, "/proc/net/" + proto + "6"}
|
||||
|
||||
var bestUID, bestInode uint32
|
||||
found := false
|
||||
|
||||
for _, path := range files {
|
||||
isV6 := strings.HasSuffix(path, "6")
|
||||
|
||||
var matchIP netip.Addr
|
||||
if unmapped.Is4() {
|
||||
if isV6 {
|
||||
matchIP = netip.AddrFrom16(unmapped.As16())
|
||||
} else {
|
||||
matchIP = unmapped
|
||||
}
|
||||
} else {
|
||||
if !isV6 {
|
||||
continue
|
||||
}
|
||||
matchIP = unmapped
|
||||
}
|
||||
|
||||
uid, inode, exact, err := searchProcNetFileByPort(path, matchIP, targetPort)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if exact {
|
||||
return uid, inode, nil
|
||||
}
|
||||
|
||||
if !found || (bestUID == 0 && uid != 0) {
|
||||
bestUID = uid
|
||||
bestInode = inode
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return bestUID, bestInode, nil
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
// searchProcNetFileByPort scans /proc/net/* for local_address matching targetPort.
|
||||
// Exact IP+port wins; else port-only (skips inode==0 entries used by TIME_WAIT).
|
||||
func searchProcNetFileByPort(path string, targetIP netip.Addr, targetPort uint16) (uid, inode uint32, exact bool, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
isV6 := strings.HasSuffix(path, "6")
|
||||
scanner := bufio.NewScanner(f)
|
||||
|
||||
if !scanner.Scan() {
|
||||
return 0, 0, false, ErrNotFound
|
||||
}
|
||||
|
||||
var bestUID, bestInode uint32
|
||||
found := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
lineIP, linePort, parseErr := parseHexAddrPort(fields[1], isV6)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
if linePort != targetPort {
|
||||
continue
|
||||
}
|
||||
|
||||
lineUID, parseErr := strconv.ParseUint(fields[7], 10, 32)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
lineInode, parseErr := strconv.ParseUint(fields[9], 10, 32)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if lineIP == targetIP {
|
||||
return uint32(lineUID), uint32(lineInode), true, nil
|
||||
}
|
||||
|
||||
if lineInode == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !found || (bestUID == 0 && lineUID != 0) {
|
||||
bestUID = uint32(lineUID)
|
||||
bestInode = uint32(lineInode)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return bestUID, bestInode, false, nil
|
||||
}
|
||||
return 0, 0, false, ErrNotFound
|
||||
}
|
||||
|
||||
func parseHexAddrPort(s string, isV6 bool) (netip.Addr, uint16, error) {
|
||||
colon := strings.IndexByte(s, ':')
|
||||
if colon < 0 {
|
||||
return netip.Addr{}, 0, fmt.Errorf("invalid addr:port: %s", s)
|
||||
}
|
||||
|
||||
port64, err := strconv.ParseUint(s[colon+1:], 16, 16)
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, err
|
||||
}
|
||||
|
||||
var addr netip.Addr
|
||||
if isV6 {
|
||||
addr, err = parseHexIPv6(s[:colon])
|
||||
} else {
|
||||
addr, err = parseHexIPv4(s[:colon])
|
||||
}
|
||||
return addr, uint16(port64), err
|
||||
}
|
||||
|
||||
func parseHexIPv4(s string) (netip.Addr, error) {
|
||||
if len(s) != 8 {
|
||||
return netip.Addr{}, fmt.Errorf("invalid ipv4 hex len: %d", len(s))
|
||||
}
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
var ip [4]byte
|
||||
if littleEndian {
|
||||
ip[0], ip[1], ip[2], ip[3] = b[3], b[2], b[1], b[0]
|
||||
} else {
|
||||
copy(ip[:], b)
|
||||
}
|
||||
return netip.AddrFrom4(ip), nil
|
||||
}
|
||||
|
||||
func parseHexIPv6(s string) (netip.Addr, error) {
|
||||
if len(s) != 32 {
|
||||
return netip.Addr{}, fmt.Errorf("invalid ipv6 hex len: %d", len(s))
|
||||
}
|
||||
var ip [16]byte
|
||||
for i := 0; i < 4; i++ {
|
||||
b, err := hex.DecodeString(s[i*8 : (i+1)*8])
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
if littleEndian {
|
||||
ip[i*4+0] = b[3]
|
||||
ip[i*4+1] = b[2]
|
||||
ip[i*4+2] = b[1]
|
||||
ip[i*4+3] = b[0]
|
||||
} else {
|
||||
copy(ip[i*4:(i+1)*4], b)
|
||||
}
|
||||
}
|
||||
return netip.AddrFrom16(ip), nil
|
||||
}
|
||||
|
||||
var littleEndian = func() bool {
|
||||
x := uint32(0x01020304)
|
||||
return *(*byte)(unsafe.Pointer(&x)) == 0x04
|
||||
}()
|
||||
|
||||
Generated
+43
-95
@@ -379,7 +379,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -1743,7 +1743,7 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1789,12 +1789,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
@@ -2360,7 +2354,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2430,7 +2424,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2889,7 +2883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4913,7 +4907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5015,16 +5009,6 @@ dependencies = [
|
||||
"which 7.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.16.1"
|
||||
@@ -6323,7 +6307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6383,9 +6367,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_allocator"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17ece0d1edc5e92822be95428460bc6b12f0dce8f95a9efabf751189a75f9f2"
|
||||
checksum = "ff805b88789451a080b3c4d49fa0ebcd02dc6c0e370ed7a37ef954fbaf79915f"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"hashbrown 0.16.1",
|
||||
@@ -6395,9 +6379,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ec0e9560cce8917197c7b13be7288707177f48a6f0ca116d0b53689e18bbc3"
|
||||
checksum = "addc03b644cd9f26996bb32883f5cf4f4e46a51d20f5fbdbf675c14b29d38e95"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"oxc_allocator",
|
||||
@@ -6412,9 +6396,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast_macros"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f266c05258e76cb84d7eee538e4fc75e2687f4220e1b2f141c490b35025a6443"
|
||||
checksum = "5950f9746248c26af04811e6db0523d354080637995be1dcc1c6bd3fca893bb2"
|
||||
dependencies = [
|
||||
"phf 0.13.1",
|
||||
"proc-macro2",
|
||||
@@ -6424,9 +6408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast_visit"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3477ca0b6dd5bebcb1d3bf4c825b65999975d6ca91d6f535bf067e979fad113a"
|
||||
checksum = "31da485219d7ca6810872ce84fbcc7d11d8492145012603ead79beaf1476dc92"
|
||||
dependencies = [
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
@@ -6436,15 +6420,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_data_structures"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8701946f2acbd655610a331cf56f0aa58349ef792e6bf2fb65c56785b87fe8e"
|
||||
checksum = "623bffc9732a0d39f248a2e7655d6d1704201790e5a8777aa188a678f1746fe8"
|
||||
|
||||
[[package]]
|
||||
name = "oxc_diagnostics"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b04ea16e6016eceb281fb61bbac5f860f075864e93ae15ec18b6c2d0b152e435"
|
||||
checksum = "3c612203fb402e998169c3e152a9fc8e736faafea0f13287c92144d4b8bc7b55"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"oxc-miette",
|
||||
@@ -6453,9 +6437,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ecmascript"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4b107cae9b8bce541a45463623e1c4b1bb073e81d966483720f0e831facdcb1"
|
||||
checksum = "04c62e45b93f4257f5ca6d00f441e669ad52d98d36332394abe9f5527cf461d6"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"num-bigint",
|
||||
@@ -6469,9 +6453,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_estree"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57b79c9e9684eab83293d67dcbbfd2b1a1f062d27a8188411eb700c6e17983fa"
|
||||
checksum = "8794e3fbcd834e8ae4246dbd3121f9ee82c6ae60bc92615a276d42b6b62a2341"
|
||||
|
||||
[[package]]
|
||||
name = "oxc_index"
|
||||
@@ -6485,9 +6469,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_parser"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "487f41bdacb3ef9afd8c5b0cb5beceec3ac4ecd0c348804aa1907606d370c731"
|
||||
checksum = "041125897019b72d23e6549d95985fe379354cf004e69cb811803109375fa91b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cow-utils",
|
||||
@@ -6508,9 +6492,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_regular_expression"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d495c085efbde1d65636497f9d3e3e58151db614a97e313e2e7a837d81865419"
|
||||
checksum = "405e9515c3ae4c7227b3596219ec256dd883cb403db3a0d1c10146f82a894c93"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"oxc_allocator",
|
||||
@@ -6524,9 +6508,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_span"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3edcf2bc8bc73cd8d252650737ef48a482484a91709b7f7a5c5ce49305f247e8"
|
||||
checksum = "894327633e5dcaef8baf34815d68100297f9776e20371502458ea3c42b8a710b"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"oxc-miette",
|
||||
@@ -6538,9 +6522,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_str"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c60f1570f04257d5678a16391f6d18dc805325e7f876b8e176a3a36fe897be"
|
||||
checksum = "50e0b900b4f66db7d5b46a454532464861f675d03e16994040484d2c04151490"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"hashbrown 0.16.1",
|
||||
@@ -6550,9 +6534,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_syntax"
|
||||
version = "0.121.0"
|
||||
version = "0.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a10c19c89298c0b126d12c5f545786405efbad9012d956ebb3190b64b29905a"
|
||||
checksum = "0a5edd0173b4667e5a1775b5d37e06a78c796fab18ee095739186831f2c54400"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cow-utils",
|
||||
@@ -7305,7 +7289,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7961,7 +7945,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8019,7 +8003,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9709,7 +9693,7 @@ dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11501,7 +11485,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12718,20 +12702,6 @@ name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
@@ -12778,28 +12748,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"constant_time_eq 0.3.1",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"getrandom 0.3.3",
|
||||
"hmac",
|
||||
"indexmap 2.13.0",
|
||||
"lzma-rust2 0.13.0",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.4.0"
|
||||
@@ -12808,14 +12756,14 @@ checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.4.2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"getrandom 0.4.1",
|
||||
"hmac",
|
||||
"indexmap 2.13.0",
|
||||
"lzma-rust2 0.16.1",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"ppmd-rust",
|
||||
@@ -12829,12 +12777,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip-extensions"
|
||||
version = "0.13.1"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9bb4da4a220bfb79c2b7bfa88466181778892ddaffaca9f06d5e9cc5de0b46f"
|
||||
checksum = "285251480403d7097be64d5c33518279ecf5f816d5ebe403c476e45ea16c5628"
|
||||
dependencies = [
|
||||
"ignore",
|
||||
"zip 6.0.0",
|
||||
"zip 8.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -107,7 +107,7 @@ semver = "1.0"
|
||||
# Compression & Encoding
|
||||
flate2 = "1.0"
|
||||
zip = "8.0.0"
|
||||
zip-extensions = "0.13.0"
|
||||
zip-extensions = "0.14.0"
|
||||
base64 = "0.22"
|
||||
adler = "1.0.2"
|
||||
hex = "0.4"
|
||||
@@ -169,12 +169,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
|
||||
|
||||
# OXC (The Oxidation Compiler)
|
||||
# We use it to parse and transpile the old script profile to esm based script profile
|
||||
oxc_parser = "0.121"
|
||||
oxc_allocator = "0.121"
|
||||
oxc_span = "0.121"
|
||||
oxc_ast = "0.121"
|
||||
oxc_syntax = "0.121"
|
||||
oxc_ast_visit = "0.121"
|
||||
oxc_parser = "0.122"
|
||||
oxc_allocator = "0.122"
|
||||
oxc_span = "0.122"
|
||||
oxc_ast = "0.122"
|
||||
oxc_syntax = "0.122"
|
||||
oxc_ast_visit = "0.122"
|
||||
|
||||
# Lua Integration
|
||||
mlua = { version = "0.11", features = [
|
||||
|
||||
@@ -155,7 +155,19 @@ async fn subscribe_url(
|
||||
url: url.to_string(),
|
||||
source: e,
|
||||
})?;
|
||||
let perform_req = || async { client.get(url.as_str()).send().await?.error_for_status() };
|
||||
let device_info = crate::utils::hwid::get_device_info();
|
||||
let sanitize = crate::utils::hwid::sanitize_for_header;
|
||||
let perform_req = || async {
|
||||
client
|
||||
.get(url.as_str())
|
||||
.header("x-hwid", &device_info.hwid)
|
||||
.header("x-device-os", sanitize(&device_info.device_os))
|
||||
.header("x-ver-os", sanitize(&device_info.os_version))
|
||||
.header("x-device-model", sanitize(&device_info.device_model))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()
|
||||
};
|
||||
let resp = perform_req
|
||||
.retry(backon::ExponentialBuilder::default())
|
||||
// Only retry on network errors or server errors
|
||||
|
||||
@@ -100,6 +100,12 @@ pub fn is_portable() -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_device_info() -> Result<crate::utils::hwid::DeviceInfo> {
|
||||
Ok(crate::utils::hwid::get_device_info())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn enhance_profiles() -> Result {
|
||||
|
||||
@@ -249,6 +249,7 @@ pub fn run() -> std::io::Result<()> {
|
||||
ipc::service::stop_service,
|
||||
ipc::service::restart_service,
|
||||
ipc::is_portable,
|
||||
ipc::get_device_info,
|
||||
ipc::get_proxies,
|
||||
ipc::select_proxy,
|
||||
ipc::update_proxy_provider,
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Type)]
|
||||
pub struct DeviceInfo {
|
||||
pub hwid: String,
|
||||
pub device_os: String,
|
||||
pub os_version: String,
|
||||
pub device_model: String,
|
||||
}
|
||||
|
||||
impl Default for DeviceInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hwid: generate_fallback_hwid(),
|
||||
device_os: get_device_os().to_string(),
|
||||
os_version: get_os_version(),
|
||||
device_model: get_device_model(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached device info — computed once, reused for all subscription requests.
|
||||
/// Starts from `Default` (which already has real OS/model data and a fallback HWID),
|
||||
/// then tries to replace the HWID with a platform-specific one.
|
||||
pub static DEVICE_INFO: Lazy<DeviceInfo> = Lazy::new(|| {
|
||||
let mut info = DeviceInfo::default();
|
||||
match get_platform_hwid() {
|
||||
Ok(hwid) => {
|
||||
tracing::debug!(
|
||||
"HWID generated: {}...{}",
|
||||
&hwid[..4],
|
||||
&hwid[hwid.len() - 4..]
|
||||
);
|
||||
info.hwid = hwid;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to generate platform HWID, using fallback: {e:?}");
|
||||
}
|
||||
}
|
||||
info
|
||||
});
|
||||
|
||||
/// Public accessor that returns a clone of the cached DeviceInfo.
|
||||
pub fn get_device_info() -> DeviceInfo {
|
||||
DEVICE_INFO.clone()
|
||||
}
|
||||
|
||||
/// Generates a platform-specific HWID from the machine ID.
|
||||
fn get_platform_hwid() -> Result<String> {
|
||||
let raw_id = get_platform_machine_id()?;
|
||||
let salted = format!("clash-nyanpasu:{}", raw_id);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(salted.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
Ok(hex::encode(&hash[..16])) // 32 hex chars
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Platform machine ID
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_platform_machine_id() -> Result<String> {
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let key = hklm
|
||||
.open_subkey("SOFTWARE\\Microsoft\\Cryptography")
|
||||
.map_err(|e| anyhow!("Failed to open Cryptography registry key: {e}"))?;
|
||||
let guid: String = key
|
||||
.get_value("MachineGuid")
|
||||
.map_err(|e| anyhow!("Failed to read MachineGuid: {e}"))?;
|
||||
if guid.is_empty() {
|
||||
return Err(anyhow!("MachineGuid is empty"));
|
||||
}
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_platform_machine_id() -> Result<String> {
|
||||
let output = std::process::Command::new("ioreg")
|
||||
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to run ioreg: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.contains("IOPlatformUUID") {
|
||||
if let Some(uuid) = line.split('"').nth(3) {
|
||||
if !uuid.is_empty() {
|
||||
return Ok(uuid.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!("IOPlatformUUID not found in ioreg output"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_platform_machine_id() -> Result<String> {
|
||||
for path in &["/etc/machine-id", "/var/lib/dbus/machine-id"] {
|
||||
if let Ok(id) = std::fs::read_to_string(path) {
|
||||
let id = id.trim().to_string();
|
||||
if !id.is_empty() {
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!(
|
||||
"Could not read machine-id from /etc/machine-id or /var/lib/dbus/machine-id"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
fn get_platform_machine_id() -> Result<String> {
|
||||
Err(anyhow!("Unsupported platform for HWID generation"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fallback HWID
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Generates a deterministic 32-char hex fallback HWID when platform-specific
|
||||
/// machine ID retrieval fails. Uses a fixed seed so the value is stable.
|
||||
fn generate_fallback_hwid() -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"clash-nyanpasu:fallback");
|
||||
let hash = hasher.finalize();
|
||||
hex::encode(&hash[..16])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ASCII sanitization for HTTP headers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Strips non-ASCII characters from a string to produce a valid HTTP header value.
|
||||
/// `reqwest::header::HeaderValue` rejects non-ASCII bytes, so this prevents
|
||||
/// runtime panics for users with localized hostnames or model names.
|
||||
pub fn sanitize_for_header(s: &str) -> String {
|
||||
s.chars().filter(|c| c.is_ascii() && *c >= ' ').collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device OS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn get_device_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"Windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macOS"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"Linux"
|
||||
} else {
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OS version
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn get_os_version() -> String {
|
||||
sysinfo::System::os_version().unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_device_model() -> String {
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
if let Ok(key) = hklm.open_subkey("HARDWARE\\DESCRIPTION\\System\\BIOS") {
|
||||
if let Ok(name) = key.get_value::<String, _>("SystemProductName") {
|
||||
if !name.is_empty() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
whoami::devicename()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_device_model() -> String {
|
||||
if let Ok(output) = std::process::Command::new("sysctl")
|
||||
.args(["-n", "hw.model"])
|
||||
.output()
|
||||
{
|
||||
let model = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !model.is_empty() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
whoami::devicename()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_device_model() -> String {
|
||||
if let Ok(name) = std::fs::read_to_string("/sys/devices/virtual/dmi/id/product_name") {
|
||||
let name = name.trim().to_string();
|
||||
if !name.is_empty() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
whoami::devicename()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
fn get_device_model() -> String {
|
||||
whoami::devicename()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hwid_is_deterministic() {
|
||||
let info1 = get_device_info();
|
||||
let info2 = get_device_info();
|
||||
assert_eq!(info1.hwid, info2.hwid);
|
||||
assert_eq!(info1.hwid.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_os_not_empty() {
|
||||
let os = get_device_os();
|
||||
assert!(!os.is_empty());
|
||||
assert!(["Windows", "macOS", "Linux", "Unknown"].contains(&os));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_os_version_not_empty() {
|
||||
let ver = get_os_version();
|
||||
assert!(!ver.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_model_not_empty() {
|
||||
let model = get_device_model();
|
||||
assert!(!model.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_hwid_is_valid_hex_32() {
|
||||
let hwid = generate_fallback_hwid();
|
||||
assert_eq!(hwid.len(), 32);
|
||||
assert!(hwid.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_for_header_strips_non_ascii() {
|
||||
assert_eq!(sanitize_for_header("Hello"), "Hello");
|
||||
assert_eq!(sanitize_for_header("Привет"), "");
|
||||
assert_eq!(sanitize_for_header("PC-Кирилл"), "PC-");
|
||||
assert_eq!(sanitize_for_header("Model\x00Name"), "ModelName");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod init;
|
||||
pub mod resolve;
|
||||
// mod winhelp;
|
||||
pub mod downloader;
|
||||
pub mod hwid;
|
||||
#[cfg(windows)]
|
||||
pub mod winreg;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"dayjs": "1.11.20",
|
||||
"framer-motion": "12.38.0",
|
||||
"i18next": "25.10.9",
|
||||
"jotai": "2.18.1",
|
||||
"jotai": "2.19.0",
|
||||
"json-schema": "0.4.0",
|
||||
"material-react-table": "3.2.1",
|
||||
"monaco-editor": "0.55.1",
|
||||
@@ -69,7 +69,7 @@
|
||||
"@csstools/normalize.css": "12.1.1",
|
||||
"@emotion/babel-plugin": "11.13.5",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@iconify/json": "2.2.454",
|
||||
"@iconify/json": "2.2.455",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@tanstack/react-query": "5.95.2",
|
||||
"@tanstack/react-router": "1.167.5",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"mihomo_alpha": "alpha-d0f3312",
|
||||
"clash_rs": "v0.9.6",
|
||||
"clash_premium": "2023-09-05-gdcc8d87",
|
||||
"clash_rs_alpha": "0.9.6-alpha+sha.7be7e89"
|
||||
"clash_rs_alpha": "0.9.6-alpha+sha.b667e62"
|
||||
},
|
||||
"arch_template": {
|
||||
"mihomo": {
|
||||
@@ -69,5 +69,5 @@
|
||||
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-03-24T22:23:10.009Z"
|
||||
"updated_at": "2026-03-25T22:24:04.344Z"
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"cross-env": "10.1.0",
|
||||
"dedent": "1.7.2",
|
||||
"globals": "17.4.0",
|
||||
"knip": "6.0.5",
|
||||
"knip": "6.0.6",
|
||||
"lint-staged": "16.4.0",
|
||||
"npm-run-all2": "8.0.4",
|
||||
"oxlint": "1.57.0",
|
||||
@@ -80,7 +80,7 @@
|
||||
"prettier-plugin-ember-template-tag": "2.1.3",
|
||||
"prettier-plugin-tailwindcss": "0.7.2",
|
||||
"prettier-plugin-toml": "2.0.6",
|
||||
"stylelint": "17.5.0",
|
||||
"stylelint": "17.6.0",
|
||||
"stylelint-config-html": "1.1.0",
|
||||
"stylelint-config-recess-order": "7.7.0",
|
||||
"stylelint-config-standard": "40.0.0",
|
||||
|
||||
Generated
+66
-76
@@ -59,8 +59,8 @@ importers:
|
||||
specifier: 17.4.0
|
||||
version: 17.4.0
|
||||
knip:
|
||||
specifier: 6.0.5
|
||||
version: 6.0.5
|
||||
specifier: 6.0.6
|
||||
version: 6.0.6
|
||||
lint-staged:
|
||||
specifier: 16.4.0
|
||||
version: 16.4.0
|
||||
@@ -95,26 +95,26 @@ importers:
|
||||
specifier: 2.0.6
|
||||
version: 2.0.6(prettier@3.8.1)
|
||||
stylelint:
|
||||
specifier: 17.5.0
|
||||
version: 17.5.0(typescript@5.9.3)
|
||||
specifier: 17.6.0
|
||||
version: 17.6.0(typescript@5.9.3)
|
||||
stylelint-config-html:
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(postcss-html@1.8.1)(stylelint@17.5.0(typescript@5.9.3))
|
||||
version: 1.1.0(postcss-html@1.8.1)(stylelint@17.6.0(typescript@5.9.3))
|
||||
stylelint-config-recess-order:
|
||||
specifier: 7.7.0
|
||||
version: 7.7.0(stylelint-order@8.1.1(stylelint@17.5.0(typescript@5.9.3)))(stylelint@17.5.0(typescript@5.9.3))
|
||||
version: 7.7.0(stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)))(stylelint@17.6.0(typescript@5.9.3))
|
||||
stylelint-config-standard:
|
||||
specifier: 40.0.0
|
||||
version: 40.0.0(stylelint@17.5.0(typescript@5.9.3))
|
||||
version: 40.0.0(stylelint@17.6.0(typescript@5.9.3))
|
||||
stylelint-declaration-block-no-ignored-properties:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0(stylelint@17.5.0(typescript@5.9.3))
|
||||
version: 3.0.0(stylelint@17.6.0(typescript@5.9.3))
|
||||
stylelint-order:
|
||||
specifier: 8.1.1
|
||||
version: 8.1.1(stylelint@17.5.0(typescript@5.9.3))
|
||||
version: 8.1.1(stylelint@17.6.0(typescript@5.9.3))
|
||||
stylelint-scss:
|
||||
specifier: 7.0.0
|
||||
version: 7.0.0(stylelint@17.5.0(typescript@5.9.3))
|
||||
version: 7.0.0(stylelint@17.6.0(typescript@5.9.3))
|
||||
tailwindcss:
|
||||
specifier: 4.2.2
|
||||
version: 4.2.2
|
||||
@@ -264,8 +264,8 @@ importers:
|
||||
specifier: 25.10.9
|
||||
version: 25.10.9(typescript@5.9.3)
|
||||
jotai:
|
||||
specifier: 2.18.1
|
||||
version: 2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
|
||||
specifier: 2.19.0
|
||||
version: 2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
|
||||
json-schema:
|
||||
specifier: 0.4.0
|
||||
version: 0.4.0
|
||||
@@ -334,8 +334,8 @@ importers:
|
||||
specifier: 11.14.0
|
||||
version: 11.14.0(@types/react@19.2.14)(react@19.2.4)
|
||||
'@iconify/json':
|
||||
specifier: 2.2.454
|
||||
version: 2.2.454
|
||||
specifier: 2.2.455
|
||||
version: 2.2.455
|
||||
'@monaco-editor/react':
|
||||
specifier: 4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -600,8 +600,8 @@ importers:
|
||||
specifier: 2.26.22
|
||||
version: 2.26.22
|
||||
undici:
|
||||
specifier: 7.24.5
|
||||
version: 7.24.5
|
||||
specifier: 7.24.6
|
||||
version: 7.24.6
|
||||
yargs:
|
||||
specifier: 18.0.0
|
||||
version: 18.0.0
|
||||
@@ -1343,8 +1343,13 @@ packages:
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.0.29':
|
||||
resolution: {integrity: sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==}
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.1':
|
||||
resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==}
|
||||
peerDependencies:
|
||||
css-tree: ^3.2.1
|
||||
peerDependenciesMeta:
|
||||
css-tree:
|
||||
optional: true
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0':
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
@@ -1704,8 +1709,8 @@ packages:
|
||||
prettier-plugin-ember-template-tag:
|
||||
optional: true
|
||||
|
||||
'@iconify/json@2.2.454':
|
||||
resolution: {integrity: sha512-U1c0+LBtcF4YDQGOVZpoA4w96hBcIVHFjBcg60pFdnF+CApRjnlIp27Jf1g7BaZlJ+vYcExyc/1EjUyZrY8MFQ==}
|
||||
'@iconify/json@2.2.455':
|
||||
resolution: {integrity: sha512-ruOl4ZTLW/qNDjuEE8Zb+LQPbRiLdRu4JEnewX8OhWDevAjrTY6UyqJdW0o2umOqcg8WmKJBihVI7YwBUCH7/Q==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
@@ -5653,10 +5658,6 @@ packages:
|
||||
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
get-east-asian-width@1.4.0:
|
||||
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
get-east-asian-width@1.5.0:
|
||||
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5992,8 +5993,8 @@ packages:
|
||||
jju@1.4.0:
|
||||
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
|
||||
|
||||
jotai@2.18.1:
|
||||
resolution: {integrity: sha512-e0NOzK+yRFwHo7DOp0DS0Ycq74KMEAObDWFGmfEL28PD9nLqBTt3/Ug7jf9ca72x0gC9LQZG9zH+0ISICmy3iA==}
|
||||
jotai@2.19.0:
|
||||
resolution: {integrity: sha512-r2wwxEXP1F2JteDLZEOPoIpAHhV89paKsN5GWVYndPNMMP/uVZDcC+fNj0A8NjKgaPWzdyO8Vp8YcYKe0uCEqQ==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0'
|
||||
@@ -6079,8 +6080,8 @@ packages:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
knip@6.0.5:
|
||||
resolution: {integrity: sha512-+i9e/ZKuYlECB5iIK82NQwnYso4oNLBhzsTbXhSqCG1qfGi6D84GNtRENafmS3C0lABX8Wf3BKM434nPXi2AbQ==}
|
||||
knip@6.0.6:
|
||||
resolution: {integrity: sha512-PA+r1mTDLHH3eShlffn2ZDyH1hHvmgDj7JsTP3JKuhV/jZTyHbRkGcOd+uaSxfJZmcZyOE5zw3naP33WllTIlA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -7475,10 +7476,6 @@ packages:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
string-width@8.1.1:
|
||||
resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
string-width@8.2.0:
|
||||
resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -7559,8 +7556,8 @@ packages:
|
||||
peerDependencies:
|
||||
stylelint: ^16.8.2 || ^17.0.0
|
||||
|
||||
stylelint@17.5.0:
|
||||
resolution: {integrity: sha512-o/NS6zhsPZFmgUm5tXX4pVNg1XDOZSlucLdf2qow/lVn4JIyzZIQ5b3kad1ugqUj3GSIgr2u5lQw7X8rjqw33g==}
|
||||
stylelint@17.6.0:
|
||||
resolution: {integrity: sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -7760,8 +7757,8 @@ packages:
|
||||
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
undici@7.24.5:
|
||||
resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
|
||||
undici@7.24.6:
|
||||
resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1:
|
||||
@@ -8093,8 +8090,8 @@ packages:
|
||||
write-file-atomic@1.3.4:
|
||||
resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==}
|
||||
|
||||
write-file-atomic@7.0.0:
|
||||
resolution: {integrity: sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==}
|
||||
write-file-atomic@7.0.1:
|
||||
resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
y18n@5.0.8:
|
||||
@@ -9157,7 +9154,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.0.29': {}
|
||||
'@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)':
|
||||
optionalDependencies:
|
||||
css-tree: 3.2.1
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
@@ -9490,7 +9489,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@iconify/json@2.2.454':
|
||||
'@iconify/json@2.2.455':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
pathe: 2.0.3
|
||||
@@ -12643,7 +12642,7 @@ snapshots:
|
||||
cli-truncate@5.1.0:
|
||||
dependencies:
|
||||
slice-ansi: 7.1.0
|
||||
string-width: 8.1.1
|
||||
string-width: 8.2.0
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
@@ -13343,8 +13342,6 @@ snapshots:
|
||||
|
||||
get-east-asian-width@1.2.0: {}
|
||||
|
||||
get-east-asian-width@1.4.0: {}
|
||||
|
||||
get-east-asian-width@1.5.0: {}
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
@@ -13619,7 +13616,7 @@ snapshots:
|
||||
|
||||
is-fullwidth-code-point@5.0.0:
|
||||
dependencies:
|
||||
get-east-asian-width: 1.4.0
|
||||
get-east-asian-width: 1.5.0
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
@@ -13667,7 +13664,7 @@ snapshots:
|
||||
|
||||
jju@1.4.0: {}
|
||||
|
||||
jotai@2.18.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
|
||||
jotai@2.19.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/template': 7.28.6
|
||||
@@ -13721,7 +13718,7 @@ snapshots:
|
||||
|
||||
kind-of@6.0.3: {}
|
||||
|
||||
knip@6.0.5:
|
||||
knip@6.0.6:
|
||||
dependencies:
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
fast-glob: 3.3.3
|
||||
@@ -13869,7 +13866,7 @@ snapshots:
|
||||
ansi-escapes: 7.0.0
|
||||
cli-cursor: 5.0.0
|
||||
slice-ansi: 7.1.0
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.2.0
|
||||
wrap-ansi: 9.0.0
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
@@ -15298,11 +15295,6 @@ snapshots:
|
||||
get-east-asian-width: 1.2.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string-width@8.1.1:
|
||||
dependencies:
|
||||
get-east-asian-width: 1.4.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string-width@8.2.0:
|
||||
dependencies:
|
||||
get-east-asian-width: 1.5.0
|
||||
@@ -15339,36 +15331,36 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.3
|
||||
|
||||
stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-config-html@1.1.0(postcss-html@1.8.1)(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
postcss-html: 1.8.1
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
|
||||
stylelint-config-recess-order@7.7.0(stylelint-order@8.1.1(stylelint@17.5.0(typescript@5.9.3)))(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-config-recess-order@7.7.0(stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)))(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint-order: 8.1.1(stylelint@17.5.0(typescript@5.9.3))
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
stylelint-order: 8.1.1(stylelint@17.6.0(typescript@5.9.3))
|
||||
|
||||
stylelint-config-recommended@18.0.0(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
|
||||
stylelint-config-standard@40.0.0(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint-config-recommended: 18.0.0(stylelint@17.5.0(typescript@5.9.3))
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3))
|
||||
|
||||
stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-declaration-block-no-ignored-properties@3.0.0(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
|
||||
stylelint-order@8.1.1(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-order@8.1.1(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
postcss: 8.5.8
|
||||
postcss-sorting: 10.0.0(postcss@8.5.8)
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
|
||||
stylelint-scss@7.0.0(stylelint@17.5.0(typescript@5.9.3)):
|
||||
stylelint-scss@7.0.0(stylelint@17.6.0(typescript@5.9.3)):
|
||||
dependencies:
|
||||
css-tree: 3.1.0
|
||||
is-plain-object: 5.0.0
|
||||
@@ -15378,13 +15370,13 @@ snapshots:
|
||||
postcss-resolve-nested-selector: 0.1.6
|
||||
postcss-selector-parser: 7.1.1
|
||||
postcss-value-parser: 4.2.0
|
||||
stylelint: 17.5.0(typescript@5.9.3)
|
||||
stylelint: 17.6.0(typescript@5.9.3)
|
||||
|
||||
stylelint@17.5.0(typescript@5.9.3):
|
||||
stylelint@17.6.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.0.29
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
'@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1)
|
||||
@@ -15403,7 +15395,6 @@ snapshots:
|
||||
html-tags: 5.1.0
|
||||
ignore: 7.0.5
|
||||
import-meta-resolve: 4.2.0
|
||||
imurmurhash: 0.1.4
|
||||
is-plain-object: 5.0.0
|
||||
mathml-tag-names: 4.0.0
|
||||
meow: 14.1.0
|
||||
@@ -15418,7 +15409,7 @@ snapshots:
|
||||
supports-hyperlinks: 4.4.0
|
||||
svg-tags: 1.0.0
|
||||
table: 6.9.0
|
||||
write-file-atomic: 7.0.0
|
||||
write-file-atomic: 7.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -15627,7 +15618,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
|
||||
undici@7.24.5: {}
|
||||
undici@7.24.6: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
|
||||
@@ -15956,7 +15947,7 @@ snapshots:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
@@ -15966,9 +15957,8 @@ snapshots:
|
||||
imurmurhash: 0.1.4
|
||||
slide: 1.1.6
|
||||
|
||||
write-file-atomic@7.0.0:
|
||||
write-file-atomic@7.0.1:
|
||||
dependencies:
|
||||
imurmurhash: 0.1.4
|
||||
signal-exit: 4.1.0
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"picocolors": "1.1.1",
|
||||
"tar": "7.5.13",
|
||||
"telegram": "2.26.22",
|
||||
"undici": "7.24.5",
|
||||
"undici": "7.24.6",
|
||||
"yargs": "18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ func (c *redisUploadCache) GetLength(filePath string) (int64, error) {
|
||||
return 0, fmt.Errorf("invalid upload length in cache: %w", err)
|
||||
}
|
||||
|
||||
c.Touch(filePath)
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
||||
|
||||
+40
@@ -22,6 +22,46 @@ Signed-off-by: Marius Durbaca <mariusd84@gmail.com>
|
||||
};
|
||||
|
||||
pwm-leds {
|
||||
@@ -113,6 +117,19 @@
|
||||
reset-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
|
||||
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
|
||||
status = "okay";
|
||||
+
|
||||
+ bridge@0,0 {
|
||||
+ reg = <0x00000000 0 0 0 0>;
|
||||
+ #address-cells = <3>;
|
||||
+ #size-cells = <2>;
|
||||
+ ranges;
|
||||
+
|
||||
+ ethernet@0,0 {
|
||||
+ compatible = "pci10ec,8125";
|
||||
+ reg = <0x000000 0 0 0 0>;
|
||||
+ led-data = <0 0x3f 0x23f 0>;
|
||||
+ };
|
||||
+ };
|
||||
};
|
||||
|
||||
&pcie30phy {
|
||||
@@ -136,6 +153,19 @@
|
||||
reset-gpios = <&gpio2 RK_PD6 GPIO_ACTIVE_HIGH>;
|
||||
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
|
||||
status = "okay";
|
||||
+
|
||||
+ bridge@0,0 {
|
||||
+ reg = <0x00200000 0 0 0 0>;
|
||||
+ #address-cells = <3>;
|
||||
+ #size-cells = <2>;
|
||||
+ ranges;
|
||||
+
|
||||
+ ethernet@0,0 {
|
||||
+ compatible = "pci10ec,8125";
|
||||
+ reg = <0x000000 0 0 0 0>;
|
||||
+ led-data = <0 0x3f 0x23f 0>;
|
||||
+ };
|
||||
+ };
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
--- a/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
|
||||
+++ b/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Marius Durbaca <mariusd84@gmail.com>
|
||||
Date: Tue Feb 27 16:25:27 2024 +0200
|
||||
Subject: [PATCH] arm64: dts: rockchip: Update LED properties for Radxa
|
||||
E25
|
||||
|
||||
Add OpenWrt's LED aliases for showing system status.
|
||||
|
||||
Signed-off-by: Marius Durbaca <mariusd84@gmail.com>
|
||||
---
|
||||
|
||||
--- a/arch/arm64/boot/dts/rockchip/rk3568-radxa-e25.dts
|
||||
+++ b/arch/arm64/boot/dts/rockchip/rk3568-radxa-e25.dts
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
aliases {
|
||||
mmc1 = &sdmmc0;
|
||||
+ led-boot = &led_user;
|
||||
+ led-failsafe = &led_user;
|
||||
+ led-running = &led_user;
|
||||
+ led-upgrade = &led_user;
|
||||
};
|
||||
|
||||
pwm-leds {
|
||||
@@ -109,6 +113,19 @@
|
||||
reset-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
|
||||
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
|
||||
status = "okay";
|
||||
+
|
||||
+ bridge@0,0 {
|
||||
+ reg = <0x00000000 0 0 0 0>;
|
||||
+ #address-cells = <3>;
|
||||
+ #size-cells = <2>;
|
||||
+ ranges;
|
||||
+
|
||||
+ ethernet@0,0 {
|
||||
+ compatible = "pci10ec,8125";
|
||||
+ reg = <0x000000 0 0 0 0>;
|
||||
+ led-data = <0 0x3f 0x23f 0>;
|
||||
+ };
|
||||
+ };
|
||||
};
|
||||
|
||||
&pcie30phy {
|
||||
@@ -132,6 +149,19 @@
|
||||
reset-gpios = <&gpio2 RK_PD6 GPIO_ACTIVE_HIGH>;
|
||||
vpcie3v3-supply = <&vcc3v3_pi6c_05>;
|
||||
status = "okay";
|
||||
+
|
||||
+ bridge@0,0 {
|
||||
+ reg = <0x00200000 0 0 0 0>;
|
||||
+ #address-cells = <3>;
|
||||
+ #size-cells = <2>;
|
||||
+ ranges;
|
||||
+
|
||||
+ ethernet@0,0 {
|
||||
+ compatible = "pci10ec,8125";
|
||||
+ reg = <0x000000 0 0 0 0>;
|
||||
+ led-data = <0 0x3f 0x23f 0>;
|
||||
+ };
|
||||
+ };
|
||||
};
|
||||
|
||||
&pinctrl {
|
||||
--- a/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
|
||||
+++ b/arch/arm64/boot/dts/rockchip/rk3568-radxa-cm3i.dtsi
|
||||
@@ -23,7 +23,7 @@
|
||||
gpios = <&gpio0 RK_PA6 GPIO_ACTIVE_HIGH>;
|
||||
function = LED_FUNCTION_HEARTBEAT;
|
||||
color = <LED_COLOR_ID_GREEN>;
|
||||
- linux,default-trigger = "heartbeat";
|
||||
+ default-state = "on";
|
||||
pinctrl-names = "default";
|
||||
pinctrl-0 = <&led_user_en>;
|
||||
};
|
||||
@@ -155,7 +155,7 @@ proxies:
|
||||
multiplexing: MULTIPLEXING_HIGH
|
||||
```
|
||||
|
||||
Clash.Meta / mihomo only support mieru TCP proxy protocol. Use `udp: true` to allow socks5 UDP associate requests. You can't have both `port` and `port-range` properties in a single configuration.
|
||||
Use `udp: true` to allow socks5 UDP associate requests. You can't have both `port` and `port-range` properties in a single configuration.
|
||||
|
||||
## Advanced Settings
|
||||
|
||||
@@ -265,4 +265,6 @@ The simple sharing link above is equivalent to the following client configuratio
|
||||
}
|
||||
```
|
||||
|
||||
You can decode the client configuration from a standard sharing link or a simple sharing link by running `mieru explain config <URL>` command.
|
||||
|
||||
Note: a simple sharing link does not contain necessary client configurations such as `socks5Port`. Therefore, importing a simple sharing link on a brand new device will fail.
|
||||
|
||||
@@ -155,7 +155,7 @@ proxies:
|
||||
multiplexing: MULTIPLEXING_HIGH
|
||||
```
|
||||
|
||||
Clash.Meta / mihomo 只支持 mieru TCP 代理协议。使用 `udp: true` 允许 socks5 UDP associate 请求。你不能在一条配置内同时使用 `port` 和 `port-range` 属性。
|
||||
使用 `udp: true` 允许 socks5 UDP associate 请求。你不能在一条配置内同时使用 `port` 和 `port-range` 属性。
|
||||
|
||||
## 高级设置
|
||||
|
||||
@@ -265,4 +265,6 @@ mierus://baozi:manlianpenfen@1.2.3.4?handshake-mode=HANDSHAKE_NO_WAIT&mtu=1400&m
|
||||
}
|
||||
```
|
||||
|
||||
你可以运行 `mieru explain config <URL>` 指令,以解码标准分享链接或简单分享链接对应的客户端配置。
|
||||
|
||||
注意,简单分享链接不含有 `socks5Port` 等必要的客户端配置。因此,在全新的设备上导入简单分享链接会失败。
|
||||
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
From f4de14a515221e27c0d79446b423849a6546e3a6 Mon Sep 17 00:00:00 2001
|
||||
From: Brad Fitzpatrick <bradfitz@golang.org>
|
||||
Date: Tue, 24 Mar 2026 23:02:09 +0000
|
||||
Subject: [PATCH] runtime: use uname version check for 64-bit time on 32-bit
|
||||
arch codepaths
|
||||
|
||||
The previous fallback-on-ENOSYS logic causes issues on forks of Linux.
|
||||
|
||||
Android: #77621 (CL 750040 added a workaround with a TODO,
|
||||
this fixes that TODO)
|
||||
Causes the OS to terminate the program when running on Android
|
||||
versions <=10 since the seccomp jail does not know about the 64-bit
|
||||
time syscall and is configured to terminate the program on any
|
||||
unknown syscall.
|
||||
|
||||
Synology's Linux: #77930
|
||||
On old versions of Synology's Linux they added custom vendor syscalls
|
||||
without adding a gap in the syscall numbers, that means when we call
|
||||
the newer Linux syscall which was added later, Synology's Linux
|
||||
interprets it as a completely different vendor syscall.
|
||||
|
||||
Originally by Jorropo in CL 751340.
|
||||
|
||||
Fixes golang/go#77930
|
||||
Updates tailscale/go#162
|
||||
Originally https://go-review.googlesource.com/c/go/+/758902/2
|
||||
|
||||
Co-authored-by: Jorropo <jorropo.pgm@gmail.com>
|
||||
Change-Id: I90e15495d9249fd7f6e112f9e3ae8ad1322f56e0
|
||||
---
|
||||
.../runtime/syscall/linux/defs_linux_386.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_amd64.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_arm.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_arm64.go | 1 +
|
||||
.../syscall/linux/defs_linux_loong64.go | 1 +
|
||||
.../syscall/linux/defs_linux_mips64x.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_mipsx.go | 1 +
|
||||
.../syscall/linux/defs_linux_ppc64x.go | 1 +
|
||||
.../syscall/linux/defs_linux_riscv64.go | 1 +
|
||||
.../runtime/syscall/linux/defs_linux_s390x.go | 1 +
|
||||
.../runtime/syscall/linux/syscall_linux.go | 14 +++++
|
||||
src/runtime/os_linux.go | 62 +++++++++++++++++++
|
||||
src/runtime/os_linux32.go | 28 +++------
|
||||
src/runtime/os_linux64.go | 2 +
|
||||
14 files changed, 97 insertions(+), 19 deletions(-)
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_386.go b/src/internal/runtime/syscall/linux/defs_linux_386.go
|
||||
index 7fdf5d3f8062fa..4e8e645dc49a66 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_386.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_386.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 295
|
||||
SYS_PREAD64 = 180
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_amd64.go b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
|
||||
index 2c8676e6e9b4d9..fa764d9ccd9b8e 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_amd64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_amd64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 257
|
||||
SYS_PREAD64 = 17
|
||||
SYS_READ = 0
|
||||
+ SYS_UNAME = 63
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm.go b/src/internal/runtime/syscall/linux/defs_linux_arm.go
|
||||
index a0b395d6762734..cef556d5f6f986 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_arm.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 322
|
||||
SYS_PREAD64 = 180
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_arm64.go b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
|
||||
index 223dce0c5b4281..eabddbac1bc063 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_arm64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_arm64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 56
|
||||
SYS_PREAD64 = 67
|
||||
SYS_READ = 63
|
||||
+ SYS_UNAME = 160
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_loong64.go b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
|
||||
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_loong64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_loong64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 56
|
||||
SYS_PREAD64 = 67
|
||||
SYS_READ = 63
|
||||
+ SYS_UNAME = 160
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
|
||||
index 84b760dc1b5545..b5794e5002af5e 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_mips64x.go
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
SYS_OPENAT = 5247
|
||||
SYS_PREAD64 = 5016
|
||||
SYS_READ = 5000
|
||||
+ SYS_UNAME = 5061
|
||||
|
||||
EFD_NONBLOCK = 0x80
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
|
||||
index a9be21414c26f9..1fb4d919d1a318 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_mipsx.go
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
SYS_OPENAT = 4288
|
||||
SYS_PREAD64 = 4200
|
||||
SYS_READ = 4003
|
||||
+ SYS_UNAME = 4122
|
||||
|
||||
EFD_NONBLOCK = 0x80
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
|
||||
index 63f4e5d7864de4..ee93ad345b810f 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_ppc64x.go
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
SYS_OPENAT = 286
|
||||
SYS_PREAD64 = 179
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
|
||||
index 8aa61c391dcdcb..08e5d49b83c9bd 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_riscv64.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 56
|
||||
SYS_PREAD64 = 67
|
||||
SYS_READ = 63
|
||||
+ SYS_UNAME = 160
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/defs_linux_s390x.go b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
|
||||
index 52945db0e5b72f..da11c704081abc 100644
|
||||
--- a/src/internal/runtime/syscall/linux/defs_linux_s390x.go
|
||||
+++ b/src/internal/runtime/syscall/linux/defs_linux_s390x.go
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SYS_OPENAT = 288
|
||||
SYS_PREAD64 = 180
|
||||
SYS_READ = 3
|
||||
+ SYS_UNAME = 122
|
||||
|
||||
EFD_NONBLOCK = 0x800
|
||||
|
||||
diff --git a/src/internal/runtime/syscall/linux/syscall_linux.go b/src/internal/runtime/syscall/linux/syscall_linux.go
|
||||
index 8201e7d1907444..b64f511b03c947 100644
|
||||
--- a/src/internal/runtime/syscall/linux/syscall_linux.go
|
||||
+++ b/src/internal/runtime/syscall/linux/syscall_linux.go
|
||||
@@ -86,3 +86,17 @@ func Pread(fd int, p []byte, offset int64) (n int, errno uintptr) {
|
||||
}
|
||||
return int(r1), e
|
||||
}
|
||||
+
|
||||
+type Utsname struct {
|
||||
+ Sysname [65]byte
|
||||
+ Nodename [65]byte
|
||||
+ Release [65]byte
|
||||
+ Version [65]byte
|
||||
+ Machine [65]byte
|
||||
+ Domainname [65]byte
|
||||
+}
|
||||
+
|
||||
+func Uname(buf *Utsname) (errno uintptr) {
|
||||
+ _, _, e := Syscall6(SYS_UNAME, uintptr(unsafe.Pointer(buf)), 0, 0, 0, 0, 0)
|
||||
+ return e
|
||||
+}
|
||||
diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go
|
||||
index 7e6af22d48a764..493567b5303673 100644
|
||||
--- a/src/runtime/os_linux.go
|
||||
+++ b/src/runtime/os_linux.go
|
||||
@@ -354,6 +354,7 @@ func osinit() {
|
||||
numCPUStartup = getCPUCount()
|
||||
physHugePageSize = getHugePageSize()
|
||||
vgetrandomInit()
|
||||
+ configure64bitsTimeOn32BitsArchitectures()
|
||||
}
|
||||
|
||||
var urandom_dev = []byte("/dev/urandom\x00")
|
||||
@@ -935,3 +936,64 @@ func mprotect(addr unsafe.Pointer, n uintptr, prot int32) (ret int32, errno int3
|
||||
r, _, err := linux.Syscall6(linux.SYS_MPROTECT, uintptr(addr), n, uintptr(prot), 0, 0, 0)
|
||||
return int32(r), int32(err)
|
||||
}
|
||||
+
|
||||
+type kernelVersion struct {
|
||||
+ major int
|
||||
+ minor int
|
||||
+}
|
||||
+
|
||||
+// getKernelVersion returns major and minor kernel version numbers
|
||||
+// parsed from the uname release field.
|
||||
+func getKernelVersion() kernelVersion {
|
||||
+ var buf linux.Utsname
|
||||
+ if e := linux.Uname(&buf); e != 0 {
|
||||
+ throw("uname failed")
|
||||
+ }
|
||||
+
|
||||
+ rel := gostringnocopy(&buf.Release[0])
|
||||
+ major, minor, _, ok := parseRelease(rel)
|
||||
+ if !ok {
|
||||
+ throw("failed to parse kernel version from uname")
|
||||
+ }
|
||||
+ return kernelVersion{major: major, minor: minor}
|
||||
+}
|
||||
+
|
||||
+// parseRelease parses a dot-separated version number. It follows the
|
||||
+// semver syntax, but allows the minor and patch versions to be
|
||||
+// elided.
|
||||
+func parseRelease(rel string) (major, minor, patch int, ok bool) {
|
||||
+ // Strip anything after a dash or plus.
|
||||
+ for i := 0; i < len(rel); i++ {
|
||||
+ if rel[i] == '-' || rel[i] == '+' {
|
||||
+ rel = rel[:i]
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ next := func() (int, bool) {
|
||||
+ for i := 0; i < len(rel); i++ {
|
||||
+ if rel[i] == '.' {
|
||||
+ ver, err := strconv.Atoi(rel[:i])
|
||||
+ rel = rel[i+1:]
|
||||
+ return ver, err == nil
|
||||
+ }
|
||||
+ }
|
||||
+ ver, err := strconv.Atoi(rel)
|
||||
+ rel = ""
|
||||
+ return ver, err == nil
|
||||
+ }
|
||||
+ if major, ok = next(); !ok || rel == "" {
|
||||
+ return
|
||||
+ }
|
||||
+ if minor, ok = next(); !ok || rel == "" {
|
||||
+ return
|
||||
+ }
|
||||
+ patch, ok = next()
|
||||
+ return
|
||||
+}
|
||||
+
|
||||
+// GE checks if the running kernel version
|
||||
+// is greater than or equal to the provided version.
|
||||
+func (kv kernelVersion) GE(x, y int) bool {
|
||||
+ return kv.major > x || (kv.major == x && kv.minor >= y)
|
||||
+}
|
||||
diff --git a/src/runtime/os_linux32.go b/src/runtime/os_linux32.go
|
||||
index 16de6fb350f624..02cb18f32d57d2 100644
|
||||
--- a/src/runtime/os_linux32.go
|
||||
+++ b/src/runtime/os_linux32.go
|
||||
@@ -7,27 +7,25 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
- "internal/runtime/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
+func configure64bitsTimeOn32BitsArchitectures() {
|
||||
+ use64bitsTimeOn32bits = getKernelVersion().GE(5, 1)
|
||||
+}
|
||||
+
|
||||
//go:noescape
|
||||
func futex_time32(addr unsafe.Pointer, op int32, val uint32, ts *timespec32, addr2 unsafe.Pointer, val3 uint32) int32
|
||||
|
||||
//go:noescape
|
||||
func futex_time64(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
|
||||
|
||||
-var isFutexTime32bitOnly atomic.Bool
|
||||
+var use64bitsTimeOn32bits bool
|
||||
|
||||
//go:nosplit
|
||||
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32 {
|
||||
- if !isFutexTime32bitOnly.Load() {
|
||||
- ret := futex_time64(addr, op, val, ts, addr2, val3)
|
||||
- // futex_time64 is only supported on Linux 5.0+
|
||||
- if ret != -_ENOSYS {
|
||||
- return ret
|
||||
- }
|
||||
- isFutexTime32bitOnly.Store(true)
|
||||
+ if use64bitsTimeOn32bits {
|
||||
+ return futex_time64(addr, op, val, ts, addr2, val3)
|
||||
}
|
||||
// Downgrade ts.
|
||||
var ts32 timespec32
|
||||
@@ -45,17 +43,10 @@ func timer_settime32(timerid int32, flags int32, new, old *itimerspec32) int32
|
||||
//go:noescape
|
||||
func timer_settime64(timerid int32, flags int32, new, old *itimerspec) int32
|
||||
|
||||
-var isSetTime32bitOnly atomic.Bool
|
||||
-
|
||||
//go:nosplit
|
||||
func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
|
||||
- if !isSetTime32bitOnly.Load() {
|
||||
- ret := timer_settime64(timerid, flags, new, old)
|
||||
- // timer_settime64 is only supported on Linux 5.0+
|
||||
- if ret != -_ENOSYS {
|
||||
- return ret
|
||||
- }
|
||||
- isSetTime32bitOnly.Store(true)
|
||||
+ if use64bitsTimeOn32bits {
|
||||
+ return timer_settime64(timerid, flags, new, old)
|
||||
}
|
||||
|
||||
var newts, oldts itimerspec32
|
||||
@@ -73,6 +64,5 @@ func timer_settime(timerid int32, flags int32, new, old *itimerspec) int32 {
|
||||
old32 = &oldts
|
||||
}
|
||||
|
||||
- // Fall back to 32-bit timer
|
||||
return timer_settime32(timerid, flags, new32, old32)
|
||||
}
|
||||
diff --git a/src/runtime/os_linux64.go b/src/runtime/os_linux64.go
|
||||
index 7b70d80fbe5a89..f9571dd7586614 100644
|
||||
--- a/src/runtime/os_linux64.go
|
||||
+++ b/src/runtime/os_linux64.go
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
+func configure64bitsTimeOn32BitsArchitectures() {}
|
||||
+
|
||||
//go:noescape
|
||||
func futex(addr unsafe.Pointer, op int32, val uint32, ts *timespec, addr2 unsafe.Pointer, val3 uint32) int32
|
||||
|
||||
+54
-42
@@ -1,4 +1,8 @@
|
||||
Subject: [PATCH] internal/poll: move rsan to heap on windows
|
||||
From 1a44be4cecdc742ac6cce9825f9ffc19857c99f3 Mon Sep 17 00:00:00 2001
|
||||
From: database64128 <free122448@hotmail.com>
|
||||
Date: Mon, 9 Mar 2026 16:25:16 +0800
|
||||
Subject: [PATCH] [release-branch.go1.26] internal/poll: move rsan to heap on
|
||||
windows
|
||||
|
||||
According to https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecvfrom,
|
||||
the memory pointed to by lpFromlen must remain available during the
|
||||
@@ -14,20 +18,27 @@ with rsa in the same sync.Pool. The new wsaRsa struct is still in the
|
||||
same size class, so no additional overhead is introduced by this
|
||||
change.
|
||||
|
||||
Fixes #77975.
|
||||
Fixes #78041.
|
||||
|
||||
Change-Id: I5ffbccb332515116ddc03fb7c40ffc9293cad2ab
|
||||
Reviewed-on: https://go-review.googlesource.com/c/go/+/753040
|
||||
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
|
||||
Reviewed-by: Cherry Mui <cherryyz@google.com>
|
||||
Commit-Queue: Cherry Mui <cherryyz@google.com>
|
||||
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
|
||||
Reviewed-by: Damien Neil <dneil@google.com>
|
||||
Reviewed-on: https://go-review.googlesource.com/c/go/+/753480
|
||||
Reviewed-by: Mark Freeman <markfreeman@google.com>
|
||||
---
|
||||
Index: src/internal/poll/fd_windows.go
|
||||
IDEA additional info:
|
||||
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||
<+>UTF-8
|
||||
===================================================================
|
||||
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
--- a/src/internal/poll/fd_windows.go (revision 8149d992682ce76c6af804b507878e19fc966f7b)
|
||||
+++ b/src/internal/poll/fd_windows.go (date 1773058735706)
|
||||
@@ -149,7 +149,7 @@
|
||||
src/internal/poll/fd_windows.go | 94 +++++++++++++++++++++------------
|
||||
1 file changed, 59 insertions(+), 35 deletions(-)
|
||||
|
||||
diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
index 2ba967f990982f..26319548e3c310 100644
|
||||
--- a/src/internal/poll/fd_windows.go
|
||||
+++ b/src/internal/poll/fd_windows.go
|
||||
@@ -149,7 +149,7 @@ var wsaMsgPool = sync.Pool{
|
||||
|
||||
// newWSAMsg creates a new WSAMsg with the provided parameters.
|
||||
// Use [freeWSAMsg] to free it.
|
||||
-func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMsg {
|
||||
@@ -35,8 +46,8 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
// The returned object can't be allocated in the stack because it is accessed asynchronously
|
||||
// by Windows in between several system calls. If the stack frame is moved while that happens,
|
||||
// then Windows may access invalid memory.
|
||||
@@ -164,33 +164,45 @@
|
||||
Buf: unsafe.SliceData(oob),
|
||||
@@ -166,34 +166,46 @@ func newWSAMsg(p []byte, oob []byte, flags int, unconnected bool) *windows.WSAMs
|
||||
}
|
||||
}
|
||||
msg.Flags = uint32(flags)
|
||||
- if unconnected {
|
||||
@@ -48,7 +59,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
|
||||
func freeWSAMsg(msg *windows.WSAMsg) {
|
||||
// Clear pointers to buffers so they can be released by garbage collector.
|
||||
+ msg.Name = nil
|
||||
@@ -65,7 +76,7 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
- }
|
||||
wsaMsgPool.Put(msg)
|
||||
}
|
||||
|
||||
|
||||
+// wsaRsa bundles a [syscall.RawSockaddrAny] with its length for efficient caching.
|
||||
+//
|
||||
+// When used by WSARecvFrom, wsaRsa must be on the heap. See
|
||||
@@ -81,20 +92,21 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ return new(wsaRsa)
|
||||
},
|
||||
}
|
||||
+
|
||||
|
||||
+func newWSARsa() *wsaRsa {
|
||||
+ rsa := wsaRsaPool.Get().(*wsaRsa)
|
||||
+ rsa.name = syscall.RawSockaddrAny{}
|
||||
+ rsa.namelen = int32(unsafe.Sizeof(syscall.RawSockaddrAny{}))
|
||||
+ return rsa
|
||||
+}
|
||||
|
||||
+
|
||||
var operationPool = sync.Pool{
|
||||
New: func() any {
|
||||
@@ -737,19 +749,18 @@
|
||||
|
||||
return new(operation)
|
||||
@@ -739,19 +751,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) {
|
||||
|
||||
fd.pin('r', &buf[0])
|
||||
|
||||
|
||||
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
|
||||
+ rsa := newWSARsa()
|
||||
defer wsaRsaPool.Put(rsa)
|
||||
@@ -113,11 +125,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ sa, _ := rsa.name.Sockaddr()
|
||||
return n, sa, nil
|
||||
}
|
||||
|
||||
@@ -768,19 +779,18 @@
|
||||
|
||||
|
||||
@@ -770,19 +781,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error)
|
||||
|
||||
fd.pin('r', &buf[0])
|
||||
|
||||
|
||||
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
|
||||
+ rsa := newWSARsa()
|
||||
defer wsaRsaPool.Put(rsa)
|
||||
@@ -136,11 +148,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ rawToSockaddrInet4(&rsa.name, sa4)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -799,19 +809,18 @@
|
||||
|
||||
|
||||
@@ -801,19 +811,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error)
|
||||
|
||||
fd.pin('r', &buf[0])
|
||||
|
||||
|
||||
- rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny)
|
||||
+ rsa := newWSARsa()
|
||||
defer wsaRsaPool.Put(rsa)
|
||||
@@ -159,11 +171,11 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ rawToSockaddrInet6(&rsa.name, sa6)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -1371,7 +1380,9 @@
|
||||
|
||||
@@ -1373,7 +1382,9 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S
|
||||
p = p[:maxRW]
|
||||
}
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, flags, true)
|
||||
+ rsa := newWSARsa()
|
||||
+ defer wsaRsaPool.Put(rsa)
|
||||
@@ -171,10 +183,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
|
||||
@@ -1396,7 +1407,9 @@
|
||||
@@ -1398,7 +1409,9 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd
|
||||
p = p[:maxRW]
|
||||
}
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, flags, true)
|
||||
+ rsa := newWSARsa()
|
||||
+ defer wsaRsaPool.Put(rsa)
|
||||
@@ -182,10 +194,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
|
||||
@@ -1420,7 +1433,9 @@
|
||||
@@ -1422,7 +1435,9 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd
|
||||
p = p[:maxRW]
|
||||
}
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, flags, true)
|
||||
+ rsa := newWSARsa()
|
||||
+ defer wsaRsaPool.Put(rsa)
|
||||
@@ -193,10 +205,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil)
|
||||
@@ -1444,15 +1459,18 @@
|
||||
@@ -1446,15 +1461,18 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err
|
||||
}
|
||||
defer fd.writeUnlock()
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, 0, sa != nil)
|
||||
- defer freeWSAMsg(msg)
|
||||
+ var rsa *wsaRsa
|
||||
@@ -215,10 +227,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
|
||||
return qty, err
|
||||
@@ -1471,11 +1489,14 @@
|
||||
@@ -1473,11 +1491,14 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in
|
||||
}
|
||||
defer fd.writeUnlock()
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, 0, sa != nil)
|
||||
- defer freeWSAMsg(msg)
|
||||
+ var rsa *wsaRsa
|
||||
@@ -233,10 +245,10 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
|
||||
return qty, err
|
||||
@@ -1494,11 +1515,14 @@
|
||||
@@ -1496,11 +1517,14 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
|
||||
}
|
||||
defer fd.writeUnlock()
|
||||
|
||||
|
||||
- msg := newWSAMsg(p, oob, 0, sa != nil)
|
||||
- defer freeWSAMsg(msg)
|
||||
+ var rsa *wsaRsa
|
||||
@@ -250,4 +262,4 @@ diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go
|
||||
+ defer freeWSAMsg(msg)
|
||||
n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) {
|
||||
err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil)
|
||||
return qty, err
|
||||
return qty, err
|
||||
Vendored
+13
-7
@@ -173,14 +173,13 @@ jobs:
|
||||
|
||||
- name: Set up Go1.26 loongarch abi1
|
||||
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
||||
run: |
|
||||
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0/go1.26.0.linux-amd64-abi1.tar.gz
|
||||
mkdir -p $HOME/usr/local
|
||||
tar zxf go1.26.0.linux-amd64-abi1.tar.gz -C $HOME/usr/local
|
||||
echo "$HOME/usr/local/go/bin" >> $GITHUB_PATH
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||
with:
|
||||
go-download-base-url: 'https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.26.0'
|
||||
go-version: 1.26.0
|
||||
|
||||
# TODO: remove after issue77731 merged, see: https://github.com/golang/go/issues/77731
|
||||
- name: Apply issue77731 for Golang1.26
|
||||
# TODO: remove after issue77731 fixed, see: https://github.com/golang/go/issues/77731
|
||||
- name: Fix issue77731 for Golang1.26
|
||||
if: ${{ matrix.jobs.goversion == '' }}
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
@@ -193,6 +192,13 @@ jobs:
|
||||
cd $(go env GOROOT)
|
||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77975.patch
|
||||
|
||||
# TODO: remove after issue77930 fixed, see: https://github.com/golang/go/issues/77930
|
||||
- name: Fix issue77930 for Golang1.26
|
||||
if: ${{ matrix.jobs.goversion == '' }}
|
||||
run: |
|
||||
cd $(go env GOROOT)
|
||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/issue77930.patch
|
||||
|
||||
# this patch file only works on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode"
|
||||
@@ -61,10 +64,29 @@ type inetDiagResponse struct {
|
||||
|
||||
func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string, error) {
|
||||
uid, inode, err := resolveSocketByNetlink(network, ip, srcPort)
|
||||
if runtime.GOOS == "android" {
|
||||
// on Android (especially recent releases), netlink INET_DIAG can fail or return UID 0 / empty process info for some apps
|
||||
// so trying fallback to resolve /proc/net/{tcp,tcp6,udp,udp6}
|
||||
if err != nil {
|
||||
uid, inode, err = resolveSocketByProcFS(network, ip, srcPort)
|
||||
} else if uid == 0 {
|
||||
pUID, pInode, pErr := resolveSocketByProcFS(network, ip, srcPort)
|
||||
if pErr == nil && pUID != 0 {
|
||||
uid, inode, err = pUID, pInode, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
pp, err := resolveProcessNameByProcSearch(inode, uid)
|
||||
if runtime.GOOS == "android" {
|
||||
// if inode-based /proc/<pid>/fd resolution fails but UID is known,
|
||||
// fall back to resolving the process/package name by UID (typical on Android where all app processes share one UID).
|
||||
if err != nil && uid != 0 {
|
||||
pp, err = resolveProcessNameByUID(uid)
|
||||
}
|
||||
}
|
||||
return uid, pp, err
|
||||
}
|
||||
|
||||
@@ -180,6 +202,47 @@ func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
||||
}
|
||||
|
||||
// resolveProcessNameByUID returns a process name for any process with uid.
|
||||
// On Android all processes of one app share the same UID; used when inode
|
||||
// lookup fails (socket closed / TIME_WAIT).
|
||||
func resolveProcessNameByUID(uid uint32) (string, error) {
|
||||
files, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if !f.IsDir() || !isPid(f.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
|
||||
processPath := filepath.Join("/proc", f.Name())
|
||||
if runtime.GOOS == "android" {
|
||||
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if name := splitCmdline(cmdline); name != "" {
|
||||
return name, nil
|
||||
}
|
||||
} else {
|
||||
if exe, err := os.Readlink(filepath.Join(processPath, "exe")); err == nil {
|
||||
return exe, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no process found with uid %d", uid)
|
||||
}
|
||||
|
||||
func splitCmdline(cmdline []byte) string {
|
||||
cmdline = bytes.Trim(cmdline, " ")
|
||||
|
||||
@@ -198,3 +261,196 @@ func isPid(s string) bool {
|
||||
return !unicode.IsDigit(r)
|
||||
}) == -1
|
||||
}
|
||||
|
||||
// resolveSocketByProcFS finds UID and inode from /proc/net/{tcp,tcp6,udp,udp6}.
|
||||
// In TUN mode metadata sourceIP is often the gateway (e.g. fake-ip range), not
|
||||
// the socket's real local address; we match by local port first and prefer
|
||||
// exact IP+port when it matches.
|
||||
func resolveSocketByProcFS(network string, ip netip.Addr, srcPort int) (uint32, uint32, error) {
|
||||
var proto string
|
||||
switch {
|
||||
case strings.HasPrefix(network, "tcp"):
|
||||
proto = "tcp"
|
||||
case strings.HasPrefix(network, "udp"):
|
||||
proto = "udp"
|
||||
default:
|
||||
return 0, 0, ErrInvalidNetwork
|
||||
}
|
||||
|
||||
targetPort := uint16(srcPort)
|
||||
unmapped := ip.Unmap()
|
||||
files := []string{"/proc/net/" + proto, "/proc/net/" + proto + "6"}
|
||||
|
||||
var bestUID, bestInode uint32
|
||||
found := false
|
||||
|
||||
for _, path := range files {
|
||||
isV6 := strings.HasSuffix(path, "6")
|
||||
|
||||
var matchIP netip.Addr
|
||||
if unmapped.Is4() {
|
||||
if isV6 {
|
||||
matchIP = netip.AddrFrom16(unmapped.As16())
|
||||
} else {
|
||||
matchIP = unmapped
|
||||
}
|
||||
} else {
|
||||
if !isV6 {
|
||||
continue
|
||||
}
|
||||
matchIP = unmapped
|
||||
}
|
||||
|
||||
uid, inode, exact, err := searchProcNetFileByPort(path, matchIP, targetPort)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if exact {
|
||||
return uid, inode, nil
|
||||
}
|
||||
|
||||
if !found || (bestUID == 0 && uid != 0) {
|
||||
bestUID = uid
|
||||
bestInode = inode
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return bestUID, bestInode, nil
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
// searchProcNetFileByPort scans /proc/net/* for local_address matching targetPort.
|
||||
// Exact IP+port wins; else port-only (skips inode==0 entries used by TIME_WAIT).
|
||||
func searchProcNetFileByPort(path string, targetIP netip.Addr, targetPort uint16) (uid, inode uint32, exact bool, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
isV6 := strings.HasSuffix(path, "6")
|
||||
scanner := bufio.NewScanner(f)
|
||||
|
||||
if !scanner.Scan() {
|
||||
return 0, 0, false, ErrNotFound
|
||||
}
|
||||
|
||||
var bestUID, bestInode uint32
|
||||
found := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
lineIP, linePort, parseErr := parseHexAddrPort(fields[1], isV6)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
if linePort != targetPort {
|
||||
continue
|
||||
}
|
||||
|
||||
lineUID, parseErr := strconv.ParseUint(fields[7], 10, 32)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
lineInode, parseErr := strconv.ParseUint(fields[9], 10, 32)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if lineIP == targetIP {
|
||||
return uint32(lineUID), uint32(lineInode), true, nil
|
||||
}
|
||||
|
||||
if lineInode == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !found || (bestUID == 0 && lineUID != 0) {
|
||||
bestUID = uint32(lineUID)
|
||||
bestInode = uint32(lineInode)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
return bestUID, bestInode, false, nil
|
||||
}
|
||||
return 0, 0, false, ErrNotFound
|
||||
}
|
||||
|
||||
func parseHexAddrPort(s string, isV6 bool) (netip.Addr, uint16, error) {
|
||||
colon := strings.IndexByte(s, ':')
|
||||
if colon < 0 {
|
||||
return netip.Addr{}, 0, fmt.Errorf("invalid addr:port: %s", s)
|
||||
}
|
||||
|
||||
port64, err := strconv.ParseUint(s[colon+1:], 16, 16)
|
||||
if err != nil {
|
||||
return netip.Addr{}, 0, err
|
||||
}
|
||||
|
||||
var addr netip.Addr
|
||||
if isV6 {
|
||||
addr, err = parseHexIPv6(s[:colon])
|
||||
} else {
|
||||
addr, err = parseHexIPv4(s[:colon])
|
||||
}
|
||||
return addr, uint16(port64), err
|
||||
}
|
||||
|
||||
func parseHexIPv4(s string) (netip.Addr, error) {
|
||||
if len(s) != 8 {
|
||||
return netip.Addr{}, fmt.Errorf("invalid ipv4 hex len: %d", len(s))
|
||||
}
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
var ip [4]byte
|
||||
if littleEndian {
|
||||
ip[0], ip[1], ip[2], ip[3] = b[3], b[2], b[1], b[0]
|
||||
} else {
|
||||
copy(ip[:], b)
|
||||
}
|
||||
return netip.AddrFrom4(ip), nil
|
||||
}
|
||||
|
||||
func parseHexIPv6(s string) (netip.Addr, error) {
|
||||
if len(s) != 32 {
|
||||
return netip.Addr{}, fmt.Errorf("invalid ipv6 hex len: %d", len(s))
|
||||
}
|
||||
var ip [16]byte
|
||||
for i := 0; i < 4; i++ {
|
||||
b, err := hex.DecodeString(s[i*8 : (i+1)*8])
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
if littleEndian {
|
||||
ip[i*4+0] = b[3]
|
||||
ip[i*4+1] = b[2]
|
||||
ip[i*4+2] = b[1]
|
||||
ip[i*4+3] = b[0]
|
||||
} else {
|
||||
copy(ip[i*4:(i+1)*4], b)
|
||||
}
|
||||
}
|
||||
return netip.AddrFrom16(ip), nil
|
||||
}
|
||||
|
||||
var littleEndian = func() bool {
|
||||
x := uint32(0x01020304)
|
||||
return *(*byte)(unsafe.Pointer(&x)) == 0x04
|
||||
}()
|
||||
|
||||
@@ -58,3 +58,4 @@ endef
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
$(eval $(call BuildPackage,luci-app-adguardhome))
|
||||
|
||||
+6
-16
@@ -1,24 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// NOTE: luci-app-adguardhome 使用多页面 CBI 模板架构,luci 23.05+ 通过 menu.d alias 跳转至 base 子页。
|
||||
// 此 JS 文件不直接调用,保留供参考。
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'get_status',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
'require form';
|
||||
|
||||
return view.extend({
|
||||
load: function() {},
|
||||
|
||||
render: function() {
|
||||
var m = new form.Map('config', _('Settings'));
|
||||
var s = m.section(form.NamedSection, 'main', 'main', _('Configuration'));
|
||||
s.anonymous = true;
|
||||
|
||||
var o = s.option(form.Value, 'name', _('Name'));
|
||||
|
||||
return m.render();
|
||||
// 实际界面由 luasrc/model/cbi/AdGuardHome/ 下多个 CBI 模型提供
|
||||
return E('p', {}, _('Loading…'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,19 +5,23 @@ local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
function index()
|
||||
local page = entry({"admin", "services", "AdGuardHome"},
|
||||
alias("admin", "services", "AdGuardHome", "base"),
|
||||
_("AdGuard Home"), 10)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-adguardhome" }
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
|
||||
if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-adguardhome.json") then
|
||||
local page = entry({"admin", "services", "AdGuardHome"},
|
||||
alias("admin", "services", "AdGuardHome", "base"),
|
||||
_("AdGuard Home"), 10)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-adguardhome" }
|
||||
|
||||
entry({"admin", "services", "AdGuardHome", "base"},
|
||||
cbi("AdGuardHome/base"), _("Base Setting"), 1).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "log"},
|
||||
form("AdGuardHome/log"), _("Log"), 2).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "manual"},
|
||||
cbi("AdGuardHome/manual"), _("Manual Config"), 3).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "base"},
|
||||
cbi("AdGuardHome/base"), _("Base Setting"), 1).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "log"},
|
||||
form("AdGuardHome/log"), _("Log"), 2).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "manual"},
|
||||
cbi("AdGuardHome/manual"), _("Manual Config"), 3).leaf = true
|
||||
end
|
||||
|
||||
-- API 路由在新旧版本均需注册
|
||||
entry({"admin", "services", "AdGuardHome", "status"},
|
||||
call("act_status"), nil).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "check"},
|
||||
@@ -80,7 +84,9 @@ function act_status()
|
||||
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
|
||||
|
||||
if fs.access(binpath) then
|
||||
result.running = (luci.sys.call("pgrep -f '" .. binpath .. "' >/dev/null 2>&1") == 0)
|
||||
-- Security fix: binpath 来自 UCI 用户数据,用单引号包裹防止命令注入
|
||||
local safe_bin = binpath:gsub("'", "'\\''")
|
||||
result.running = (luci.sys.call("pgrep -f '" .. safe_bin .. "' >/dev/null 2>&1") == 0)
|
||||
else
|
||||
result.running = false
|
||||
end
|
||||
@@ -105,6 +111,7 @@ function do_update()
|
||||
local script = "/usr/share/AdGuardHome/update_core.sh"
|
||||
if fs.access("/var/run/update_core") then
|
||||
if arg == "force" then
|
||||
-- Security fix: script 路径是固定常量,arg 只能是 "force" 或 "",无注入风险
|
||||
luci.sys.exec("pkill -f '" .. script .. "' 2>/dev/null; " .. script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
|
||||
end
|
||||
else
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"admin/services/AdGuardHome": {
|
||||
"title": "AdGuard Home",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "alias",
|
||||
"path": "admin/services/AdGuardHome/base"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/base": {
|
||||
"title": "Base Setting",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "AdGuardHome/base"
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/log": {
|
||||
"title": "Log",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "form",
|
||||
"path": "AdGuardHome/log"
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/manual": {
|
||||
"title": "Manual Config",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "AdGuardHome/manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (C) 2019 sirpdboy <https://github.com/sirpdboy/luci-app-advanced/>
|
||||
# Maintained by kenzok78
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-advanced
|
||||
PKG_VERSION:=1.20
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=kenzok78 <https://github.com/kenzok78>
|
||||
|
||||
LUCI_TITLE:=LuCI Support for Advanced Settings and File Manager
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(call Package/$(PKG_NAME)/install/default,$(1))
|
||||
|
||||
$(INSTALL_DIR) $(1)/bin
|
||||
$(INSTALL_BIN) ./root/bin/* $(1)/bin/
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,114 @@
|
||||
# luci-app-advanced
|
||||
|
||||
[](LICENSE)
|
||||
[](https://github.com/kenzok78/luci-app-advanced/pulls)
|
||||
[](https://github.com/kenzok78/luci-app-advanced/releases)
|
||||
|
||||
LuCI 高级设置插件,提供系统配置、网络设置、防火墙等功能的管理界面。
|
||||
|
||||
<small>
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 系统高级设置
|
||||
- 网络配置管理
|
||||
- 防火墙规则配置
|
||||
- DHCP 设置
|
||||
- 文件浏览器
|
||||
- 文件管理器
|
||||
|
||||
## 系统要求
|
||||
|
||||
- OpenWrt 23.* 或更高版本
|
||||
- LuCI 23.* + Web 界面
|
||||
|
||||
## 安装
|
||||
|
||||
### 从源码编译
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kenzok78/luci-app-advanced.git
|
||||
mv luci-app-advanced /path/to/openwrt/package/feeds/luci/
|
||||
make package/luci-app-advanced/compile V=99
|
||||
```
|
||||
|
||||
### 在线安装
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install luci-app-advanced
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
1. 登录 LuCI 管理界面
|
||||
2. 进入 **系统 → 高级设置**
|
||||
3. 根据需要进行配置
|
||||
4. 保存并应用
|
||||
|
||||
## 代码优化
|
||||
|
||||
### 修复的问题
|
||||
|
||||
- uci-defaults 脚本:添加文件存在性检查,避免文件不存在时报错
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
luci-app-advanced/
|
||||
├── htdocs/
|
||||
│ └── luci-static/
|
||||
│ └── resources/
|
||||
│ └── fileassistant/
|
||||
│ ├── fb.js
|
||||
│ ├── fb.css
|
||||
│ ├── folder-icon.png
|
||||
│ ├── file-icon.png
|
||||
│ └── link-icon.png
|
||||
├── luasrc/
|
||||
│ ├── controller/
|
||||
│ │ ├── advanced.lua
|
||||
│ │ └── fileassistant.lua
|
||||
│ ├── model/
|
||||
│ │ └── cbi/
|
||||
│ │ └── advanced.lua
|
||||
│ └── view/
|
||||
│ ├── filebrowser.htm
|
||||
│ └── fileassistant.htm
|
||||
├── root/
|
||||
│ ├── bin/
|
||||
│ │ ├── normalmode
|
||||
│ │ ├── nuc
|
||||
│ │ ├── ipmode4
|
||||
│ │ └── ipmode6
|
||||
│ ├── etc/
|
||||
│ │ ├── config/
|
||||
│ │ │ └── advanced
|
||||
│ │ └── uci-defaults/
|
||||
│ │ └── 40_luci-fb
|
||||
│ └── usr/
|
||||
│ └── share/
|
||||
│ └── rpcd/
|
||||
│ └── acl.d/
|
||||
│ └── luci-app-advanced.json
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
Apache License 2.0
|
||||
|
||||
## 致谢
|
||||
|
||||
- 原始项目:[sirpdboy/luci-app-advanced](https://github.com/sirpdboy/luci-app-advanced)
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.20 (2026-03-24)
|
||||
|
||||
- 标准化代码结构
|
||||
- 修复 uci-defaults 脚本
|
||||
- 添加中文 README
|
||||
|
||||
</small>
|
||||
@@ -0,0 +1,68 @@
|
||||
.fb-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.fb-container .cbi-button {
|
||||
height: 2.6rem;
|
||||
}
|
||||
.fb-container .cbi-input-text {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.fb-container .panel-title {
|
||||
padding-bottom: 0;
|
||||
width: 50%;
|
||||
border-bottom: none;
|
||||
}
|
||||
.fb-container .panel-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
.fb-container .upload-container {
|
||||
display: none;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.fb-container .upload-file {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
.fb-container .cbi-value-field {
|
||||
text-align: left;
|
||||
}
|
||||
.fb-container .parent-icon strong {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.fb-container td[class$="-icon"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fb-container .file-icon, .fb-container .folder-icon, .fb-container .link-icon {
|
||||
position: relative;
|
||||
}
|
||||
.fb-container .file-icon:before, .fb-container .folder-icon:before, .fb-container .link-icon:before {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
content: '';
|
||||
background-size: contain;
|
||||
margin: 0 0.5rem 0 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.fb-container .file-icon:before {
|
||||
background-image: url(file-icon.png);
|
||||
}
|
||||
.fb-container .folder-icon:before {
|
||||
background-image: url(folder-icon.png);
|
||||
}
|
||||
.fb-container .link-icon:before {
|
||||
background-image: url(link-icon.png);
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
.fb-container .upload-file {
|
||||
width: 14.6rem;
|
||||
}
|
||||
.fb-container .cbi-value-owner,
|
||||
.fb-container .cbi-value-perm {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
String.prototype.replaceAll = function(search, replacement) {
|
||||
var target = this;
|
||||
return target.replace(new RegExp(search, 'g'), replacement);
|
||||
};
|
||||
(function () {
|
||||
var iwxhr = new XHR();
|
||||
var listElem = document.getElementById("list-content");
|
||||
listElem.onclick = handleClick;
|
||||
var currentPath;
|
||||
var pathElem = document.getElementById("current-path");
|
||||
pathElem.onblur = function () {
|
||||
update_list(this.value.trim());
|
||||
};
|
||||
pathElem.onkeyup = function (evt) {
|
||||
if (evt.keyCode == 13) {
|
||||
this.blur();
|
||||
}
|
||||
};
|
||||
function removePath(filename, isdir) {
|
||||
var c = confirm('你确定要删除 ' + filename + ' 吗?');
|
||||
if (c) {
|
||||
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/delete',
|
||||
{
|
||||
path: concatPath(currentPath, filename),
|
||||
isdir: isdir
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, currentPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function installPath(filename, isdir) {
|
||||
if (isdir === "1") {
|
||||
alert('这是一个目录,请选择 ipk 文件进行安装!');
|
||||
return;
|
||||
}
|
||||
var isipk = isIPK(filename);
|
||||
if (isipk === 0) {
|
||||
alert('只允许安装 ipk 格式的文件!');
|
||||
return;
|
||||
}
|
||||
var c = confirm('你确定要安装 ' + filename + ' 吗?');
|
||||
if (c) {
|
||||
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/install',
|
||||
{
|
||||
filepath: concatPath(currentPath, filename),
|
||||
isdir: isdir
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
location.reload();
|
||||
alert('安装成功!');
|
||||
} else {
|
||||
alert('安装失败,请检查文件格式!');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isIPK(filename) {
|
||||
var index= filename.lastIndexOf(".");
|
||||
var ext = filename.substr(index+1);
|
||||
if (ext === 'ipk') {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function renamePath(filename) {
|
||||
var newname = prompt('请输入新的文件名:', filename);
|
||||
if (newname) {
|
||||
newname = newname.trim();
|
||||
if (newname != filename) {
|
||||
var newpath = concatPath(currentPath, newname);
|
||||
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/rename',
|
||||
{
|
||||
filepath: concatPath(currentPath, filename),
|
||||
newpath: newpath
|
||||
},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, currentPath);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openpath(filename, dirname) {
|
||||
dirname = dirname || currentPath;
|
||||
window.open('/cgi-bin/luci/admin/system/fileassistant/open?path='
|
||||
+ encodeURIComponent(dirname) + '&filename='
|
||||
+ encodeURIComponent(filename));
|
||||
}
|
||||
|
||||
function getFileElem(elem) {
|
||||
if (elem.className.indexOf('-icon') > -1) {
|
||||
return elem;
|
||||
}
|
||||
else if (elem.parentNode.className.indexOf('-icon') > -1) {
|
||||
return elem.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function concatPath(path, filename) {
|
||||
if (path === '/') {
|
||||
return path + filename;
|
||||
}
|
||||
else {
|
||||
return path.replace(/\/$/, '') + '/' + filename;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(evt) {
|
||||
var targetElem = evt.target;
|
||||
var infoElem;
|
||||
if (targetElem.className.indexOf('cbi-button-remove') > -1) {
|
||||
infoElem = targetElem.parentNode.parentNode;
|
||||
removePath(infoElem.dataset['filename'] , infoElem.dataset['isdir'])
|
||||
}
|
||||
else if (targetElem.className.indexOf('cbi-button-install') > -1) {
|
||||
infoElem = targetElem.parentNode.parentNode;
|
||||
installPath(infoElem.dataset['filename'] , infoElem.dataset['isdir'])
|
||||
}
|
||||
else if (targetElem.className.indexOf('cbi-button-edit') > -1) {
|
||||
renamePath(targetElem.parentNode.parentNode.dataset['filename']);
|
||||
}
|
||||
else if (targetElem = getFileElem(targetElem)) {
|
||||
if (targetElem.className.indexOf('parent-icon') > -1) {
|
||||
update_list(currentPath.replace(/\/[^/]+($|\/$)/, ''));
|
||||
}
|
||||
else if (targetElem.className.indexOf('file-icon') > -1) {
|
||||
openpath(targetElem.parentNode.dataset['filename']);
|
||||
}
|
||||
else if (targetElem.className.indexOf('link-icon') > -1) {
|
||||
infoElem = targetElem.parentNode;
|
||||
var filepath = infoElem.dataset['linktarget'];
|
||||
if (filepath) {
|
||||
if (infoElem.dataset['isdir'] === "1") {
|
||||
update_list(filepath);
|
||||
}
|
||||
else {
|
||||
var lastSlash = filepath.lastIndexOf('/');
|
||||
openpath(filepath.substring(lastSlash + 1), filepath.substring(0, lastSlash));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (targetElem.className.indexOf('folder-icon') > -1) {
|
||||
update_list(concatPath(currentPath, targetElem.parentNode.dataset['filename']))
|
||||
}
|
||||
}
|
||||
}
|
||||
function refresh_list(filenames, path) {
|
||||
var listHtml = '<table class="cbi-section-table"><tbody>';
|
||||
if (path !== '/') {
|
||||
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-2"><td class="parent-icon" colspan="6"><strong>..返回上级目录</strong></td></tr>';
|
||||
}
|
||||
if (filenames) {
|
||||
for (var i = 0; i < filenames.length; i++) {
|
||||
var line = filenames[i];
|
||||
if (line) {
|
||||
var f = line.match(/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([\S\s]+)/);
|
||||
var isLink = f[1][0] === 'z' || f[1][0] === 'l' || f[1][0] === 'x';
|
||||
var o = {
|
||||
displayname: f[9],
|
||||
filename: isLink ? f[9].split(' -> ')[0] : f[9],
|
||||
perms: f[1],
|
||||
date: f[7] + ' ' + f[6] + ' ' + f[8],
|
||||
size: f[5],
|
||||
owner: f[3],
|
||||
icon: (f[1][0] === 'd') ? "folder-icon" : (isLink ? "link-icon" : "file-icon")
|
||||
};
|
||||
|
||||
var install_btn = ' <button class="cbi-button cbi-button-install" style="visibility: hidden;">安装</button>';
|
||||
var index= o.filename.lastIndexOf(".");
|
||||
var ext = o.filename.substr(index+1);
|
||||
if (ext === 'ipk') {
|
||||
install_btn = ' <button class="cbi-button cbi-button-install">安装</button>';
|
||||
}
|
||||
|
||||
listHtml += '<tr class="cbi-section-table-row cbi-rowstyle-' + (1 + i%2)
|
||||
+ '" data-filename="' + o.filename + '" data-isdir="' + Number(f[1][0] === 'd' || f[1][0] === 'z') + '"'
|
||||
+ ((f[1][0] === 'z' || f[1][0] === 'l') ? (' data-linktarget="' + f[9].split(' -> ')[1]) : '')
|
||||
+ '">'
|
||||
+ '<td class="cbi-value-field ' + o.icon + '">'
|
||||
+ '<strong>' + o.displayname + '</strong>'
|
||||
+ '</td>'
|
||||
|
||||
+ '<td class="cbi-value-field cbi-value-date">'+o.date+'</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-size">'+o.size+'</td>'
|
||||
+ '<td class="cbi-value-field cbi-value-perm">'+o.perms+'</td>'
|
||||
+ '<td class="cbi-section-table-cell">\
|
||||
<button class="cbi-button cbi-button-edit">重命名</button>\
|
||||
<button class="cbi-button cbi-button-remove">删除</button>'
|
||||
+ install_btn
|
||||
+ '</td>'
|
||||
+ '</tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
listHtml += "</table>";
|
||||
listElem.innerHTML = listHtml;
|
||||
}
|
||||
function update_list(path, opt) {
|
||||
opt = opt || {};
|
||||
path = concatPath(path, '');
|
||||
if (currentPath != path) {
|
||||
iwxhr.get('/cgi-bin/luci/admin/system/fileassistant/list',
|
||||
{path: path},
|
||||
function (x, res) {
|
||||
if (res.ec === 0) {
|
||||
refresh_list(res.data, path);
|
||||
}
|
||||
else {
|
||||
refresh_list([], path);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!opt.popState) {
|
||||
history.pushState({path: path}, null, '?path=' + path);
|
||||
}
|
||||
currentPath = path;
|
||||
pathElem.value = currentPath;
|
||||
}
|
||||
};
|
||||
|
||||
var uploadToggle = document.getElementById('upload-toggle');
|
||||
var uploadContainer = document.getElementById('upload-container');
|
||||
var isUploadHide = true;
|
||||
uploadToggle.onclick = function() {
|
||||
if (isUploadHide) {
|
||||
uploadContainer.style.display = 'inline-flex';
|
||||
}
|
||||
else {
|
||||
uploadContainer.style.display = 'none';
|
||||
}
|
||||
isUploadHide = !isUploadHide;
|
||||
};
|
||||
var uploadBtn = uploadContainer.getElementsByClassName('cbi-input-apply')[0];
|
||||
uploadBtn.onclick = function (evt) {
|
||||
var uploadinput = document.getElementById('upload-file');
|
||||
var fullPath = uploadinput.value;
|
||||
if (!fullPath) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
else {
|
||||
var formData = new FormData();
|
||||
var startIndex = (fullPath.indexOf('\\') >= 0 ? fullPath.lastIndexOf('\\') : fullPath.lastIndexOf('/'));
|
||||
formData.append('upload-filename', fullPath.substring(startIndex + 1));
|
||||
formData.append('upload-dir', concatPath(currentPath, ''));
|
||||
formData.append('upload-file', uploadinput.files[0]);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/cgi-bin/luci/admin/system/fileassistant/upload", true);
|
||||
xhr.onload = function() {
|
||||
if (xhr.status == 200) {
|
||||
var res = JSON.parse(xhr.responseText);
|
||||
refresh_list(res.data, currentPath);
|
||||
uploadinput.value = '';
|
||||
}
|
||||
else {
|
||||
alert('上传失败,请稍后再试...');
|
||||
}
|
||||
};
|
||||
xhr.send(formData);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(evt) {
|
||||
var initPath = '/';
|
||||
if (/path=([/\w]+)/.test(location.search)) {
|
||||
initPath = RegExp.$1;
|
||||
}
|
||||
update_list(initPath, {popState: true});
|
||||
});
|
||||
window.addEventListener('popstate', function (evt) {
|
||||
var path = '/';
|
||||
if (evt.state && evt.state.path) {
|
||||
path = evt.state.path;
|
||||
}
|
||||
update_list(path, {popState: true});
|
||||
});
|
||||
|
||||
})();
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,13 @@
|
||||
module("luci.controller.advanced",package.seeall)
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/advanced")then
|
||||
return
|
||||
end
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复
|
||||
if nixio.fs.access("/usr/share/luci/menu.d/luci-app-advanced.json")then
|
||||
return
|
||||
end
|
||||
local e
|
||||
e=entry({"admin","system","advanced"},cbi("advanced"),_("高级设置"),60)
|
||||
e.dependent=true
|
||||
end
|
||||
@@ -0,0 +1,224 @@
|
||||
module("luci.controller.fileassistant", package.seeall)
|
||||
|
||||
local util = require "luci.util"
|
||||
|
||||
function index()
|
||||
-- API 路由前置,供页面 XHR 使用
|
||||
local function leaf(name, func)
|
||||
entry({"admin", "system", "fileassistant", name}, call(func), nil).leaf = true
|
||||
end
|
||||
leaf("list", "fileassistant_list")
|
||||
leaf("open", "fileassistant_open")
|
||||
leaf("delete", "fileassistant_delete")
|
||||
leaf("rename", "fileassistant_rename")
|
||||
leaf("upload", "fileassistant_upload")
|
||||
leaf("install", "fileassistant_install")
|
||||
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复
|
||||
if nixio.fs.access("/usr/share/luci/menu.d/luci-app-advanced.json") then
|
||||
return
|
||||
end
|
||||
|
||||
local page
|
||||
page = entry({"admin", "system", "fileassistant"}, template("fileassistant"), _("文件管理"), 84)
|
||||
page.i18n = "base"
|
||||
page.dependent = true
|
||||
end
|
||||
|
||||
function list_response(path, success)
|
||||
luci.http.prepare_content("application/json")
|
||||
local result
|
||||
if success then
|
||||
local rv = scandir(path)
|
||||
result = {
|
||||
ec = 0,
|
||||
data = rv
|
||||
}
|
||||
else
|
||||
result = {
|
||||
ec = 1
|
||||
}
|
||||
end
|
||||
luci.http.write_json(result)
|
||||
end
|
||||
|
||||
function fileassistant_list()
|
||||
local path = luci.http.formvalue("path")
|
||||
list_response(path, true)
|
||||
end
|
||||
|
||||
function fileassistant_open()
|
||||
local path = luci.http.formvalue("path")
|
||||
local filename = luci.http.formvalue("filename")
|
||||
local io = require "io"
|
||||
local mime = to_mime(filename)
|
||||
|
||||
file = path..filename
|
||||
|
||||
local download_fpi = io.open(file, "r")
|
||||
luci.http.header('Content-Disposition', 'inline; filename="'..filename..'"' )
|
||||
luci.http.prepare_content(mime)
|
||||
luci.ltn12.pump.all(luci.ltn12.source.file(download_fpi), luci.http.write)
|
||||
end
|
||||
|
||||
function fileassistant_delete()
|
||||
local path = luci.http.formvalue("path")
|
||||
local isdir = luci.http.formvalue("isdir")
|
||||
path = path:gsub("<>", "/")
|
||||
local success
|
||||
if isdir then
|
||||
success = os.execute('rm -r ' .. util.shellquote(path)) == 0
|
||||
else
|
||||
success = os.remove(path)
|
||||
end
|
||||
list_response(nixio.fs.dirname(path), success)
|
||||
end
|
||||
|
||||
function fileassistant_rename()
|
||||
local filepath = luci.http.formvalue("filepath")
|
||||
local newpath = luci.http.formvalue("newpath")
|
||||
local success = os.execute('mv ' .. util.shellquote(filepath) .. ' ' .. util.shellquote(newpath)) == 0
|
||||
list_response(nixio.fs.dirname(filepath), success)
|
||||
end
|
||||
|
||||
function fileassistant_install()
|
||||
local filepath = luci.http.formvalue("filepath")
|
||||
local isdir = luci.http.formvalue("isdir")
|
||||
local ext = filepath:match(".+%%.(%w+)$")
|
||||
filepath = filepath:gsub("<>", "/")
|
||||
local success
|
||||
if isdir == "1" then
|
||||
success = false
|
||||
elseif ext == "ipk" then
|
||||
success = installIPK(filepath)
|
||||
else
|
||||
success = false
|
||||
end
|
||||
list_response(nixio.fs.dirname(filepath), success)
|
||||
end
|
||||
|
||||
function installIPK(filepath)
|
||||
luci.sys.exec('opkg --force-depends install ' .. util.shellquote(filepath))
|
||||
luci.sys.exec('rm -rf /tmp/luci-*')
|
||||
return true
|
||||
end
|
||||
|
||||
function fileassistant_upload()
|
||||
local filecontent = luci.http.formvalue("upload-file")
|
||||
local filename = luci.http.formvalue("upload-filename")
|
||||
local uploaddir = luci.http.formvalue("upload-dir")
|
||||
local filepath = uploaddir..filename
|
||||
|
||||
local fp
|
||||
luci.http.setfilehandler(
|
||||
function(meta, chunk, eof)
|
||||
if not fp and meta and meta.name == "upload-file" then
|
||||
fp = io.open(filepath, "w")
|
||||
end
|
||||
if fp and chunk then
|
||||
fp:write(chunk)
|
||||
end
|
||||
if fp and eof then
|
||||
fp:close()
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
list_response(uploaddir, true)
|
||||
end
|
||||
|
||||
function scandir(directory)
|
||||
local i, t, popen = 0, {}, io.popen
|
||||
local qdir = util.shellquote(directory)
|
||||
local pfile = popen("ls -lh " .. qdir .. " | egrep '^d' ; ls -lh " .. qdir .. " | egrep -v '^d|^l'")
|
||||
for fileinfo in pfile:lines() do
|
||||
i = i + 1
|
||||
t[i] = fileinfo
|
||||
end
|
||||
pfile:close()
|
||||
pfile = popen("ls -lh " .. qdir .. " | egrep '^l' ;")
|
||||
for fileinfo in pfile:lines() do
|
||||
i = i + 1
|
||||
linkindex, _, linkpath = string.find(fileinfo, "->%s+(.+)$")
|
||||
local finalpath;
|
||||
if string.sub(linkpath, 1, 1) == "/" then
|
||||
finalpath = linkpath
|
||||
else
|
||||
finalpath = nixio.fs.realpath(directory..linkpath)
|
||||
end
|
||||
local linktype;
|
||||
if not finalpath then
|
||||
finalpath = linkpath;
|
||||
linktype = 'x'
|
||||
elseif nixio.fs.stat(finalpath, "type") == "dir" then
|
||||
linktype = 'z'
|
||||
else
|
||||
linktype = 'l'
|
||||
end
|
||||
fileinfo = string.sub(fileinfo, 2, linkindex - 1)
|
||||
fileinfo = linktype..fileinfo.."-> "..finalpath
|
||||
t[i] = fileinfo
|
||||
end
|
||||
pfile:close()
|
||||
return t
|
||||
end
|
||||
|
||||
MIME_TYPES = {
|
||||
["txt"] = "text/plain";
|
||||
["conf"] = "text/plain";
|
||||
["ovpn"] = "text/plain";
|
||||
["log"] = "text/plain";
|
||||
["js"] = "text/javascript";
|
||||
["json"] = "application/json";
|
||||
["css"] = "text/css";
|
||||
["htm"] = "text/html";
|
||||
["html"] = "text/html";
|
||||
["patch"] = "text/x-patch";
|
||||
["c"] = "text/x-csrc";
|
||||
["h"] = "text/x-chdr";
|
||||
["o"] = "text/x-object";
|
||||
["ko"] = "text/x-object";
|
||||
|
||||
["bmp"] = "image/bmp";
|
||||
["gif"] = "image/gif";
|
||||
["png"] = "image/png";
|
||||
["jpg"] = "image/jpeg";
|
||||
["jpeg"] = "image/jpeg";
|
||||
["svg"] = "image/svg+xml";
|
||||
|
||||
["zip"] = "application/zip";
|
||||
["pdf"] = "application/pdf";
|
||||
["xml"] = "application/xml";
|
||||
["xsl"] = "application/xml";
|
||||
["doc"] = "application/msword";
|
||||
["ppt"] = "application/vnd.ms-powerpoint";
|
||||
["xls"] = "application/vnd.ms-excel";
|
||||
["odt"] = "application/vnd.oasis.opendocument.text";
|
||||
["odp"] = "application/vnd.oasis.opendocument.presentation";
|
||||
["pl"] = "application/x-perl";
|
||||
["sh"] = "application/x-shellscript";
|
||||
["php"] = "application/x-php";
|
||||
["deb"] = "application/x-deb";
|
||||
["iso"] = "application/x-cd-image";
|
||||
["tgz"] = "application/x-compressed-tar";
|
||||
|
||||
["mp3"] = "audio/mpeg";
|
||||
["ogg"] = "audio/x-vorbis+ogg";
|
||||
["wav"] = "audio/x-wav";
|
||||
|
||||
["mpg"] = "video/mpeg";
|
||||
["mpeg"] = "video/mpeg";
|
||||
["avi"] = "video/x-msvideo";
|
||||
}
|
||||
|
||||
function to_mime(filename)
|
||||
if type(filename) == "string" then
|
||||
local ext = filename:match("[^%.]+$")
|
||||
|
||||
if ext and MIME_TYPES[ext:lower()] then
|
||||
return MIME_TYPES[ext:lower()]
|
||||
end
|
||||
end
|
||||
|
||||
return "application/octet-stream"
|
||||
end
|
||||
@@ -0,0 +1,340 @@
|
||||
local e=require"nixio.fs"
|
||||
local t=require"luci.sys"
|
||||
local uci=luci.model.uci.cursor()
|
||||
m=Map("advanced",translate("高级进阶设置"),translate("<font color=\"Red\"><strong>配置文档是直接编辑的除非你知道自己在干什么,否则请不要轻易修改这些配置文档。配置不正确可能会导致不能开机等错误。</strong></font><br/>"))
|
||||
m.apply_on_parse=true
|
||||
s=m:section(TypedSection,"advanced")
|
||||
s.anonymous=true
|
||||
|
||||
if nixio.fs.access("/etc/dnsmasq.conf")then
|
||||
|
||||
s:tab("dnsmasqconf",translate("dnsmasq"),translate("本页是配置/etc/dnsmasq.conf的文档内容。应用保存后自动重启生效"))
|
||||
|
||||
conf=s:taboption("dnsmasqconf",Value,"dnsmasqconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/dnsmasq.conf")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/dnsmasq.conf",t)
|
||||
if(luci.sys.call("cmp -s /tmp/dnsmasq.conf /etc/dnsmasq.conf")==1)then
|
||||
e.writefile("/etc/dnsmasq.conf",t)
|
||||
luci.sys.call("/etc/init.d/dnsmasq restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/dnsmasq.conf")
|
||||
end
|
||||
end
|
||||
end
|
||||
if nixio.fs.access("/etc/config/network")then
|
||||
s:tab("netwrokconf",translate("网络"),translate("本页是配置/etc/config/network包含网络配置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("netwrokconf",Value,"netwrokconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/network")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/network",t)
|
||||
if(luci.sys.call("cmp -s /tmp/network /etc/config/network")==1)then
|
||||
e.writefile("/etc/config/network",t)
|
||||
luci.sys.call("/etc/init.d/network restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/network")
|
||||
end
|
||||
end
|
||||
end
|
||||
if nixio.fs.access("/etc/config/wireless")then
|
||||
s:tab("wirelessconf",translate("无线"), translate("本页是/etc/config/wireless的配置文件内容,应用保存后自动重启生效."))
|
||||
|
||||
conf=s:taboption("wirelessconf",Value,"wirelessconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/wireless")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/etc/config/wireless.tmp",t)
|
||||
if(luci.sys.call("cmp -s /etc/config/wireless.tmp /etc/config/wireless")==1)then
|
||||
e.writefile("/etc/config/wireless",t)
|
||||
luci.sys.call("wifi reload >/dev/null &")
|
||||
end
|
||||
e.remove("/etc/config/wireless.tmp")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/hosts")then
|
||||
s:tab("hostsconf",translate("hosts"),translate("本页是配置/etc/hosts的文档内容。应用保存后自动重启生效"))
|
||||
|
||||
conf=s:taboption("hostsconf",Value,"hostsconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/hosts")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/hosts.tmp",t)
|
||||
if(luci.sys.call("cmp -s /tmp/hosts.tmp /etc/hosts")==1)then
|
||||
e.writefile("/etc/hosts",t)
|
||||
luci.sys.call("/etc/init.d/dnsmasq restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/hosts.tmp")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/arpbind")then
|
||||
s:tab("arpbindconf",translate("ARP绑定"),translate("本页是配置/etc/config/arpbind包含APR绑定MAC地址文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("arpbindconf",Value,"arpbindconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/arpbind")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/arpbind",t)
|
||||
if(luci.sys.call("cmp -s /tmp/arpbind /etc/config/arpbind")==1)then
|
||||
e.writefile("/etc/config/arpbind",t)
|
||||
luci.sys.call("/etc/init.d/arpbind restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/arpbind")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/firewall")then
|
||||
s:tab("firewallconf",translate("防火墙"),translate("本页是配置/etc/config/firewall包含防火墙协议设置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("firewallconf",Value,"firewallconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/firewall")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/firewall",t)
|
||||
if(luci.sys.call("cmp -s /tmp/firewall /etc/config/firewall")==1)then
|
||||
e.writefile("/etc/config/firewall",t)
|
||||
luci.sys.call("/etc/init.d/firewall restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/firewall")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/mwan3")then
|
||||
s:tab("mwan3conf",translate("负载均衡"),translate("本页是配置/etc/config/mwan3包含负载均衡设置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("mwan3conf",Value,"mwan3conf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/mwan3")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/mwan3",t)
|
||||
if(luci.sys.call("cmp -s /tmp/mwan3 /etc/config/mwan3")==1)then
|
||||
e.writefile("/etc/config/mwan3",t)
|
||||
luci.sys.call("/etc/init.d/mwan3 restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/mwan3")
|
||||
end
|
||||
end
|
||||
end
|
||||
if nixio.fs.access("/etc/config/dhcp")then
|
||||
s:tab("dhcpconf",translate("DHCP"),translate("本页是配置/etc/config/DHCP包含机器名等设置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("dhcpconf",Value,"dhcpconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/dhcp")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/dhcp",t)
|
||||
if(luci.sys.call("cmp -s /tmp/dhcp /etc/config/dhcp")==1)then
|
||||
e.writefile("/etc/config/dhcp",t)
|
||||
luci.sys.call("/etc/init.d/network restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/dhcp")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/ddns")then
|
||||
s:tab("ddnsconf",translate("DDNS"),translate("本页是配置/etc/config/ddns包含动态域名设置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("ddnsconf",Value,"ddnsconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/ddns")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/ddns",t)
|
||||
if(luci.sys.call("cmp -s /tmp/ddns /etc/config/ddns")==1)then
|
||||
e.writefile("/etc/config/ddns",t)
|
||||
luci.sys.call("/etc/init.d/ddns restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/ddns")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/parentcontrol")then
|
||||
s:tab("parentcontrolconf",translate("家长控制"),translate("本页是配置/etc/config/parentcontrol包含家长控制配置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("parentcontrolconf",Value,"parentcontrolconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/parentcontrol")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/parentcontrol",t)
|
||||
if(luci.sys.call("cmp -s /tmp/parentcontrol /etc/config/parentcontrol")==1)then
|
||||
e.writefile("/etc/config/parentcontrol",t)
|
||||
luci.sys.call("/etc/init.d/parentcontrol restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/parentcontrol")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/autotimeset")then
|
||||
s:tab("autotimesetconf",translate("定时设置"),translate("本页是配置/etc/config/autotimeset包含定时设置任务配置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("autotimesetconf",Value,"autotimesetconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/autotimeset")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/autotimeset",t)
|
||||
if(luci.sys.call("cmp -s /tmp/autotimeset /etc/config/autotimeset")==1)then
|
||||
e.writefile("/etc/config/autotimeset",t)
|
||||
luci.sys.call("/etc/init.d/autotimeset restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/autotimeset")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/wolplus")then
|
||||
s:tab("wolplusconf",translate("网络唤醒"),translate("本页是配置/etc/config/wolplus包含网络唤醒配置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("wolplusconf",Value,"wolplusconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/wolplus")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/wolplus",t)
|
||||
if(luci.sys.call("cmp -s /tmp/wolplus /etc/config/wolplus")==1)then
|
||||
e.writefile("/etc/config/wolplus",t)
|
||||
luci.sys.call("/etc/init.d/wolplus restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/wolplus")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/smartdns")then
|
||||
s:tab("smartdnsconf",translate("SMARTDNS"),translate("本页是配置/etc/config/smartdns包含smartdns配置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("smartdnsconf",Value,"smartdnsconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/smartdns")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/smartdns",t)
|
||||
if(luci.sys.call("cmp -s /tmp/smartdns /etc/config/smartdns")==1)then
|
||||
e.writefile("/etc/config/smartdns",t)
|
||||
luci.sys.call("/etc/init.d/smartdns restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/smartdns")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/bypass")then
|
||||
s:tab("bypassconf",translate("BYPASS"),translate("本页是配置/etc/config/bypass包含bypass配置文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("bypassconf",Value,"bypassconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/bypass")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/bypass",t)
|
||||
if(luci.sys.call("cmp -s /tmp/bypass /etc/config/bypass")==1)then
|
||||
e.writefile("/etc/config/bypass",t)
|
||||
luci.sys.call("/etc/init.d/bypass restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/bypass")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if nixio.fs.access("/etc/config/openclash")then
|
||||
s:tab("openclashconf",translate("openclash"),translate("本页是配置/etc/config/openclash的文档内容。应用保存后自动重启生效"))
|
||||
conf=s:taboption("openclashconf",Value,"openclashconf",nil,translate("开头的数字符号(#)或分号的每一行(;)被视为注释;删除(;)启用指定选项。"))
|
||||
conf.template="cbi/tvalue"
|
||||
conf.rows=20
|
||||
conf.wrap="off"
|
||||
conf.cfgvalue=function(t,t)
|
||||
return e.readfile("/etc/config/openclash")or""
|
||||
end
|
||||
conf.write=function(a,a,t)
|
||||
if t then
|
||||
t=t:gsub("\r\n?","\n")
|
||||
e.writefile("/tmp/openclash",t)
|
||||
if(luci.sys.call("cmp -s /tmp/openclash /etc/config/openclash")==1)then
|
||||
e.writefile("/etc/config/openclash",t)
|
||||
luci.sys.call("/etc/init.d/openclash restart >/dev/null")
|
||||
end
|
||||
e.remove("/tmp/openclash")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,20 @@
|
||||
<%+header%>
|
||||
|
||||
<link rel="stylesheet" href="/luci-static/resources/fileassistant/fb.css?v=@ver">
|
||||
<h2 name="content">文件管理【集成上传删除及安装,非专业人员请谨慎操作】</h2>
|
||||
<fieldset class="cbi-section fb-container">
|
||||
<input id="current-path" type="text" class="current-path cbi-input-text" value="/"/>
|
||||
<div class="panel-container">
|
||||
<div class="panel-title">文件列表</div>
|
||||
<button id="upload-toggle" class="upload-toggle cbi-button cbi-button-edit">上传文件</button>
|
||||
</div>
|
||||
<div class="upload-container" id="upload-container">
|
||||
<input id="upload-file" name="upload-file" class="upload-file" type="file">
|
||||
<button type="button" class="cbi-button cbi-input-apply">执行上传</button>
|
||||
</div>
|
||||
<div id="list-content"></div>
|
||||
</fieldset>
|
||||
|
||||
<script src="/luci-static/resources/fileassistant/fb.js?v=@ver"></script>
|
||||
|
||||
<%+footer%>
|
||||
@@ -0,0 +1,20 @@
|
||||
<%+header%>
|
||||
|
||||
<link rel="stylesheet" href="/luci-static/resources/fb/fb.css?v=@ver">
|
||||
<h2 name="content">文件管理</h2>
|
||||
<fieldset class="cbi-section fb-container">
|
||||
<input id="current-path" type="text" class="current-path cbi-input-text" value="/"/>
|
||||
<div class="panel-container">
|
||||
<div class="panel-title">文件列表:</div>
|
||||
<button id="upload-toggle" class="upload-toggle cbi-button cbi-button-edit">上传</button>
|
||||
</div>
|
||||
<div class="upload-container" id="upload-container">
|
||||
<input id="upload-file" name="upload-file" class="upload-file" type="file">
|
||||
<button type="button" class="cbi-button cbi-input-apply">点我上传</button>
|
||||
</div>
|
||||
<div id="list-content"></div>
|
||||
</fieldset>
|
||||
|
||||
<script src="/luci-static/resources/fb/fb.js?v=@ver"></script>
|
||||
|
||||
<%+footer%>
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
uci set network.@globals[0].ula_prefix=''
|
||||
uci set network.lan.delegate='0'
|
||||
uci set network.wan.mtu=1460
|
||||
uci set network.wan.metric='41'
|
||||
uci set network.wan.delegate='0'
|
||||
uci set network.wan.ipv6='0'
|
||||
uci commit network
|
||||
uci set dhcp.@dnsmasq[0].cachesize='15000'
|
||||
uci set dhcp.@dnsmasq[0].min_ttl='3600'
|
||||
uci set dhcp.@dnsmasq[0].filter_aaaa='1'
|
||||
uci set dhcp.@dnsmasq[0].localservice='0'
|
||||
uci set dhcp.@dnsmasq[0].nonwildcard='0'
|
||||
uci set dhcp.@dnsmasq[0].rebind_protection='0'
|
||||
uci set dhcp.@dnsmasq[0].noresolv='0'
|
||||
uci set dhcp.lan.ra=''
|
||||
uci set dhcp.lan.ndp=''
|
||||
uci set dhcp.lan.dhcpv6=''
|
||||
uci set dhcp.lan.ignore='0'
|
||||
uci set dhcp.lan.ra_management='1'
|
||||
uci set dhcp.lan.ra_default='1'
|
||||
uci set dhcp.lan.force='1'
|
||||
uci commit dhcp
|
||||
sed -i "/list server/d" /etc/config/dhcp
|
||||
/etc/init.d/network restart
|
||||
/etc/init.d/dnsmasq restart
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci set dhcp.@dnsmasq[0].cachesize='15000'
|
||||
uci set dhcp.@dnsmasq[0].min_ttl='3600'
|
||||
uci set dhcp.@dnsmasq[0].filter_aaaa='0'
|
||||
uci set dhcp.@dnsmasq[0].localservice='0'
|
||||
uci set dhcp.@dnsmasq[0].nonwildcard='0'
|
||||
uci set dhcp.@dnsmasq[0].rebind_protection='0'
|
||||
uci set dhcp.@dnsmasq[0].noresolv='1'
|
||||
uci set dhcp.lan.ra='server'
|
||||
uci set dhcp.lan.ndp=''
|
||||
uci set dhcp.lan.dhcpv6=''
|
||||
uci set dhcp.lan.ignore='0'
|
||||
uci set dhcp.lan.ra_management='1'
|
||||
uci set dhcp.lan.ra_default='1'
|
||||
uci set dhcp.lan.force='1'
|
||||
uci commit dhcp
|
||||
uci set network.@globals[0].ula_prefix=''
|
||||
uci set network.lan.delegate='0'
|
||||
uci set network.wan.mtu=1460
|
||||
uci set network.wan.metric='41'
|
||||
uci set network.wan.delegate='0'
|
||||
uci set network.wan.ipv6='auto'
|
||||
uci commit network
|
||||
sed -i "/list server/d" /etc/config/dhcp
|
||||
/etc/init.d/network restart
|
||||
/etc/init.d/dnsmasq restart
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci set system.@system[0].hostname="Openwrt"
|
||||
uci commit
|
||||
|
||||
cat > /etc/config/network <<EOF
|
||||
config interface 'loopback'
|
||||
option ifname 'lo'
|
||||
option proto 'static'
|
||||
option ipaddr '127.0.0.1'
|
||||
option netmask '255.0.0.0'
|
||||
|
||||
config interface 'lan'
|
||||
option type 'bridge'
|
||||
option proto 'static'
|
||||
option ipaddr '192.168.1.1'
|
||||
option netmask '255.255.255.0'
|
||||
option ifname 'eth0 eth2 eth3'
|
||||
|
||||
config interface 'wan'
|
||||
option ifname 'eth1'
|
||||
option proto 'dhcp'
|
||||
option hostname 'Openwrt'
|
||||
EOF
|
||||
|
||||
sed -i '/REDIRECT --to-ports 53/d' /etc/firewall.user
|
||||
sed -i '/MASQUERADE/d' /etc/firewall.user
|
||||
echo "iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 53" >> /etc/firewall.user
|
||||
echo "iptables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 53" >> /etc/firewall.user
|
||||
reboot
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
ip=/usr/sbin/ip
|
||||
vconfig=/sbin/vconfig
|
||||
ifconfig=/sbin/ifconfig
|
||||
|
||||
uci set system.@system[0].hostname="Openwrt"
|
||||
uci commit
|
||||
cat > /etc/config/network <<EOF
|
||||
config interface 'loopback'
|
||||
option ifname 'lo'
|
||||
option proto 'static'
|
||||
option ipaddr '127.0.0.1'
|
||||
option netmask '255.0.0.0'
|
||||
|
||||
config interface 'lan'
|
||||
option type 'bridge'
|
||||
option ifname 'eth0 eth1 eth2 eth3 eth4 eth5 eth0.12 eth0.13 eth0.14 eth0.15 eth0.16 eth0.17 eth0.18'
|
||||
option proto 'static'
|
||||
option ipaddr '192.168.1.2'
|
||||
option netmask '255.255.255.0'
|
||||
option gateway '192.168.1.1'
|
||||
option dns '223.5.5.5'
|
||||
|
||||
config interface 'wan'
|
||||
option ifname 'eth0.11'
|
||||
option proto 'dhcp'
|
||||
option hostname 'Openwrt'
|
||||
EOF
|
||||
|
||||
#for eth1
|
||||
$ip link add link eth0 eth0.11 type vlan proto 802.1ad id 11
|
||||
$ifconfig eth0.11 hw ether 58:b0:35:86:cf:11
|
||||
$vconfig set_flag eth0.11 1 1
|
||||
$ifconfig eth0.11 up
|
||||
|
||||
#for eth2
|
||||
$ip link add link eth0 eth0.12 type vlan proto 802.1ad id 12
|
||||
$ifconfig eth0.12 hw ether 58:b0:35:86:cf:12
|
||||
$vconfig set_flag eth0.12 1 2
|
||||
$ifconfig eth0.12 up
|
||||
|
||||
#for eth3
|
||||
$ip link add link eth0 eth0.13 type vlan proto 802.1ad id 13
|
||||
$ifconfig eth0.13 hw ether 58:b0:35:86:cf:13
|
||||
$vconfig set_flag eth0.13 1 3
|
||||
$ifconfig eth0.13 up
|
||||
|
||||
#for eth4
|
||||
$ip link add link eth0 eth0.14 type vlan proto 802.1ad id 14
|
||||
$ifconfig eth0.14 hw ether 58:b0:35:86:cf:14
|
||||
$vconfig set_flag eth0.14 1 3
|
||||
$ifconfig eth0.14 up
|
||||
|
||||
#for eth5
|
||||
$ip link add link eth0 eth0.15 type vlan proto 802.1ad id 15
|
||||
$ifconfig eth0.15 hw ether 58:b0:35:86:cf:15
|
||||
$vconfig set_flag eth0.15 1 3
|
||||
$ifconfig eth0.15 up
|
||||
|
||||
#for eth6
|
||||
$ip link add link eth0 eth0.16 type vlan proto 802.1ad id 16
|
||||
$ifconfig eth0.16 hw ether 58:b0:35:86:cf:16
|
||||
$vconfig set_flag eth0.16 1 3
|
||||
$ifconfig eth0.16 up
|
||||
|
||||
#for eth7
|
||||
$ip link add link eth0 eth0.17 type vlan proto 802.1ad id 17
|
||||
$ifconfig eth0.17 hw ether 58:b0:35:86:cf:17
|
||||
$vconfig set_flag eth0.17 1 3
|
||||
$ifconfig eth0.17 up
|
||||
|
||||
#for eth8
|
||||
$ip link add link eth0 eth0.18 type vlan proto 802.1ad id 18
|
||||
$ifconfig eth0.18 hw ether 58:b0:35:86:cf:18
|
||||
$vconfig set_flag eth0.18 1 3
|
||||
$ifconfig eth0.18 up
|
||||
|
||||
sed -i '/MASQUERADE/d' /etc/firewall.user
|
||||
echo "iptables -t nat -I POSTROUTING -j MASQUERADE" >> /etc/firewall.user
|
||||
reboot
|
||||
@@ -0,0 +1,2 @@
|
||||
config advanced
|
||||
option enabled '1'
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
ver=$(date +%s)
|
||||
[ -f /usr/lib/lua/luci/view/filebrowser.htm ] && sed -i "s/@ver/$ver/g" /usr/lib/lua/luci/view/filebrowser.htm
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"admin/system/advanced": {
|
||||
"title": "高级设置",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "advanced"
|
||||
},
|
||||
"acl": {
|
||||
"anonymous": false
|
||||
}
|
||||
},
|
||||
"admin/system/fileassistant": {
|
||||
"title": "文件管理",
|
||||
"order": 84,
|
||||
"action": {
|
||||
"type": "template",
|
||||
"path": "fileassistant"
|
||||
},
|
||||
"acl": {
|
||||
"anonymous": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"luci-app-advanced": {
|
||||
"description": "Grant UCI access for luci-app-advanced",
|
||||
"read": {
|
||||
"uci": [ "advanced" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "advanced" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
<div align="center">
|
||||
</a><a href="https://github.com/gngpp/luci-app-design-config/releases">
|
||||
<img src="https://img.shields.io/github/release/gngpp/luci-app-design-config.svg?style=flat">
|
||||
</a><a href="https://github.com/gngpp/luci-app-design-config/releases">
|
||||
<img src="https://img.shields.io/github/downloads/gngpp/luci-app-design-config/total?style=flat">
|
||||
</a><a href="https://github.com/kenzok78/luci-design-bundle/releases">
|
||||
<img src="https://img.shields.io/github/release/kenzok78/luci-design-bundle.svg?style=flat">
|
||||
</a><a href="https://github.com/kenzok78/luci-design-bundle/releases">
|
||||
<img src="https://img.shields.io/github/downloads/kenzok78/luci-design-bundle/total?style=flat">
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
# luci-app-design-config
|
||||
Design Theme Config Plugin
|
||||
Design 主题配置插件
|
||||
|
||||
### Features
|
||||
- Support changing theme dark/light mode
|
||||
- Support display/hide navbar
|
||||
- Support for replacing commonly used proxy icons
|
||||
### 功能特性
|
||||
- 支持更改主题深色/浅色模式
|
||||
- 支持显示/隐藏导航栏
|
||||
- 支持更换常用的代理图标
|
||||
|
||||
### Compile
|
||||
### 编译
|
||||
|
||||
```
|
||||
git clone https://github.com/gngpp/luci-app-design-config.git package/luci-app-design-config
|
||||
make menuconfig # choose LUCI->Applications->luci-app-design-config
|
||||
git clone https://github.com/kenzok78/luci-design-bundle.git package/luci-app-design-config
|
||||
make menuconfig # 选择 LUCI->Applications->luci-app-design-config
|
||||
make V=s
|
||||
```
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-easymesh
|
||||
LUCI_TITLE:=LuCI Support for easymesh
|
||||
LUCI_DEPENDS:= +kmod-cfg80211 +batctl-default +kmod-batman-adv +wpad-mesh-openssl +dawn
|
||||
PKG_VERSION:=2.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=kenzok78 <https://github.com/kenzok78>
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@@ -1,10 +1,69 @@
|
||||
# luci-app-easymesh
|
||||
水平有限写了基于kmod-batman-adv+802.11s 有线+无线回程的mesh luci设置插件。
|
||||
|
||||
新增ap设置让设置更加方便。
|
||||
## 软件包描述
|
||||
|
||||
新增KVR设置并添加dawn依赖在ap之间切换延迟明显降低。
|
||||
LuCI 管理界面 for EasyMesh。基于 LuCI 的 Batman-Adv mesh 网络配置界面,支持 802.11s,可在 OpenWrt 上实现无缝无线/有线回程 mesh 网络。
|
||||
|
||||
插件只对https://github.com/coolsnowwolf/lede lean版openwrt做了测试 其他源码出现问题请自行解决。
|
||||
## 功能特性
|
||||
|
||||
写的很烂暂时能用
|
||||
<small>
|
||||
|
||||
- Batman-Adv mesh 网络配置
|
||||
- 802.11s 无线 mesh 支持
|
||||
- 有线/无线回程支持
|
||||
- AP 模式,支持自定义 IP 配置
|
||||
- K/V/R(802.11k/v/r)支持,优化漫游
|
||||
- 与 DAWN 集成,实现动态漫游决策
|
||||
|
||||
</small>
|
||||
|
||||
## 依赖项
|
||||
|
||||
<small>
|
||||
|
||||
- `kmod-cfg80211`
|
||||
- `kmod-batman-adv`
|
||||
- `batctl-default`
|
||||
- `wpad-mesh-openssl`
|
||||
- `dawn`
|
||||
|
||||
</small>
|
||||
|
||||
## 硬件要求
|
||||
|
||||
<small>
|
||||
|
||||
- 支持 802.11s mesh 的无线网卡(如 MediaTek MT76、Qualcomm Atheros 等)
|
||||
- 已加载 Batman-Adv 内核模块
|
||||
|
||||
</small>
|
||||
|
||||
## 软件包路径说明
|
||||
|
||||
这是原始 `kenzok8/openwrt-packages` 软件包的标准化修复版本,遵循标准 OpenWrt LuCI 应用布局。
|
||||
|
||||
## 修复的问题
|
||||
|
||||
<small>
|
||||
|
||||
- CBI 模型 `detect_Node()` 函数中的语法错误(括号不平衡)
|
||||
- CBI 模型中的全局变量泄漏(Lua 全局变量 `v`、`s`、`apRadio`、`enable`)
|
||||
- 控制器中缺少 `nixio` require
|
||||
- 控制器中 `nixio.fs.access` 调用未 require `nixio.fs`
|
||||
- init 脚本 shell 函数中缺少 `local` 声明
|
||||
- shell 脚本中未引用的变量
|
||||
- 脆弱的 `grep` 命令解析替换为健壮的 `uci show` 解析
|
||||
- `add_wifi_mesh` 函数重构为接受 `apall` 参数,而不是依赖全局作用域
|
||||
- `uci commit` 调用适当批处理
|
||||
- `batctl n` 命令输出解析修复,使用正确的 `io.popen` 而不是 `util.execi`
|
||||
- `tail -n +2` 与标题跳过逻辑一致
|
||||
- `encryption` 字符串比较从数字修复为字符串
|
||||
- `po/zh-cn` 目录已移除(`po/zh_Hans` 的重复)
|
||||
- uci-defaults 脚本添加了 `IPKG_INSTROOT` 检查
|
||||
- Makefile 添加了缺少的 `PKG_MAINTAINER` 和 `PKG_RELEASE` 字段
|
||||
|
||||
</small>
|
||||
|
||||
## 原始作者
|
||||
|
||||
dz <dingzhong110@gmail.com>
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
module("luci.controller.easymesh", package.seeall)
|
||||
|
||||
local fs = require "nixio.fs"
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/easymesh") then
|
||||
if not fs.access("/etc/config/easymesh") then
|
||||
return
|
||||
end
|
||||
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复
|
||||
if fs.access("/usr/share/luci/menu.d/luci-app-easymesh.json") then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
-- Copyright (C) 2021 dz <dingzhong110@gmail.com>
|
||||
|
||||
local m,s,o
|
||||
local m, s, o
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local util = require "luci.util"
|
||||
|
||||
m = Map("easymesh")
|
||||
|
||||
function detect_Node()
|
||||
local function detect_Node()
|
||||
local data = {}
|
||||
local lps = luci.util.execi(" batctl n 2>/dev/null | tail +2 | sed 's/^[ ][ ]*//g' | sed 's/[ ][ ]*/ /g' | sed 's/$/ /g' ")
|
||||
for value in lps do
|
||||
local row = {}
|
||||
local pos = string.find(value, " ")
|
||||
local IFA = string.sub(value, 1, pos - 1)
|
||||
local value = string.sub(value, pos + 1, string.len(value))
|
||||
pos = string.find(value, " ")
|
||||
local pos = string.find(value, " ")
|
||||
local Neighbora = string.sub(value, 1, pos - 1)
|
||||
local value = string.sub(value, pos + 1, string.len(value))
|
||||
pos = string.find(value, " ")
|
||||
local pos = string.find(value, " ")
|
||||
local lastseena = string.sub(value, 1, pos - 1)
|
||||
local value = string.sub(value, pos + 1, string.len(value))
|
||||
pos = string.find(value, " ")
|
||||
row["IF"] = IFA
|
||||
row["Neighbor"] = Neighbora
|
||||
row["lastseen"] = lastseena
|
||||
table.insert(data, row)
|
||||
local cmd = "batctl n 2>/dev/null | tail -n +2 | sed 's/^[ ]*//g' | sed 's/[ ]*/ /g'"
|
||||
local fp = io.popen(cmd)
|
||||
if not fp then return data end
|
||||
for line in fp:lines() do
|
||||
local pos = string.find(line, " ")
|
||||
if pos then
|
||||
local ifa = string.sub(line, 1, pos - 1)
|
||||
local rest = string.sub(line, pos + 1)
|
||||
pos = string.find(rest, " ")
|
||||
if pos then
|
||||
local neighbor = string.sub(rest, 1, pos - 1)
|
||||
rest = string.sub(rest, pos + 1)
|
||||
pos = string.find(rest, " ")
|
||||
if pos then
|
||||
local lastseen = string.sub(rest, 1, pos - 1)
|
||||
table.insert(data, { IF = ifa, Neighbor = neighbor, lastseen = lastseen })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
fp:close()
|
||||
return data
|
||||
end
|
||||
local Nodes = luci.sys.exec("batctl n 2>/dev/null| tail +3 | wc -l")
|
||||
|
||||
local Nodes = sys.exec("batctl n 2>/dev/null | tail -n +2 | wc -l")
|
||||
local Node = detect_Node()
|
||||
v = m:section(Table, Node, "" ,"<b>" .. translate("Active node") .. ":" .. Nodes .. "</b>")
|
||||
local v = m:section(Table, Node, "", "<b>" .. translate("Active node") .. ":" .. Nodes .. "</b>")
|
||||
v:option(DummyValue, "IF", translate("IF"))
|
||||
v:option(DummyValue, "Neighbor", translate("Neighbor"))
|
||||
v:option(DummyValue, "lastseen", translate("lastseen"))
|
||||
|
||||
-- Basic
|
||||
s = m:section(TypedSection, "easymesh", translate("Settings"), translate("General Settings"))
|
||||
s.anonymous = true
|
||||
|
||||
---- Eanble
|
||||
o = s:option(Flag, "enabled", translate("Enable"), translate("Enable or disable EASY MESH"))
|
||||
o.default = 0
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(ListValue, "role", translate("role"))
|
||||
@@ -52,76 +53,68 @@ o:value("server", translate("host MESH"))
|
||||
o:value("client", translate("son MESH"))
|
||||
o.rmempty = false
|
||||
|
||||
apRadio = s:option(ListValue, "apRadio", translate("MESH Radio device"), translate("The radio device which MESH use"))
|
||||
local apRadio = s:option(ListValue, "apRadio", translate("MESH Radio device"), translate("The radio device which MESH use"))
|
||||
uci:foreach("wireless", "wifi-device",
|
||||
function(s)
|
||||
apRadio:value(s['.name'])
|
||||
end)
|
||||
function(sect)
|
||||
apRadio:value(sect['.name'])
|
||||
end)
|
||||
apRadio:value("all", translate("ALL"))
|
||||
o.default = "radio0"
|
||||
o.rmempty = false
|
||||
apRadio.default = "radio0"
|
||||
apRadio.rmempty = false
|
||||
|
||||
---- mesh
|
||||
o = s:option(Value, "mesh_id", translate("MESH ID"))
|
||||
o.default = "easymesh"
|
||||
o.description = translate("MESH ID")
|
||||
|
||||
enable = s:option(Flag, "encryption", translate("Encryption"), translate(""))
|
||||
enable.default = 0
|
||||
enable.rmempty = false
|
||||
o = s:option(Flag, "encryption", translate("Encryption"), "")
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "key", translate("Key"))
|
||||
o.default = "easymesh"
|
||||
o:depends("encryption", 1)
|
||||
o:depends("encryption", "1")
|
||||
|
||||
---- kvr
|
||||
enable = s:option(Flag, "kvr", translate("K/V/R"), translate(""))
|
||||
enable.default = 1
|
||||
enable.rmempty = false
|
||||
o = s:option(Flag, "kvr", translate("K/V/R"), "")
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "mobility_domain", translate("Mobility Domain"), translate("4-character hexadecimal ID"))
|
||||
o.default = "4f57"
|
||||
o.datatype = "and(hexstring,rangelength(4,4))"
|
||||
o:depends("kvr", 1)
|
||||
o:depends("kvr", "1")
|
||||
|
||||
o = s:option(Value, "rssi_val", translate("Threshold for an good RSSI"))
|
||||
o = s:option(Value, "rssi_val", translate("Threshold for a good RSSI"))
|
||||
o.default = "-60"
|
||||
o.atatype = "range(-1,-120)"
|
||||
o:depends("kvr", 1)
|
||||
o.datatype = "range(-120,-1)"
|
||||
o:depends("kvr", "1")
|
||||
|
||||
o = s:option(Value, "low_rssi_val", translate("Threshold for an bad RSSI"))
|
||||
o = s:option(Value, "low_rssi_val", translate("Threshold for a bad RSSI"))
|
||||
o.default = "-88"
|
||||
o.atatype = "range(-1,-120)"
|
||||
o:depends("kvr", 1)
|
||||
o.datatype = "range(-120,-1)"
|
||||
o:depends("kvr", "1")
|
||||
|
||||
---- 802.11F
|
||||
--enable = s:option(Flag, "iapp", translate("inter-access point protocol"), translate("Wireless Access Points (APs) running on different vendors can communicate with each other"))
|
||||
--enable.default = 0
|
||||
--enable.rmempty = false
|
||||
|
||||
---- ap_mode
|
||||
enable = s:option(Flag, "ap_mode", translate("AP MODE Enable"), translate("Enable or disable AP MODE"))
|
||||
enable.default = 0
|
||||
enable.rmempty = false
|
||||
o = s:option(Flag, "ap_mode", translate("AP MODE Enable"), translate("Enable or disable AP MODE"))
|
||||
o.default = "0"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "ipaddr", translate("IPv4-Address"))
|
||||
o.default = "192.168.1.10"
|
||||
o.datatype = "ip4addr"
|
||||
o:depends("ap_mode", 1)
|
||||
o:depends("ap_mode", "1")
|
||||
|
||||
o = s:option(Value, "netmask", translate("IPv4 netmask"))
|
||||
o.default = "255.255.255.0"
|
||||
o.datatype = "ip4addr"
|
||||
o:depends("ap_mode", 1)
|
||||
o:depends("ap_mode", "1")
|
||||
|
||||
o = s:option(Value, "gateway", translate("IPv4 gateway"))
|
||||
o.default = "192.168.1.1"
|
||||
o.datatype = "ip4addr"
|
||||
o:depends("ap_mode", 1)
|
||||
o:depends("ap_mode", "1")
|
||||
|
||||
o = s:option(Value, "dns", translate("Use custom DNS servers"))
|
||||
o.default = "192.168.1.1"
|
||||
o.datatype = "ip4addr"
|
||||
o:depends("ap_mode", 1)
|
||||
o:depends("ap_mode", "1")
|
||||
|
||||
return m
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
zh-cn
|
||||
@@ -1,35 +1,37 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
STOP=70
|
||||
|
||||
load_easymesh_config() {
|
||||
enable=$(uci -q get easymesh.config.enabled)
|
||||
mesh_bat0=$(uci -q get network.bat0)
|
||||
ap_mode=$(uci -q get easymesh.config.ap_mode)
|
||||
lan=$(uci -q get network.lan.ifname)
|
||||
ipaddr=$(uci -q get easymesh.config.ipaddr)
|
||||
netmask=$(uci -q get easymesh.config.netmask)
|
||||
gateway=$(uci -q get easymesh.config.gateway)
|
||||
dns=$(uci -q get easymesh.config.dns)
|
||||
ap_ipaddr=$(uci -q get network.lan.ipaddr)
|
||||
ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
|
||||
apRadio=$(uci -q get easymesh.config.apRadio)
|
||||
kvr=$(uci -q get easymesh.config.kvr)
|
||||
iapp=$(uci -q get easymesh.config.iapp)
|
||||
brlan=$(uci -q get network.@device[0].name)
|
||||
role=$(uci -q get easymesh.config.role)
|
||||
local enable=$(uci -q get easymesh.config.enabled)
|
||||
local mesh_bat0=$(uci -q get network.bat0)
|
||||
local ap_mode=$(uci -q get easymesh.config.ap_mode)
|
||||
local lan=$(uci -q get network.lan.ifname)
|
||||
local ipaddr=$(uci -q get easymesh.config.ipaddr)
|
||||
local netmask=$(uci -q get easymesh.config.netmask)
|
||||
local gateway=$(uci -q get easymesh.config.gateway)
|
||||
local dns=$(uci -q get easymesh.config.dns)
|
||||
local ap_ipaddr=$(uci -q get network.lan.ipaddr)
|
||||
local ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
|
||||
local apRadio=$(uci -q get easymesh.config.apRadio)
|
||||
local kvr=$(uci -q get easymesh.config.kvr)
|
||||
local iapp=$(uci -q get easymesh.config.iapp)
|
||||
local brlan=$(uci -q get network.@device[0].name)
|
||||
local role=$(uci -q get easymesh.config.role)
|
||||
}
|
||||
|
||||
ap_mode_stop() {
|
||||
ap_ipaddr=$(uci -q get network.lan.ipaddr)
|
||||
ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
|
||||
dns1=$(sed -n '2p' /etc/easymesh 2>/dev/null)
|
||||
local ap_ipaddr=$(uci -q get network.lan.ipaddr)
|
||||
local ap_ipaddr1=$(sed -n '1p' /etc/easymesh 2>/dev/null)
|
||||
local dns1=$(sed -n '2p' /etc/easymesh 2>/dev/null)
|
||||
|
||||
if [ "$ap_ipaddr" = "$ap_ipaddr1" ]; then
|
||||
uci -q delete network.lan.gateway
|
||||
uci -q del_list network.lan.dns=$dns1
|
||||
uci -q del_list network.lan.dns="$dns1"
|
||||
uci commit network
|
||||
|
||||
echo "" >/etc/easymesh
|
||||
echo "" > /etc/easymesh
|
||||
|
||||
uci -q delete dhcp.lan.dynamicdhcp
|
||||
uci -q delete dhcp.lan.ignore
|
||||
@@ -41,94 +43,98 @@ ap_mode_stop() {
|
||||
}
|
||||
|
||||
add_wifi_mesh() {
|
||||
mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
|
||||
mesh_apRadio=$(uci -q get wireless.mesh_${apall}.device)
|
||||
mesh_mesh=$(uci -q get wireless.mesh_${apall})
|
||||
mesh_id=$(uci -q get easymesh.config.mesh_id)
|
||||
mobility_domain=$(uci -q get easymesh.config.mobility_domain)
|
||||
key=$(uci -q get easymesh.config.key)
|
||||
encryption=$(uci -q get easymesh.config.encryption)
|
||||
local apall="$1"
|
||||
local mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
|
||||
local mesh_apRadio=$(uci -q get wireless.mesh_${apall}.device)
|
||||
local mesh_mesh=$(uci -q get wireless.mesh_${apall})
|
||||
local mesh_id=$(uci -q get easymesh.config.mesh_id)
|
||||
local mobility_domain=$(uci -q get easymesh.config.mobility_domain)
|
||||
local key=$(uci -q get easymesh.config.key)
|
||||
local encryption=$(uci -q get easymesh.config.encryption)
|
||||
|
||||
if [ "$mesh_nwi_mesh" != "interface" ]; then
|
||||
uci set network.nwi_mesh_$apall=interface
|
||||
uci set network.nwi_mesh_$apall.proto='batadv_hardif'
|
||||
uci set network.nwi_mesh_$apall.master='bat0'
|
||||
uci set network.nwi_mesh_$apall.mtu='1536'
|
||||
uci set network.nwi_mesh_${apall}=interface
|
||||
uci set network.nwi_mesh_${apall}.proto='batadv_hardif'
|
||||
uci set network.nwi_mesh_${apall}.master='bat0'
|
||||
uci set network.nwi_mesh_${apall}.mtu='1536'
|
||||
uci commit network
|
||||
fi
|
||||
|
||||
if [ "$mesh_mesh" != "wifi-iface" ]; then
|
||||
uci set wireless.mesh_$apall=wifi-iface
|
||||
uci set wireless.mesh_$apall.device=$apall
|
||||
uci set wireless.mesh_$apall.ifname=mesh_${apall}
|
||||
uci set wireless.mesh_$apall.network=nwi_mesh_${apall}
|
||||
uci set wireless.mesh_$apall.mode='mesh'
|
||||
uci set wireless.mesh_$apall.mesh_id=$mesh_id
|
||||
uci set wireless.mesh_$apall.mesh_fwding='0'
|
||||
uci set wireless.mesh_$apall.mesh_ttl='1'
|
||||
uci set wireless.mesh_$apall.mcast_rate='24000'
|
||||
uci set wireless.mesh_$apall.disabled='0'
|
||||
uci set wireless.mesh_${apall}=wifi-iface
|
||||
uci set wireless.mesh_${apall}.device="${apall}"
|
||||
uci set wireless.mesh_${apall}.ifname="mesh_${apall}"
|
||||
uci set wireless.mesh_${apall}.network="nwi_mesh_${apall}"
|
||||
uci set wireless.mesh_${apall}.mode='mesh'
|
||||
uci set wireless.mesh_${apall}.mesh_id="${mesh_id}"
|
||||
uci set wireless.mesh_${apall}.mesh_fwding='0'
|
||||
uci set wireless.mesh_${apall}.mesh_ttl='1'
|
||||
uci set wireless.mesh_${apall}.mcast_rate='24000'
|
||||
uci set wireless.mesh_${apall}.disabled='0'
|
||||
uci commit wireless
|
||||
fi
|
||||
|
||||
if [ "$mesh_mesh" = "wifi-iface" ]; then
|
||||
if [ "$mesh_apRadio" != "$apall" ]; then
|
||||
uci set wireless.mesh_$apall.device=$apall
|
||||
uci commit wireless
|
||||
fi
|
||||
if [ "$mesh_mesh" = "wifi-iface" ] && [ "$mesh_apRadio" != "$apall" ]; then
|
||||
uci set wireless.mesh_${apall}.device="${apall}"
|
||||
uci commit wireless
|
||||
fi
|
||||
|
||||
if [ "$encryption" != 1 ]; then
|
||||
uci set wireless.mesh_$apall.encryption='none'
|
||||
if [ "$encryption" != "1" ]; then
|
||||
uci set wireless.mesh_${apall}.encryption='none'
|
||||
uci commit wireless
|
||||
else
|
||||
uci set wireless.mesh_$apall.encryption='sae'
|
||||
uci set wireless.mesh_$apall.key=$key
|
||||
uci set wireless.mesh_${apall}.encryption='sae'
|
||||
uci set wireless.mesh_${apall}.key="${key}"
|
||||
uci commit wireless
|
||||
fi
|
||||
}
|
||||
|
||||
add_kvr() {
|
||||
kvr=$(uci -q get easymesh.config.kvr)
|
||||
mobility_domain=$(uci -q get easymesh.config.mobility_domain)
|
||||
iapp=$(uci -q get easymesh.config.iapp)
|
||||
for apall in $(uci -X show wireless | grep wifi-device | awk -F'[.=]' '{print $2}'); do
|
||||
if [ "$kvr" = 1 ]; then
|
||||
uci set wireless.default_$apall.ieee80211k='1'
|
||||
uci set wireless.default_$apall.rrm_neighbor_report='1'
|
||||
uci set wireless.default_$apall.rrm_beacon_report='1'
|
||||
uci set wireless.default_$apall.ieee80211v='1'
|
||||
uci set wireless.default_$apall.bss_transition='1'
|
||||
uci set wireless.default_$apall.ieee80211r='1'
|
||||
uci set wireless.default_$apall.encryption='psk2+ccmp'
|
||||
uci set wireless.default_$apall.mobility_domain=$mobility_domain
|
||||
uci set wireless.default_$apall.ft_over_ds='1'
|
||||
uci set wireless.default_$apall.ft_psk_generate_local='1'
|
||||
uci commit wireless
|
||||
local kvr=$(uci -q get easymesh.config.kvr)
|
||||
local mobility_domain=$(uci -q get easymesh.config.mobility_domain)
|
||||
local iapp=$(uci -q get easymesh.config.iapp)
|
||||
local commit_wireless=
|
||||
|
||||
for apall in $(uci show wireless 2>/dev/null | grep "=wifi-device" | sed -n 's/^wireless\.\([^=]*\)=.*/\1/p'); do
|
||||
if [ "$kvr" = "1" ]; then
|
||||
uci set wireless.default_${apall}.ieee80211k='1'
|
||||
uci set wireless.default_${apall}.rrm_neighbor_report='1'
|
||||
uci set wireless.default_${apall}.rrm_beacon_report='1'
|
||||
uci set wireless.default_${apall}.ieee80211v='1'
|
||||
uci set wireless.default_${apall}.bss_transition='1'
|
||||
uci set wireless.default_${apall}.ieee80211r='1'
|
||||
uci set wireless.default_${apall}.encryption='psk2+ccmp'
|
||||
uci set wireless.default_${apall}.mobility_domain="${mobility_domain}"
|
||||
uci set wireless.default_${apall}.ft_over_ds='1'
|
||||
uci set wireless.default_${apall}.ft_psk_generate_local='1'
|
||||
commit_wireless=1
|
||||
else
|
||||
uci -q delete wireless.default_$apall.ieee80211k
|
||||
uci -q delete wireless.default_$apall.ieee80211v
|
||||
uci -q delete wireless.default_$apall.ieee80211r
|
||||
uci commit wireless
|
||||
uci -q delete wireless.default_${apall}.ieee80211k
|
||||
uci -q delete wireless.default_${apall}.ieee80211v
|
||||
uci -q delete wireless.default_${apall}.ieee80211r
|
||||
commit_wireless=1
|
||||
fi
|
||||
if [ "$iapp" = 1 ]; then
|
||||
uci set wireless.default_$apall.iapp_interface='br-lan'
|
||||
uci commit wireless
|
||||
|
||||
if [ "$iapp" = "1" ]; then
|
||||
uci set wireless.default_${apall}.iapp_interface='br-lan'
|
||||
commit_wireless=1
|
||||
else
|
||||
uci -q delete wireless.default_$apall.iapp_interface
|
||||
uci commit wireless
|
||||
uci -q delete wireless.default_${apall}.iapp_interface
|
||||
commit_wireless=1
|
||||
fi
|
||||
done
|
||||
|
||||
[ -n "$commit_wireless" ] && uci commit wireless
|
||||
}
|
||||
|
||||
add_dawn() {
|
||||
kvr=$(uci -q get easymesh.config.kvr)
|
||||
rssi_val=$(uci -q get easymesh.config.rssi_val)
|
||||
low_rssi_val=$(uci -q get easymesh.config.low_rssi_val)
|
||||
local kvr=$(uci -q get easymesh.config.kvr)
|
||||
local rssi_val=$(uci -q get easymesh.config.rssi_val)
|
||||
local low_rssi_val=$(uci -q get easymesh.config.low_rssi_val)
|
||||
|
||||
if [ "$kvr" = 1 ]; then
|
||||
uci set dawn.@metric[0].rssi_val=$rssi_val
|
||||
uci set dawn.@metric[0].low_rssi_val=$low_rssi_val
|
||||
if [ "$kvr" = "1" ]; then
|
||||
uci set dawn.@metric[0].rssi_val="${rssi_val}"
|
||||
uci set dawn.@metric[0].low_rssi_val="${low_rssi_val}"
|
||||
uci commit dawn
|
||||
/etc/init.d/dawn enable && /etc/init.d/dawn start
|
||||
else
|
||||
@@ -138,7 +144,8 @@ add_dawn() {
|
||||
|
||||
set_easymesh() {
|
||||
load_easymesh_config
|
||||
if [ "$enable" = 1 ]; then
|
||||
|
||||
if [ "$enable" = "1" ]; then
|
||||
if [ "$mesh_bat0" != "interface" ]; then
|
||||
uci set network.bat0=interface
|
||||
uci set network.bat0.proto='batadv'
|
||||
@@ -149,8 +156,6 @@ set_easymesh() {
|
||||
uci set network.bat0.bridge_loop_avoidance='1'
|
||||
uci set network.bat0.distributed_arp_table='1'
|
||||
uci set network.bat0.fragmentation='1'
|
||||
# uci set network.bat0.gw_bandwidth='10000/2000'
|
||||
# uci set network.bat0.gw_sel_class='20'
|
||||
uci set network.bat0.hop_penalty='30'
|
||||
uci set network.bat0.isolation_mark='0x00000000/0x00000000'
|
||||
uci set network.bat0.log_level='0'
|
||||
@@ -176,28 +181,27 @@ set_easymesh() {
|
||||
fi
|
||||
|
||||
if [ "$apRadio" = "all" ]; then
|
||||
for apall in $(uci -X show wireless | grep wifi-device | awk -F'[.=]' '{print $2}'); do
|
||||
add_wifi_mesh
|
||||
for apall in $(uci show wireless 2>/dev/null | grep "=wifi-device" | sed -n 's/^wireless\.\([^=]*\)=.*/\1/p'); do
|
||||
add_wifi_mesh "${apall}"
|
||||
done
|
||||
else
|
||||
apall=$apRadio
|
||||
add_wifi_mesh
|
||||
add_wifi_mesh "${apRadio}"
|
||||
fi
|
||||
|
||||
add_kvr
|
||||
add_dawn
|
||||
|
||||
if [ "$ap_mode" = 1 ]; then
|
||||
if [ "$ap_mode" = "1" ]; then
|
||||
if [ "$ap_ipaddr" != "$ipaddr" ]; then
|
||||
uci set network.lan.ipaddr=$ipaddr
|
||||
uci set network.lan.netmask=$netmask
|
||||
uci set network.lan.gateway=$gateway
|
||||
uci add_list network.lan.dns=$dns
|
||||
uci set network.lan.ipaddr="${ipaddr}"
|
||||
uci set network.lan.netmask="${netmask}"
|
||||
uci set network.lan.gateway="${gateway}"
|
||||
uci add_list network.lan.dns="${dns}"
|
||||
uci commit network
|
||||
|
||||
echo "" >/etc/easymesh
|
||||
echo "$ipaddr" >/etc/easymesh
|
||||
echo "$dns" >>/etc/easymesh
|
||||
echo "" > /etc/easymesh
|
||||
echo "${ipaddr}" > /etc/easymesh
|
||||
echo "${dns}" >> /etc/easymesh
|
||||
|
||||
uci set dhcp.lan.dynamicdhcp='0'
|
||||
uci set dhcp.lan.ignore='1'
|
||||
@@ -223,17 +227,17 @@ set_easymesh() {
|
||||
uci commit network
|
||||
fi
|
||||
|
||||
for apall in $(uci -X show wireless | grep wifi-device | awk -F'[.=]' '{print $2}'); do
|
||||
mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
|
||||
mesh_mesh=$(uci -q get wireless.mesh_${apall})
|
||||
for apall in $(uci show wireless 2>/dev/null | grep "=wifi-device" | sed -n 's/^wireless\.\([^=]*\)=.*/\1/p'); do
|
||||
local mesh_nwi_mesh=$(uci -q get network.nwi_mesh_${apall})
|
||||
local mesh_mesh=$(uci -q get wireless.mesh_${apall})
|
||||
|
||||
if [ "$mesh_nwi_mesh" = "interface" ]; then
|
||||
uci -q delete network.nwi_mesh_$apall
|
||||
uci -q delete network.nwi_mesh_${apall}
|
||||
uci commit network
|
||||
fi
|
||||
|
||||
if [ "$mesh_mesh" = "wifi-iface" ]; then
|
||||
uci -q delete wireless.mesh_$apall
|
||||
uci -q delete wireless.mesh_${apall}
|
||||
uci commit wireless
|
||||
fi
|
||||
done
|
||||
@@ -241,10 +245,11 @@ set_easymesh() {
|
||||
add_kvr
|
||||
add_dawn
|
||||
|
||||
if [ "$ap_mode" = 1 ]; then
|
||||
if [ "$ap_mode" = "1" ]; then
|
||||
ap_mode_stop
|
||||
fi
|
||||
fi
|
||||
|
||||
/etc/init.d/network restart
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
[ -z "${IPKG_INSTROOT}" ] || exit 0
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@easymesh[-1]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"admin/network/easymesh": {
|
||||
"title": "EASY MESH",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "easymesh"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-easymesh" ],
|
||||
"uci": { "easymesh": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"luci-app-easymesh": {
|
||||
"description": "Grant UCI access for luci-app-easymesh",
|
||||
"read": {
|
||||
"uci": [ "easymesh" ]
|
||||
"uci": [ "easymesh", "wireless", "network" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "easymesh" ]
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
#
|
||||
# Copyright (C) 2006-2017 OpenWrt.org
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-eqos
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Jianhui Zhao <jianhuizhao329@gmail.com> GaryPang <https://github.com/garypang13/luci-app-eqos>
|
||||
PKG_MAINTAINER:=kenzok78 <admin@kenzok78.com>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
LUCI_TITLE:=EQOS - LuCI interface
|
||||
LUCI_TITLE:=LuCI support for EQOS
|
||||
LUCI_DEPENDS:=+luci-base +tc +kmod-sched-core +kmod-ifb
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
define Package/luci-app-eqos/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/etc/config/eqos $(1)/etc/config/eqos
|
||||
$(INSTALL_BIN) ./files/etc/init.d/eqos $(1)/etc/init.d/eqos
|
||||
$(INSTALL_BIN) ./files/etc/hotplug.d/iface/10-eqos $(1)/etc/hotplug.d/iface/10-eqos
|
||||
$(INSTALL_BIN) ./files/usr/sbin/eqos $(1)/usr/sbin/eqos
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-eqos))
|
||||
|
||||
@@ -1,32 +1,118 @@
|
||||
# Easy QoS for OpenWRT/Lede([中文](https://github.com/garypang13/luci-app-eqos/blob/master/README_ZH.md))
|
||||
# luci-app-eqos
|
||||
|
||||

|
||||
[](LICENSE)
|
||||
|
||||

|
||||
EQOS (Easy QoS) for OpenWrt LuCI - 基于IP限速的流量控制工具
|
||||
|
||||
# Features
|
||||
* Support speed limit based on IP address
|
||||
* No marking by iptables
|
||||
* Support LuCI interface
|
||||
## 特性
|
||||
|
||||
# Install to OpenWRT/LEDE
|
||||
|
||||
git clone https://github.com/garypang13/luci-app-eqos
|
||||
cp -r eqos LEDE_DIR/package/eqos
|
||||
|
||||
cd LEDE_DIR
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
|
||||
make menuconfig
|
||||
LuCI --->
|
||||
1. Collections --->
|
||||
<*> luci
|
||||
3. Applications --->
|
||||
<*> luci-app-eqos...................................... EQOS - LuCI interface
|
||||
4. Themes --->
|
||||
<*> luci-theme-material
|
||||
Network --->
|
||||
-*- eqos................... Easy QoS(Support speed limit based on IP address)
|
||||
|
||||
make package/eqos/compile V=s
|
||||
- 支持基于IP地址限速
|
||||
- 支持自定义WAN设备
|
||||
- 不使用iptables MARK,更高效
|
||||
- 提供直观的LuCI Web界面
|
||||
- 支持IPv4地址自动发现
|
||||
|
||||
## 系统要求
|
||||
|
||||
- OpenWrt 18.06 或更高版本
|
||||
- LuCI Web 界面
|
||||
- kmod-sched-core
|
||||
- kmod-ifb
|
||||
- tc (iproute2)
|
||||
|
||||
## 安装
|
||||
|
||||
### 编译安装
|
||||
|
||||
```bash
|
||||
# 克隆到 OpenWrt SDK
|
||||
git clone https://github.com/kenzok78/luci-app-eqos.git
|
||||
|
||||
# 放入 packages 目录
|
||||
mv luci-app-eqos /path/to/openwrt/package/feeds/luci/
|
||||
|
||||
# 编译
|
||||
make package/luci-app-eqos/compile V=s
|
||||
```
|
||||
|
||||
### 在线安装
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install luci-app-eqos
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
1. 登录 LuCI 管理界面
|
||||
2. 进入 **网络 → EQOS**
|
||||
3. 启用 EQOS 并设置总带宽
|
||||
4. 添加需要限速的 IP 地址及带宽
|
||||
|
||||
## 命令行用法
|
||||
|
||||
```bash
|
||||
# 启动
|
||||
eqos start
|
||||
|
||||
# 停止
|
||||
eqos stop
|
||||
|
||||
# 重启
|
||||
eqos restart
|
||||
|
||||
# 添加规则
|
||||
eqos add 192.168.1.100 10 5
|
||||
|
||||
# 查看状态
|
||||
eqos show
|
||||
|
||||
# 清除所有规则
|
||||
eqos flush
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
luci-app-eqos/
|
||||
├── Makefile
|
||||
├── luasrc/
|
||||
│ ├── controller/eqos.lua
|
||||
│ └── model/cbi/eqos.lua
|
||||
├── po/
|
||||
│ └── zh_Hans/eqos.po
|
||||
├── root/
|
||||
│ ├── etc/
|
||||
│ │ ├── config/eqos
|
||||
│ │ ├── init.d/eqos
|
||||
│ │ └── hotplug.d/iface/10-eqos
|
||||
│ └── usr/
|
||||
│ ├── sbin/eqos
|
||||
│ └── share/rpcd/acl.d/luci-app-eqos.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. 使用 HTB (Hierarchical Token Bucket) 队列规则
|
||||
2. 下载流量通过 ifb (Intermediate Functional Block) 设备控制
|
||||
3. 上传流量直接在 WAN 接口控制
|
||||
|
||||
## 许可证
|
||||
|
||||
GPL-2.0
|
||||
|
||||
## 致谢
|
||||
|
||||
- 原始项目: [luci-app-eqos](https://github.com/garypang13/luci-app-eqos) by GaryPang
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v2.0.0 (2026-03-22)
|
||||
|
||||
- 重构 Lua CBI 模型,修复全局变量泄漏
|
||||
- 添加输入验证 (IP 地址、速度)
|
||||
- 添加日志记录
|
||||
- 支持自定义 WAN 设备
|
||||
- 添加 flush/show 命令
|
||||
- 完善中文翻译
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Easy QoS for OpenWRT/Lede
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 特性
|
||||
* 支持基于IP地址限速
|
||||
* 未使用iptables打MARK
|
||||
* 提供Luci界面
|
||||
|
||||
# 安装到OpenWRT/LEDE
|
||||
|
||||
git clone https://github.com/garypang13/luci-app-eqos
|
||||
cp -r eqos LEDE_DIR/package/eqos
|
||||
|
||||
cd LEDE_DIR
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
|
||||
make menuconfig
|
||||
LuCI --->
|
||||
1. Collections --->
|
||||
<*> luci
|
||||
3. Applications --->
|
||||
<*> luci-app-eqos...................................... EQOS - LuCI interface
|
||||
4. Themes --->
|
||||
<*> luci-theme-material
|
||||
Network --->
|
||||
-*- eqos................... Easy QoS(Support speed limit based on IP address)
|
||||
|
||||
make package/eqos/compile V=s
|
||||
@@ -1,10 +1,11 @@
|
||||
module("luci.controller.eqos", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/eqos") then
|
||||
return
|
||||
end
|
||||
|
||||
local page
|
||||
entry({"admin", "network", "eqos"}, cbi("eqos"), _("EQoS"), 121).dependent = true
|
||||
if not nixio.fs.access("/etc/config/eqos") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry({"admin", "network", "eqos"}, cbi("eqos"), _("EQOS"), 121)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-eqos" }
|
||||
end
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
local ipc = require "luci.ip"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
local m = Map("eqos", translate("Quality of Service"))
|
||||
|
||||
local s = m:section(TypedSection, "eqos", "")
|
||||
s.anonymous = true
|
||||
local global_section = m:section(NamedSection, "global", "eqos", translate("Global Settings"))
|
||||
global_section.addremove = false
|
||||
global_section.cfgsections = {"global"}
|
||||
|
||||
local e = s:option(Flag, "enabled", translate("Enable"))
|
||||
e.rmempty = false
|
||||
local enabled = global_section:option(Flag, "enabled", translate("Enable EQOS"))
|
||||
enabled.rmempty = false
|
||||
|
||||
local dl = s:option(Value, "download", translate("Download speed (Mbit/s)"), translate("Total bandwidth"))
|
||||
dl.datatype = "and(uinteger,min(1))"
|
||||
local wan_device = global_section:option(Value, "wan", translate("WAN Device"))
|
||||
wan_device.datatype = "network"
|
||||
wan_device.default = "wan"
|
||||
|
||||
local ul = s:option(Value, "upload", translate("Upload speed (Mbit/s)"), translate("Total bandwidth"))
|
||||
ul.datatype = "and(uinteger,min(1))"
|
||||
local dl_speed = global_section:option(Value, "download", translate("Download Speed (Mbit/s)"), translate("Total bandwidth"))
|
||||
dl_speed.datatype = "and(uinteger,min(1))"
|
||||
dl_speed.default = "100"
|
||||
|
||||
s = m:section(TypedSection, "device", translate("Speed limit based on IP address"))
|
||||
s.template = "cbi/tblsection"
|
||||
s.anonymous = true
|
||||
s.addremove = true
|
||||
s.sortable = true
|
||||
local ul_speed = global_section:option(Value, "upload", translate("Upload Speed (Mbit/s)"), translate("Total bandwidth"))
|
||||
ul_speed.datatype = "and(uinteger,min(1))"
|
||||
ul_speed.default = "50"
|
||||
|
||||
local ip = s:option(Value, "ip", translate("IP address"))
|
||||
local device_section = m:section(TypedSection, "device", translate("Speed Limit by IP Address"))
|
||||
device_section.template = "cbi/tblsection"
|
||||
device_section.anonymous = true
|
||||
device_section.addremove = true
|
||||
|
||||
local ip_addr = device_section:option(Value, "ip", translate("IP Address"))
|
||||
ip_addr.datatype = "ipaddr"
|
||||
ip_addr.placeholder = "192.168.1.100"
|
||||
|
||||
ipc.neighbors({family = 4, dev = "br-lan"}, function(n)
|
||||
if n.mac and n.dest then
|
||||
ip:value(n.dest:string(), "%s (%s)" %{ n.dest:string(), n.mac })
|
||||
ip_addr:value(n.dest:string(), "%s (%s)" %{ n.dest:string(), n.mac })
|
||||
end
|
||||
end)
|
||||
|
||||
dl = s:option(Value, "download", translate("Download speed (Mbit/s)"))
|
||||
dl.datatype = "and(uinteger,min(1))"
|
||||
local dl_limit = device_section:option(Value, "download", translate("Download (Mbit/s)"))
|
||||
dl_limit.datatype = "and(uinteger,min(1))"
|
||||
dl_limit.placeholder = "10"
|
||||
|
||||
ul = s:option(Value, "upload", translate("Upload speed (Mbit/s)"))
|
||||
ul.datatype = "and(uinteger,min(1))"
|
||||
local ul_limit = device_section:option(Value, "upload", translate("Upload (Mbit/s)"))
|
||||
ul_limit.datatype = "and(uinteger,min(1))"
|
||||
ul_limit.placeholder = "5"
|
||||
|
||||
comment = s:option(Value, "comment", translate("Comment"))
|
||||
local comment = device_section:option(Value, "comment", translate("Comment"))
|
||||
comment.placeholder = translate("Description")
|
||||
|
||||
return m
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: LuCi Chinese Translation\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Pootle 2.0.6\n"
|
||||
|
||||
msgid "EQoS"
|
||||
msgstr "IP限速"
|
||||
|
||||
msgid "Comment"
|
||||
msgstr "注解"
|
||||
|
||||
msgid "Download speed (Mbit/s)"
|
||||
msgstr "下载速度 (Mbit/s)"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "开启"
|
||||
|
||||
msgid "QoS"
|
||||
msgstr "QoS"
|
||||
|
||||
msgid "Quality of Service"
|
||||
msgstr "IP限速"
|
||||
|
||||
msgid "Upload speed (Mbit/s)"
|
||||
msgstr "上传速度 (Mbit/s)"
|
||||
|
||||
msgid "Speed limit based on IP address"
|
||||
msgstr "IP限速"
|
||||
|
||||
msgid "Total bandwidth"
|
||||
msgstr "总带宽"
|
||||
@@ -1,6 +1,6 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: LuCi Chinese Translation\n"
|
||||
"Project-Id-Version: LuCI Chinese Translation\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -9,29 +9,44 @@ msgstr ""
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Pootle 2.0.6\n"
|
||||
|
||||
msgid "EQoS"
|
||||
msgstr "IP限速"
|
||||
|
||||
msgid "Comment"
|
||||
msgstr "注解"
|
||||
|
||||
msgid "Download speed (Mbit/s)"
|
||||
msgstr "下载速度 (Mbit/s)"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "开启"
|
||||
|
||||
msgid "QoS"
|
||||
msgstr "QoS"
|
||||
msgid "EQOS"
|
||||
msgstr "EQOS"
|
||||
|
||||
msgid "Quality of Service"
|
||||
msgstr "IP限速"
|
||||
|
||||
msgid "Upload speed (Mbit/s)"
|
||||
msgstr "上传速度 (Mbit/s)"
|
||||
msgid "Global Settings"
|
||||
msgstr "全局设置"
|
||||
|
||||
msgid "Speed limit based on IP address"
|
||||
msgstr "IP限速"
|
||||
msgid "Enable EQOS"
|
||||
msgstr "启用EQOS"
|
||||
|
||||
msgid "WAN Device"
|
||||
msgstr "WAN设备"
|
||||
|
||||
msgid "Download Speed (Mbit/s)"
|
||||
msgstr "下载速度 (Mbit/s)"
|
||||
|
||||
msgid "Upload Speed (Mbit/s)"
|
||||
msgstr "上传速度 (Mbit/s)"
|
||||
|
||||
msgid "Total bandwidth"
|
||||
msgstr "总带宽"
|
||||
|
||||
msgid "Speed Limit by IP Address"
|
||||
msgstr "按IP限速"
|
||||
|
||||
msgid "IP Address"
|
||||
msgstr "IP地址"
|
||||
|
||||
msgid "Download (Mbit/s)"
|
||||
msgstr "下载 (Mbit/s)"
|
||||
|
||||
msgid "Upload (Mbit/s)"
|
||||
msgstr "上传 (Mbit/s)"
|
||||
|
||||
msgid "Comment"
|
||||
msgstr "备注"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "描述"
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# The bandwidth unit is Mbit/s
|
||||
config eqos
|
||||
option enabled 0
|
||||
option download 50
|
||||
option upload 20
|
||||
|
||||
# Limiting the bandwidth of a single Device
|
||||
#config device
|
||||
# option ip "192.168.1.100"
|
||||
# option download 10
|
||||
# option upload 5
|
||||
# option comment "test"
|
||||
config eqos 'global'
|
||||
option enabled '0'
|
||||
option wan 'wan'
|
||||
option download '100'
|
||||
option upload '50'
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2006 OpenWrt.org
|
||||
# Copyright (C) 2024 kenzok78
|
||||
|
||||
START=50
|
||||
STOP=10
|
||||
USE_PROCD=0
|
||||
|
||||
parse_device() {
|
||||
local cfg="$1" ip download upload
|
||||
|
||||
config_get ip "$cfg" ip
|
||||
config_get download "$cfg" download
|
||||
config_get upload "$cfg" upload
|
||||
|
||||
eqos add $ip $download $upload
|
||||
start_service() {
|
||||
eqos start
|
||||
}
|
||||
|
||||
eqos_start() {
|
||||
local cfg="$1" enabled download upload
|
||||
|
||||
config_get_bool enabled "$cfg" enabled 0
|
||||
[ $enabled -eq 0 ] && return 0
|
||||
|
||||
config_get download "$cfg" download
|
||||
config_get upload "$cfg" upload
|
||||
|
||||
eqos start $download $upload
|
||||
|
||||
config_foreach parse_device device
|
||||
stop_service() {
|
||||
eqos stop
|
||||
}
|
||||
|
||||
start() {
|
||||
eqos stop
|
||||
|
||||
config_load eqos
|
||||
config_foreach eqos_start eqos
|
||||
reload_service() {
|
||||
eqos restart
|
||||
}
|
||||
|
||||
stop() {
|
||||
eqos stop
|
||||
}
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger eqos
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@eqos[-1]
|
||||
add ucitrack eqos
|
||||
set ucitrack.@eqos[-1].init=eqos
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
@@ -1,60 +1,194 @@
|
||||
#!/bin/sh
|
||||
# EQOS - Easy QoS for OpenWrt
|
||||
# Copyright (C) 2024 kenzok78
|
||||
# Licensed under GPL v2
|
||||
|
||||
dev=br-lan
|
||||
LOGGER="logger -t eqos"
|
||||
$LOGGER "eqos service started"
|
||||
|
||||
[ -f /etc/config/eqos ] || exit 1
|
||||
|
||||
. /lib/functions.sh
|
||||
. /lib/functions/network.sh
|
||||
|
||||
validate_ip() {
|
||||
local ip="$1"
|
||||
echo "$ip" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$' || return 1
|
||||
for octet in $(echo "$ip" | tr '.' ' '); do
|
||||
[ "$octet" -gt 255 ] 2>/dev/null && return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_speed() {
|
||||
local speed="$1"
|
||||
echo "$speed" | grep -qE '^[0-9]+$' && [ "$speed" -gt 0 ] && [ "$speed" -le 10000 ]
|
||||
}
|
||||
|
||||
get_wan_device() {
|
||||
local wan_cfg="$1"
|
||||
local wan_dev=$(uci get network.$wan_cfg.ifname 2>/dev/null)
|
||||
if [ -z "$wan_dev" ]; then
|
||||
wan_dev=$(uci get eqos.$wan_cfg.wan 2>/dev/null)
|
||||
fi
|
||||
echo "${wan_dev:-br-lan}"
|
||||
}
|
||||
|
||||
stop_qos() {
|
||||
tc qdisc del dev $dev root 2>/dev/null
|
||||
tc qdisc del dev $dev ingress 2>/dev/null
|
||||
tc qdisc del dev ${dev}-ifb root 2>/dev/null
|
||||
ip link del dev ${dev}-ifb 2>/dev/null
|
||||
local dev="$1"
|
||||
$LOGGER "Stopping QoS on device: $dev"
|
||||
tc qdisc del dev "$dev" root 2>/dev/null
|
||||
tc qdisc del dev "$dev" ingress 2>/dev/null
|
||||
tc qdisc del dev "${dev}-ifb" root 2>/dev/null
|
||||
ip link del dev "${dev}-ifb" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
start_qos() {
|
||||
local dl=$1
|
||||
local up=$2
|
||||
|
||||
tc qdisc add dev $dev root handle 1: htb
|
||||
tc class add dev $dev parent 1: classid 1:1 htb rate ${dl}mbit
|
||||
|
||||
ip link add dev ${dev}-ifb name ${dev}-ifb type ifb
|
||||
ip link set dev ${dev}-ifb up
|
||||
|
||||
tc qdisc add dev ${dev}-ifb root handle 1: htb
|
||||
tc class add dev ${dev}-ifb parent 1: classid 1:1 htb rate ${up}mbit
|
||||
|
||||
tc qdisc add dev $dev ingress
|
||||
tc filter add dev $dev parent ffff: protocol ip u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ${dev}-ifb
|
||||
local dl="$1"
|
||||
local up="$2"
|
||||
local dev="$3"
|
||||
|
||||
$LOGGER "Starting QoS: DL=$dl Mbit/s UP=$up Mbit/s DEV=$dev"
|
||||
|
||||
validate_speed "$dl" || { $LOGGER "Invalid download speed: $dl"; return 1; }
|
||||
validate_speed "$up" || { $LOGGER "Invalid upload speed: $up"; return 1; }
|
||||
|
||||
stop_qos "$dev"
|
||||
|
||||
tc qdisc add dev "$dev" root handle 1: htb 2>&1 | while read line; do $LOGGER "$line"; done
|
||||
tc class add dev "$dev" parent 1: classid 1:1 htb rate "${dl}mbit" 2>&1 | while read line; do $LOGGER "$line"; done
|
||||
|
||||
ip link add dev "${dev}-ifb" name "${dev}-ifb" type ifb 2>/dev/null
|
||||
ip link set dev "${dev}-ifb" up 2>/dev/null
|
||||
|
||||
tc qdisc add dev "${dev}-ifb" root handle 1: htb 2>&1 | while read line; do $LOGGER "$line"; done
|
||||
tc class add dev "${dev}-ifb" parent 1: classid 1:1 htb rate "${up}mbit" 2>&1 | while read line; do $LOGGER "$line"; done
|
||||
|
||||
tc qdisc add dev "$dev" ingress 2>&1 | while read line; do $LOGGER "$line"; done
|
||||
tc filter add dev "$dev" parent ffff: protocol ip u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev "${dev}-ifb" 2>&1 | while read line; do $LOGGER "$line"; done
|
||||
|
||||
$LOGGER "QoS started successfully"
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"stop")
|
||||
stop_qos
|
||||
;;
|
||||
"start")
|
||||
stop_qos
|
||||
start_qos $2 $3
|
||||
;;
|
||||
"add")
|
||||
ip="$2"
|
||||
dl="$3"
|
||||
up="$4"
|
||||
|
||||
cnt=$(tc class show dev $dev | wc -l)
|
||||
|
||||
tc class add dev $dev parent 1:1 classid 1:1$cnt htb rate ${dl}mbit ceil ${dl}mbit
|
||||
tc filter add dev $dev parent 1:0 protocol ip u32 match ip dst $ip flowid 1:1$cnt
|
||||
|
||||
tc class add dev ${dev}-ifb parent 1:1 classid 1:1$cnt htb rate ${up}mbit ceil ${up}mbit
|
||||
tc filter add dev ${dev}-ifb parent 1:0 protocol ip u32 match ip src $ip flowid 1:1$cnt
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 <command> [options]"
|
||||
echo "Commands:"
|
||||
echo " start dl_rate up_rate #Total bandwidth (Mbit/s)"
|
||||
echo " stop"
|
||||
echo " add ip dl_rate up_rate #Limiting the bandwidth of a single IP (Mbit/s)"
|
||||
echo "Example:"
|
||||
echo " $0 start 30 20 # Total bandwidth: down 30Mbit/s up 20Mbit/s"
|
||||
echo " $0 add 192.168.22.12 10 2 # down 10Mbit/s up 2Mbit/s"
|
||||
;;
|
||||
esac
|
||||
add_device() {
|
||||
local ip="$1"
|
||||
local dl="$2"
|
||||
local up="$3"
|
||||
local dev="$4"
|
||||
|
||||
validate_ip "$ip" || { $LOGGER "Invalid IP address: $ip"; return 1; }
|
||||
validate_speed "$dl" || { $LOGGER "Invalid download speed: $dl"; return 1; }
|
||||
validate_speed "$up" || { $LOGGER "Invalid upload speed: $up"; return 1; }
|
||||
|
||||
local cnt=$(tc class show dev "$dev" 2>/dev/null | grep -c "class htb" || echo "0")
|
||||
local classid=$((100 + cnt))
|
||||
|
||||
$LOGGER "Adding QoS rule: IP=$ip DL=$dl Mbit/s UP=$up Mbit/s"
|
||||
|
||||
tc class add dev "$dev" parent 1:1 classid "1:${classid}" htb rate "${dl}mbit" ceil "${dl}mbit" 2>/dev/null
|
||||
tc filter add dev "$dev" parent 1:0 protocol ip prio 1 u32 match ip dst "$ip/32" flowid "1:${classid}" 2>/dev/null
|
||||
|
||||
tc class add dev "${dev}-ifb" parent 1:1 classid "1:${classid}" htb rate "${up}mbit" ceil "${up}mbit" 2>/dev/null
|
||||
tc filter add dev "${dev}-ifb" parent 1:0 protocol ip prio 1 u32 match ip src "$ip/32" flowid "1:${classid}" 2>/dev/null
|
||||
|
||||
$LOGGER "QoS rule added for $ip"
|
||||
return 0
|
||||
}
|
||||
|
||||
flush_devices() {
|
||||
local dev="$1"
|
||||
$LOGGER "Flushing all device QoS rules"
|
||||
local class_list=$(tc class show dev "$dev" 2>/dev/null | grep -oE '1:[0-9]+' | grep -v '^1:1$' | sort -u)
|
||||
for class in $class_list; do
|
||||
local filter_list=$(tc filter show dev "$dev" 2>/dev/null | grep "$class" | grep -oE 'pref [0-9]+' | awk '{print $2}')
|
||||
for pref in $filter_list; do
|
||||
tc filter del dev "$dev" parent 1:0 prio "$pref" 2>/dev/null
|
||||
done
|
||||
tc class del dev "$dev" "$class" 2>/dev/null
|
||||
|
||||
local filter_list_ifb=$(tc filter show dev "${dev}-ifb" 2>/dev/null | grep "$class" | grep -oE 'pref [0-9]+' | awk '{print $2}')
|
||||
for pref in $filter_list_ifb; do
|
||||
tc filter del dev "${dev}-ifb" parent 1:0 prio "$pref" 2>/dev/null
|
||||
done
|
||||
tc class del dev "${dev}-ifb" "$class" 2>/dev/null
|
||||
done
|
||||
$LOGGER "All device QoS rules flushed"
|
||||
}
|
||||
|
||||
cmd="$1"
|
||||
shift
|
||||
|
||||
case "$cmd" in
|
||||
start)
|
||||
config_load eqos
|
||||
config_get enabled global enabled 0
|
||||
[ "$enabled" = "1" ] || { $LOGGER "EQOS is disabled"; exit 0; }
|
||||
|
||||
config_get wan_dev global wan "wan"
|
||||
config_get dl global download "100"
|
||||
config_get up global upload "50"
|
||||
|
||||
real_wan=$(get_wan_device "$wan_dev")
|
||||
start_qos "$dl" "$up" "$real_wan" || exit 1
|
||||
|
||||
config_foreach add_device device
|
||||
;;
|
||||
stop)
|
||||
config_load eqos
|
||||
config_get wan_dev global wan "wan"
|
||||
real_wan=$(get_wan_device "$wan_dev")
|
||||
stop_qos "$real_wan"
|
||||
;;
|
||||
restart)
|
||||
config_load eqos
|
||||
config_get wan_dev global wan "wan"
|
||||
real_wan=$(get_wan_device "$wan_dev")
|
||||
stop_qos "$real_wan"
|
||||
config_get enabled global enabled 0
|
||||
[ "$enabled" = "1" ] || exit 0
|
||||
config_get dl global download "100"
|
||||
config_get up global upload "50"
|
||||
start_qos "$dl" "$up" "$real_wan"
|
||||
;;
|
||||
add)
|
||||
local ip="$1"; local dl="$2"; local up="$3"
|
||||
[ -z "$ip" ] || [ -z "$dl" ] || [ -z "$up" ] && { $LOGGER "Usage: eqos add <ip> <dl> <up>"; exit 1; }
|
||||
|
||||
config_load eqos
|
||||
config_get wan_dev global wan "wan"
|
||||
real_wan=$(get_wan_device "$wan_dev")
|
||||
|
||||
add_device "$ip" "$dl" "$up" "$real_wan"
|
||||
;;
|
||||
flush)
|
||||
config_load eqos
|
||||
config_get wan_dev global wan "wan"
|
||||
real_wan=$(get_wan_device "$wan_dev")
|
||||
flush_devices "$real_wan"
|
||||
;;
|
||||
show)
|
||||
config_load eqos
|
||||
config_get wan_dev global wan "wan"
|
||||
real_wan=$(get_wan_device "$wan_dev")
|
||||
echo "=== QoS Classes on $real_wan ==="
|
||||
tc class show dev "$real_wan" 2>/dev/null
|
||||
echo "=== QoS Filters on $real_wan ==="
|
||||
tc filter show dev "$real_wan" 2>/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 <command> [options]"
|
||||
echo "Commands:"
|
||||
echo " start - Start EQOS service"
|
||||
echo " stop - Stop EQOS service"
|
||||
echo " restart - Restart EQOS service"
|
||||
echo " add <ip> <dl> <up> - Add IP rule (Mbit/s)"
|
||||
echo " flush - Flush all device rules"
|
||||
echo " show - Show QoS status"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 start"
|
||||
echo " $0 add 192.168.1.100 10 5"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"luci-app-eqos": {
|
||||
"description": "Grant UCI access for luci-app-eqos",
|
||||
"description": "Grant access to EQOS",
|
||||
"read": {
|
||||
"uci": [ "eqos" ]
|
||||
"uci": { "eqos": ["global", "device"] }
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "eqos" ]
|
||||
"uci": { "eqos": ["global", "device"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ LUCI_PKGARCH:=all
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
$(eval $(call BuildPackage,luci-app-fileassistant))
|
||||
|
||||
+7
-16
@@ -1,24 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// NOTE: luci-app-fileassistant 使用模板视图(luasrc/view/fileassistant.htm)
|
||||
// 此文件在 luci 23.05+ 下通过 menu.d 的 action.type=template 加载,
|
||||
// 不会直接调用本 JS view,保留此文件仅供参考。
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'get_status',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
'require form';
|
||||
|
||||
return view.extend({
|
||||
load: function() {},
|
||||
|
||||
render: function() {
|
||||
var m = new form.Map('config', _('Settings'));
|
||||
var s = m.section(form.NamedSection, 'main', 'main', _('Configuration'));
|
||||
s.anonymous = true;
|
||||
|
||||
var o = s.option(form.Value, 'name', _('Name'));
|
||||
|
||||
return m.render();
|
||||
// 实际界面由 luasrc/view/fileassistant.htm 模板提供
|
||||
return E('p', {}, _('Loading…'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,14 +13,18 @@ local ALLOWED_PATHS = {
|
||||
local MAX_UPLOAD_SIZE = 500 * 1024 * 1024
|
||||
|
||||
function index()
|
||||
entry({"admin", "nas"}, firstchild(), _("NAS"), 44).dependent = false
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册菜单项
|
||||
if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-fileassistant.json") then
|
||||
entry({"admin", "nas"}, firstchild(), _("NAS"), 44).dependent = false
|
||||
|
||||
local page
|
||||
page = entry({"admin", "nas", "fileassistant"}, template("fileassistant"), _("文件助手"), 1)
|
||||
page.i18n = "base"
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-fileassistant" }
|
||||
local page
|
||||
page = entry({"admin", "nas", "fileassistant"}, template("fileassistant"), _("文件助手"), 1)
|
||||
page.i18n = "base"
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-fileassistant" }
|
||||
end
|
||||
|
||||
-- API 路由在新旧版本均需注册(fb.js 的 AJAX 请求依赖这些路由)
|
||||
entry({"admin", "nas", "fileassistant", "list"}, call("fileassistant_list"), nil)
|
||||
entry({"admin", "nas", "fileassistant", "open"}, call("fileassistant_open"), nil)
|
||||
entry({"admin", "nas", "fileassistant", "delete"}, call("fileassistant_delete"), nil)
|
||||
@@ -67,7 +71,8 @@ function sanitize_path(path)
|
||||
if not path then
|
||||
return nil
|
||||
end
|
||||
path = path:gsub("<>", "/"):gsub("//+", "/"):gsub("/$", "")
|
||||
-- Bug fix: 原代码将 "<>" 替换为 "/",应为 "\\"(Windows 路径反斜杠)
|
||||
path = path:gsub("\\\\", "/"):gsub("//+", "/"):gsub("/$", "")
|
||||
if path == "" then
|
||||
return "/"
|
||||
end
|
||||
@@ -81,7 +86,8 @@ function fileassistant_list()
|
||||
list_response(path, false, "Path not allowed")
|
||||
return
|
||||
end
|
||||
if not nixio.fs.stat(realpath, "type") == "dir" and not nixio.fs.stat(realpath) then
|
||||
-- Bug fix: Lua 中 not 优先级高于 ==,原写法恒为 false
|
||||
if not nixio.fs.stat(realpath) or nixio.fs.stat(realpath, "type") ~= "dir" then
|
||||
list_response(path, false, "Invalid directory")
|
||||
return
|
||||
end
|
||||
@@ -218,7 +224,9 @@ function fileassistant_install()
|
||||
end
|
||||
|
||||
function installIPK(filepath)
|
||||
local output = luci.sys.exec('opkg --force-depends install "' .. filepath .. '" 2>&1')
|
||||
-- Security fix: 用单引号包裹路径,防止命令注入
|
||||
local safe_path = filepath:gsub("'", "'\\''")
|
||||
local output = luci.sys.exec("opkg --force-depends install '" .. safe_path .. "' 2>&1")
|
||||
luci.sys.exec('rm -rf /tmp/luci-*')
|
||||
if output:match("Installing") and output:match("completed") then
|
||||
return true
|
||||
@@ -233,7 +241,8 @@ function fileassistant_upload()
|
||||
list_response(uploaddir, false, "Path not allowed")
|
||||
return
|
||||
end
|
||||
if not nixio.fs.stat(realpath, "type") == "dir" then
|
||||
-- Bug fix: Lua 中 not 优先级高于 ==,原写法恒为 false
|
||||
if not nixio.fs.stat(realpath) or nixio.fs.stat(realpath, "type") ~= "dir" then
|
||||
list_response(uploaddir, false, "Invalid directory")
|
||||
return
|
||||
end
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"admin/nas": {
|
||||
"title": "NAS",
|
||||
"order": 44
|
||||
},
|
||||
"admin/nas/fileassistant": {
|
||||
"title": "文件助手",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "template",
|
||||
"path": "fileassistant"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-fileassistant" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,40 @@
|
||||
# Copyright (C) 2018-2024 OpenWrt luci-app-filebrowser contributors
|
||||
#
|
||||
# Copyright (C) 2021 ImmortalWrt
|
||||
# <https://immortalwrt.org>
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-filebrowser
|
||||
PKG_MAINTAINER:=kenzok8 <https://github.com/kenzok78>
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
LUCI_TITLE:=LuCI Support for FileBrowser
|
||||
LUCI_DEPENDS:=+filebrowser
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DESCRIPTION:=FileBrowser - a web-based file manager for your OpenWrt
|
||||
|
||||
PKG_NAME:=luci-app-filebrowser
|
||||
PKG_VERSION:=snapshot
|
||||
PKG_RELEASE:=118071b
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/filebrowser
|
||||
/etc/init.d/filebrowser
|
||||
endef
|
||||
|
||||
PKG_LICENSE:=GPLv3
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -z "$${IPKG_INSTROOT}" ] && /etc/init.d/filebrowser enable 2>/dev/null
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
/etc/init.d/filebrowser disable 2>/dev/null
|
||||
/etc/init.d/filebrowser stop 2>/dev/null
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# luci-app-filebrowser
|
||||
|
||||
适用于 OpenWrt/Lede 的 FileBrowser LuCI 控制面板插件。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **Web 文件管理**: 通过美观的 Web 界面管理服务器上的文件
|
||||
- **多架构支持**: 自动检测 x86_64、aarch64、ramips、ar71xx、armv5/6/7/8 等架构
|
||||
- **自动下载更新**: 自动从 GitHub releases 获取最新版本并下载
|
||||
- **配置管理**: 支持 SSL、自定义端口、数据库路径等
|
||||
- **日志查看**: 实时查看 FileBrowser 运行日志
|
||||
|
||||
## 依赖
|
||||
|
||||
- `luci`
|
||||
- `luci-base`
|
||||
- `filebrowser` (主程序)
|
||||
|
||||
## 安装
|
||||
|
||||
### OpenWrt Feed 方式
|
||||
|
||||
```bash
|
||||
# 添加 feed
|
||||
echo 'src-git filebrowser https://github.com/kenzok78/luci-app-filebrowser' >> feeds.conf.default
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a -p filebrowser
|
||||
|
||||
# 编译
|
||||
make package/luci-app-filebrowser/compile V=s
|
||||
```
|
||||
|
||||
### 直接编译
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kenzok78/luci-app-filebrowser.git package/luci-app-filebrowser
|
||||
make menuconfig # 选择 LuCI -> Applications -> luci-app-filebrowser
|
||||
make -j$(nproc) V=s
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 基本配置
|
||||
|
||||
1. 访问 OpenWrt Web 管理界面 → 服务 → File Browser
|
||||
2. 启用插件
|
||||
3. 设置可执行文件存放目录(建议使用 `/tmp` 或挂载的 USB 存储)
|
||||
4. 点击"手动下载"获取 FileBrowser 二进制文件
|
||||
5. 保存并应用配置
|
||||
|
||||
### 配置说明
|
||||
|
||||
| 选项 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| 监听地址 | 绑定地址 | 0.0.0.0 |
|
||||
| 监听端口 | Web 界面端口 | 8088 |
|
||||
| 数据库路径 | SQLite 数据库文件位置 | /etc/filebrowser.db |
|
||||
| 初始账户 | 登录用户名 | admin |
|
||||
| 初始密码 | 登录密码 | admin |
|
||||
| SSL 证书 | HTTPS 证书路径 | 空 |
|
||||
| SSL 私钥 | HTTPS 私钥路径 | 空 |
|
||||
| 根目录 | 文件浏览的起始目录 | /root |
|
||||
| 可执行文件目录 | 二进制文件存放位置 | /tmp |
|
||||
|
||||
## 配置示例
|
||||
|
||||
### /etc/config/filebrowser
|
||||
|
||||
```
|
||||
config global
|
||||
option address '0.0.0.0'
|
||||
option port '8088'
|
||||
option database '/etc/filebrowser.db'
|
||||
option username 'admin'
|
||||
option password 'admin'
|
||||
option ssl_cert ''
|
||||
option ssl_key ''
|
||||
option root_path '/root'
|
||||
option executable_directory '/tmp'
|
||||
option enable '1'
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
luci-app-filebrowser/
|
||||
├── luasrc/
|
||||
│ ├── controller/ # LuCI 控制器
|
||||
│ ├── model/cbi/ # CBI 模型
|
||||
│ └── view/ # HTML 视图模板
|
||||
├── po/ # 翻译文件
|
||||
├── root/
|
||||
│ ├── etc/
|
||||
│ │ ├── config/ # UCI 配置
|
||||
│ │ └── init.d/ # 启动脚本
|
||||
│ └── usr/share/rpcd/ # ACL 配置
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 下载失败怎么办?
|
||||
|
||||
检查网络连接,确保可以访问 GitHub。也可以手动下载 FileBrowser 二进制文件并放置到指定目录。
|
||||
|
||||
### Q: 如何手动下载二进制文件?
|
||||
|
||||
访问 [FileBrowser Releases](https://github.com/filebrowser/filebrowser/releases) 下载对应架构的版本,解压后放置到可执行文件目录。
|
||||
|
||||
### Q: 如何查看日志?
|
||||
|
||||
通过 LuCI 界面的日志区域查看,或:
|
||||
|
||||
```bash
|
||||
cat /var/log/filebrowser.log
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
Apache-2.0
|
||||
|
||||
## 来源
|
||||
|
||||
- [FileBrowser](https://github.com/filebrowser/filebrowser)
|
||||
- 基于 Lienol 的 luci-app-filebrowser 维护
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require form';
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.filebrowser',
|
||||
method: 'status',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.load('filebrowser');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m = new form.Map('filebrowser', _('File Browser'), _('FileBrowser - a web-based file manager'));
|
||||
|
||||
var s = m.section(form.NamedSection, 'global', 'global', _('Running Status'));
|
||||
s.anonymous = true;
|
||||
|
||||
var o = s.option(form.Value, 'port', _('Port'));
|
||||
o.optional = true;
|
||||
o.default = '8088';
|
||||
|
||||
var status = s.option(form.DummyValue, '_status', _('Status'));
|
||||
status.load = function() {
|
||||
return callStatus().then(function(res) {
|
||||
this._running = res.status;
|
||||
}.bind(this));
|
||||
};
|
||||
status.render = function() {
|
||||
var running = this._running;
|
||||
var cls = running ? 'green' : 'red';
|
||||
var text = running ? _('RUNNING') : _('NOT RUNNING');
|
||||
return E('p', {}, E('span', { style: 'color:' + cls + ';' }, text));
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1,19 +1,78 @@
|
||||
module("luci.controller.filebrowser", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/filebrowser") then
|
||||
return
|
||||
end
|
||||
entry({"admin", "nas"}, firstchild(), _("NAS") , 45).dependent = false
|
||||
local page
|
||||
page = entry({"admin", "nas", "filebrowser"}, cbi("filebrowser"), _("文件管理器"), 100)
|
||||
page.dependent = true
|
||||
entry({"admin","nas","filebrowser","status"},call("act_status")).leaf=true
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local e={}
|
||||
e.running=luci.sys.call("pgrep filebrowser >/dev/null")==0
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
module("luci.controller.filebrowser", package.seeall)
|
||||
|
||||
local http = require "luci.http"
|
||||
local fs = require "nixio.fs"
|
||||
local api = require "luci.model.cbi.filebrowser.api"
|
||||
|
||||
function index()
|
||||
if not fs.access("/etc/config/filebrowser") then return end
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
|
||||
if fs.access("/usr/share/luci/menu.d/luci-app-filebrowser.json") then return end
|
||||
|
||||
entry({"admin", "services"}, firstchild(), "Services", 44).dependent = false
|
||||
entry({"admin", "services", "filebrowser"}, cbi("filebrowser/settings"),
|
||||
_("File Browser"), 2).dependent = true
|
||||
|
||||
entry({"admin", "services", "filebrowser", "check"}, call("action_check")).leaf = true
|
||||
entry({"admin", "services", "filebrowser", "download"}, call("action_download")).leaf = true
|
||||
entry({"admin", "services", "filebrowser", "status"}, call("act_status")).leaf = true
|
||||
entry({"admin", "services", "filebrowser", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "services", "filebrowser", "clear_log"}, call("clear_log")).leaf = true
|
||||
end
|
||||
|
||||
local function http_write_json(content)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(content or {code = 1})
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local result = {}
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local exec_dir = uci:get("filebrowser", "@global[0]", "executable_directory") or "/tmp"
|
||||
local fb_bin = exec_dir .. "/filebrowser"
|
||||
|
||||
if fs.access(fb_bin) then
|
||||
result.status = (luci.sys.call("pgrep -f '" .. fb_bin .. "' >/dev/null 2>&1") == 0)
|
||||
else
|
||||
result.status = false
|
||||
end
|
||||
|
||||
http_write_json(result)
|
||||
end
|
||||
|
||||
function action_check()
|
||||
local json = api.to_check()
|
||||
http_write_json(json)
|
||||
end
|
||||
|
||||
function action_download()
|
||||
local task = http.formvalue("task")
|
||||
local json
|
||||
|
||||
if task == "extract" then
|
||||
json = api.to_extract(http.formvalue("file"))
|
||||
elseif task == "move" then
|
||||
json = api.to_move(http.formvalue("file"))
|
||||
else
|
||||
json = api.to_download(http.formvalue("url"))
|
||||
end
|
||||
|
||||
http_write_json(json)
|
||||
end
|
||||
|
||||
function get_log()
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
local log_path = "/var/log/filebrowser.log"
|
||||
if fs.access(log_path) then
|
||||
http.write(fs.readfile(log_path) or "")
|
||||
else
|
||||
http.write("")
|
||||
end
|
||||
end
|
||||
|
||||
function clear_log()
|
||||
local log_path = "/var/log/filebrowser.log"
|
||||
fs.writefile(log_path, "")
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({code = 0})
|
||||
end
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
m = Map("filebrowser", translate("文件管理器"), translate("FileBrowser是一个基于Go的在线文件管理器,助您方便的管理设备上的文件。"))
|
||||
|
||||
m:section(SimpleSection).template = "filebrowser/filebrowser_status"
|
||||
|
||||
s = m:section(TypedSection, "filebrowser")
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
|
||||
enable = s:option(Flag, "enabled", translate("启用"))
|
||||
enable.rmempty = false
|
||||
|
||||
o = s:option(ListValue, "addr_type", translate("监听地址"))
|
||||
o:value("local", translate("监听本机地址"))
|
||||
o:value("lan", translate("监听局域网地址"))
|
||||
o:value("wan", translate("监听全部地址"))
|
||||
o.default = "lan"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "port", translate("监听端口"))
|
||||
o.placeholder = 8989
|
||||
o.default = 8989
|
||||
o.datatype = "port"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "root_dir", translate("开放目录"))
|
||||
o.placeholder = "/"
|
||||
o.default = "/"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "db_dir", translate("数据库目录"))
|
||||
o.placeholder = "/etc"
|
||||
o.default = "/etc"
|
||||
o.rmempty = false
|
||||
o.description = translate("普通用户请勿随意更改")
|
||||
|
||||
o = s:option(Value, "db_name", translate("数据库名"))
|
||||
o.placeholder = "filebrowser.db"
|
||||
o.default = "filebrowser.db"
|
||||
o.rmempty = false
|
||||
o.description = translate("普通用户请勿随意更改")
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,383 @@
|
||||
local fs = require "nixio.fs"
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local util = require "luci.util"
|
||||
local i18n = require "luci.i18n"
|
||||
|
||||
module("luci.model.cbi.filebrowser.api", package.seeall)
|
||||
|
||||
local appname = "filebrowser"
|
||||
local api_url = "https://api.github.com/repos/filebrowser/filebrowser/releases/latest"
|
||||
|
||||
local function get_downloader()
|
||||
if fs.access("/usr/bin/curl") or fs.access("/bin/curl") then
|
||||
return "/usr/bin/curl", {
|
||||
"-L", "-k", "--retry", "2", "--connect-timeout", "10", "-o"
|
||||
}
|
||||
end
|
||||
if fs.access("/usr/bin/wget") then
|
||||
return "/usr/bin/wget", {
|
||||
"--no-check-certificate", "--quiet", "--timeout=10", "--tries=2", "-O"
|
||||
}
|
||||
end
|
||||
return nil, {}
|
||||
end
|
||||
|
||||
local command_timeout = 300
|
||||
|
||||
local lede_board = nil
|
||||
local distrib_target = nil
|
||||
|
||||
local function uci_get_type(t, config, default)
|
||||
local value
|
||||
value = uci:get_first(appname, t, config)
|
||||
if not value or value == "" then
|
||||
value = sys.exec("uci -q get " .. appname .. ".@" .. t .. "[0]." .. config)
|
||||
end
|
||||
if (value == nil or value == "") and default and default ~= "" then
|
||||
value = default
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function exec(cmd, args, writer, timeout)
|
||||
local os = require "os"
|
||||
local nixio = require "nixio"
|
||||
|
||||
local fdi, fdo = nixio.pipe()
|
||||
local pid = nixio.fork()
|
||||
|
||||
if pid > 0 then
|
||||
fdo:close()
|
||||
|
||||
if writer or timeout then
|
||||
local starttime = os.time()
|
||||
while true do
|
||||
if timeout and os.difftime(os.time(), starttime) >= timeout then
|
||||
nixio.kill(pid, nixio.const.SIGTERM)
|
||||
return 1
|
||||
end
|
||||
|
||||
if writer then
|
||||
local buffer = fdi:read(2048)
|
||||
if buffer and #buffer > 0 then
|
||||
writer(buffer)
|
||||
end
|
||||
end
|
||||
|
||||
local wpid, stat, code = nixio.waitpid(pid, "nohang")
|
||||
|
||||
if wpid and stat == "exited" then return code end
|
||||
|
||||
if not writer and timeout then nixio.nanosleep(1) end
|
||||
end
|
||||
else
|
||||
local wpid, stat, code = nixio.waitpid(pid)
|
||||
return wpid and stat == "exited" and code
|
||||
end
|
||||
elseif pid == 0 then
|
||||
nixio.dup(fdo, nixio.stdout)
|
||||
fdi:close()
|
||||
fdo:close()
|
||||
nixio.exece(cmd, args, nil)
|
||||
nixio.stdout:close()
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function compare_versions(ver1, comp, ver2)
|
||||
local av1 = util.split(ver1, "[%.%-]", nil, true)
|
||||
local av2 = util.split(ver2, "[%.%-]", nil, true)
|
||||
|
||||
local max = #av1
|
||||
local n2 = #av2
|
||||
if max < n2 then max = n2 end
|
||||
|
||||
for i = 1, max do
|
||||
local s1 = av1[i] or ""
|
||||
local s2 = av2[i] or ""
|
||||
|
||||
if comp == "~=" and (s1 ~= s2) then return true end
|
||||
if (comp == "<" or comp == "<=") and (s1 < s2) then return true end
|
||||
if (comp == ">" or comp == ">=") and (s1 > s2) then return true end
|
||||
if (s1 ~= s2) then return false end
|
||||
end
|
||||
|
||||
return not (comp == "<" or comp == ">")
|
||||
end
|
||||
|
||||
local function auto_get_arch()
|
||||
local nixio = require "nixio"
|
||||
local arch = nixio.uname().machine or ""
|
||||
|
||||
if fs.access("/etc/openwrt_release") then
|
||||
local target = sys.exec("grep 'DISTRIB_TARGET=' /etc/openwrt_release 2>/dev/null | cut -d\"'\" -f2 | head -1")
|
||||
distrib_target = target
|
||||
end
|
||||
|
||||
if fs.access("/usr/lib/os-release") then
|
||||
local board = sys.exec("grep 'OPENWRT_BOARD=' /usr/lib/os-release 2>/dev/null | cut -d\"'\" -f2 | head -1")
|
||||
if board and board ~= "" then
|
||||
lede_board = board
|
||||
end
|
||||
end
|
||||
|
||||
if arch == "mips" then
|
||||
local target_str = distrib_target or lede_board or ""
|
||||
if target_str:match("ramips") then
|
||||
arch = "ramips"
|
||||
elseif target_str:match("ar71xx") then
|
||||
arch = "ar71xx"
|
||||
end
|
||||
end
|
||||
|
||||
return util.trim(arch)
|
||||
end
|
||||
|
||||
local function get_file_info(arch)
|
||||
local file_tree = ""
|
||||
local sub_version = ""
|
||||
|
||||
if arch == "x86_64" then
|
||||
file_tree = "amd64"
|
||||
elseif arch == "aarch64" then
|
||||
file_tree = "arm64"
|
||||
elseif arch == "ramips" then
|
||||
file_tree = "mipsle"
|
||||
elseif arch == "ar71xx" then
|
||||
file_tree = "mips"
|
||||
elseif arch:match("^i[%d]86$") then
|
||||
file_tree = "386"
|
||||
elseif arch:match("^armv[5-8]") then
|
||||
file_tree = "armv"
|
||||
sub_version = arch:match("armv([5-8])")
|
||||
local target_str = lede_board or distrib_target or ""
|
||||
if target_str:match("bcm53xx") then
|
||||
sub_version = "5"
|
||||
end
|
||||
end
|
||||
|
||||
return file_tree, sub_version
|
||||
end
|
||||
|
||||
local function get_api_json(url)
|
||||
local jsonc = require "luci.jsonc"
|
||||
|
||||
local downloader, args = get_downloader()
|
||||
if not downloader then
|
||||
return {}
|
||||
end
|
||||
|
||||
local tmpfile = "/tmp/filebrowser_api_json"
|
||||
if downloader:match("curl") then
|
||||
local ret = sys.call(downloader .. " -L -k --connect-timeout 10 -s -o " .. tmpfile .. " " .. url)
|
||||
if ret ~= 0 then
|
||||
return {}
|
||||
end
|
||||
else
|
||||
local ret = sys.call(downloader .. " --no-check-certificate --timeout=10 -t 2 -O " .. tmpfile .. " " .. url)
|
||||
if ret ~= 0 then
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
local content = fs.readfile(tmpfile) or ""
|
||||
fs.remove(tmpfile)
|
||||
|
||||
if content == "" then return {} end
|
||||
|
||||
local json, err = jsonc.parse(content)
|
||||
return json or {}
|
||||
end
|
||||
|
||||
function get_version()
|
||||
return uci_get_type("global", "version", "0")
|
||||
end
|
||||
|
||||
function to_check(arch)
|
||||
if not arch or arch == "" then
|
||||
arch = auto_get_arch()
|
||||
end
|
||||
|
||||
local file_tree, sub_version = get_file_info(arch)
|
||||
|
||||
if file_tree == "" then
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translate("Can't determine ARCH, or ARCH not supported.")
|
||||
}
|
||||
end
|
||||
|
||||
local json = get_api_json(api_url)
|
||||
|
||||
if not json or json.tag_name == nil then
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translate("Get remote version info failed.")
|
||||
}
|
||||
end
|
||||
|
||||
local remote_version = json.tag_name:match("[^v]+")
|
||||
local needs_update = compare_versions(get_version(), "<", remote_version)
|
||||
local html_url, download_url
|
||||
|
||||
if needs_update then
|
||||
html_url = json.html_url
|
||||
for _, v in ipairs(json.assets or {}) do
|
||||
if v.name then
|
||||
local pattern = "linux%-" .. file_tree
|
||||
if sub_version and sub_version ~= "" then
|
||||
pattern = pattern .. sub_version .. "$"
|
||||
end
|
||||
if v.name:match(pattern) then
|
||||
download_url = v.browser_download_url
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if needs_update and not download_url then
|
||||
return {
|
||||
code = 1,
|
||||
version = remote_version,
|
||||
html_url = html_url,
|
||||
error = i18n.translate("New version found, but failed to get new version download url.")
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
code = 0,
|
||||
update = needs_update,
|
||||
version = remote_version,
|
||||
url = {html = html_url, download = download_url}
|
||||
}
|
||||
end
|
||||
|
||||
function to_download(url)
|
||||
if not url or url == "" then
|
||||
return {code = 1, error = i18n.translate("Download url is required.")}
|
||||
end
|
||||
|
||||
sys.call("rm -f /tmp/filebrowser_download.*")
|
||||
|
||||
local tmp_file = sys.exec("mktemp -u -t filebrowser_download.XXXXXX 2>/dev/null")
|
||||
if not tmp_file or tmp_file == "" then
|
||||
return {code = 1, error = i18n.translate("Failed to create temp file.")}
|
||||
end
|
||||
tmp_file = util.trim(tmp_file)
|
||||
|
||||
local downloader, args = get_downloader()
|
||||
if not downloader then
|
||||
return {code = 1, error = i18n.translate("No downloader available (curl or wget).")}
|
||||
end
|
||||
|
||||
local outfile = "/tmp/filebrowser_download.bin"
|
||||
local cmd
|
||||
if downloader:match("curl") then
|
||||
cmd = downloader .. " -L -k --connect-timeout 10 -o " .. outfile .. " " .. url .. " 2>/dev/null"
|
||||
else
|
||||
cmd = downloader .. " --no-check-certificate --timeout=10 -t 2 -O " .. outfile .. " " .. url .. " 2>/dev/null"
|
||||
end
|
||||
|
||||
local ret = sys.call(cmd)
|
||||
if ret ~= 0 or not fs.access(outfile) then
|
||||
sys.call("rm -f " .. outfile)
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translatef("File download failed or timed out: %s", url)
|
||||
}
|
||||
end
|
||||
|
||||
return {code = 0, file = outfile}
|
||||
end
|
||||
|
||||
function to_extract(file)
|
||||
if not file or file == "" or not fs.access(file) then
|
||||
return {code = 1, error = i18n.translate("File path required.")}
|
||||
end
|
||||
|
||||
sys.call("rm -rf /tmp/filebrowser_extract.*")
|
||||
|
||||
local tmp_dir = sys.exec("mktemp -d -t filebrowser_extract.XXXXXX 2>/dev/null")
|
||||
if not tmp_dir or tmp_dir == "" then
|
||||
return {code = 1, error = i18n.translate("Failed to create temp directory.")}
|
||||
end
|
||||
tmp_dir = util.trim(tmp_dir)
|
||||
|
||||
local output = {}
|
||||
exec("/bin/tar", {"-C", tmp_dir, "-zxvf", file},
|
||||
function(chunk)
|
||||
if chunk then
|
||||
output[#output + 1] = chunk
|
||||
end
|
||||
end)
|
||||
|
||||
local files = util.split(table.concat(output))
|
||||
|
||||
exec("/bin/rm", {"-f", file})
|
||||
|
||||
local new_file
|
||||
for _, f in pairs(files) do
|
||||
if f and f:match("filebrowser") then
|
||||
local candidate = tmp_dir .. "/" .. util.trim(f)
|
||||
if fs.access(candidate) then
|
||||
new_file = candidate
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not new_file then
|
||||
exec("/bin/rm", {"-rf", tmp_dir})
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translatef("Can't find client in file: %s", file)
|
||||
}
|
||||
end
|
||||
|
||||
return {code = 0, file = new_file}
|
||||
end
|
||||
|
||||
function to_move(file)
|
||||
if not file or file == "" or not fs.access(file) then
|
||||
sys.call("rm -rf /tmp/filebrowser_extract.*")
|
||||
return {code = 1, error = i18n.translate("Client file is required.")}
|
||||
end
|
||||
|
||||
local executable_directory = uci_get_type("global", "executable_directory", "/tmp")
|
||||
|
||||
if not fs.access(executable_directory) then
|
||||
fs.mkdir(executable_directory)
|
||||
end
|
||||
|
||||
local client_path = executable_directory .. "/filebrowser"
|
||||
local client_path_bak
|
||||
|
||||
if fs.access(client_path) then
|
||||
client_path_bak = "/tmp/filebrowser.bak"
|
||||
exec("/bin/mv", {"-f", client_path, client_path_bak})
|
||||
end
|
||||
|
||||
local result = exec("/bin/mv", {"-f", file, client_path}, nil, command_timeout) == 0
|
||||
|
||||
if not result or not fs.access(client_path) then
|
||||
if client_path_bak and fs.access(client_path_bak) then
|
||||
exec("/bin/mv", {"-f", client_path_bak, client_path})
|
||||
end
|
||||
return {
|
||||
code = 1,
|
||||
error = i18n.translatef("Can't move new file to path: %s", client_path)
|
||||
}
|
||||
end
|
||||
|
||||
exec("/bin/chmod", {"755", client_path})
|
||||
|
||||
if client_path_bak then
|
||||
exec("/bin/rm", {"-f", client_path_bak})
|
||||
end
|
||||
|
||||
sys.call("rm -rf /tmp/filebrowser_extract.*")
|
||||
|
||||
return {code = 0}
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
m = Map("filebrowser",
|
||||
translate("FileBrowser"),
|
||||
translate("File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"))
|
||||
|
||||
m:append(Template("filebrowser/status"))
|
||||
|
||||
s = m:section(TypedSection, "global", translate("Global Settings"))
|
||||
s.anonymous = true
|
||||
s.addremove = false
|
||||
|
||||
o = s:option(Flag, "enable", translate("Enable"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "address", translate("Listen address"))
|
||||
o.default = "0.0.0.0"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "port", translate("Listen port"))
|
||||
o.datatype = "port"
|
||||
o.default = "8088"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "database", translate("Database path"))
|
||||
o.default = "/etc/filebrowser.db"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "username", translate("Initial username"))
|
||||
o.default = "admin"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "password", translate("Initial password"))
|
||||
o.default = "admin"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "ssl_cert", translate("SSL cert"))
|
||||
o.default = ""
|
||||
|
||||
o = s:option(Value, "ssl_key", translate("SSL key"))
|
||||
o.default = ""
|
||||
|
||||
o = s:option(Value, "root_path", translate("Root path"),
|
||||
translate("Point to a path to access your files in the web interface, default is /root"))
|
||||
o.default = "/root"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "executable_directory", translate("Executable directory"),
|
||||
translate("The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory.<br />For example: /mnt/sda1<br />For example: /tmp"))
|
||||
o.default = "/tmp"
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Button, "_download", translate("Manually download"),
|
||||
translate("Make sure you have enough space.<br /><font style='color:red'>Be sure to fill out the executable storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"))
|
||||
o.template = "filebrowser/download"
|
||||
o.inputstyle = "apply"
|
||||
o.id = "download_btn"
|
||||
|
||||
m:append(Template("filebrowser/log"))
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,171 @@
|
||||
<%
|
||||
local dsp = require "luci.dispatcher"
|
||||
-%>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
(function() {
|
||||
var msgInfo = null;
|
||||
|
||||
var tokenStr = '<%=token%>';
|
||||
var clickToDownloadText = '<%:Click to download%>';
|
||||
var inProgressText = '<%:Downloading...%>';
|
||||
var downloadInProgressNotice = '<%:Download, are you sure to close?%>';
|
||||
var downloadSuccessText = '<%:Download successful%>';
|
||||
var unexpectedErrorText = '<%:Unexpected error%>';
|
||||
|
||||
function addPageNotice() {
|
||||
window.onbeforeunload = function(e) {
|
||||
e.returnValue = downloadInProgressNotice;
|
||||
return downloadInProgressNotice;
|
||||
};
|
||||
}
|
||||
|
||||
function removePageNotice() {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
function onUpdateSuccess(btn) {
|
||||
alert(downloadSuccessText);
|
||||
|
||||
if (btn) {
|
||||
btn.value = downloadSuccessText;
|
||||
btn.placeholder = downloadSuccessText;
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
window.setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function onRequestError(btn, errorMessage) {
|
||||
if (!btn) return;
|
||||
btn.disabled = false;
|
||||
btn.value = btn.placeholder || clickToDownloadText;
|
||||
|
||||
if (errorMessage) {
|
||||
alert(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function doAjaxGet(url, data, onResult) {
|
||||
new XHR().get(url, data, function(_, json) {
|
||||
var resultJson = json || {
|
||||
code: 1,
|
||||
error: unexpectedErrorText
|
||||
};
|
||||
|
||||
if (typeof onResult === 'function') {
|
||||
onResult(resultJson);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkUpdate(btn) {
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
btn.value = inProgressText;
|
||||
|
||||
addPageNotice();
|
||||
|
||||
var detailEl = document.getElementById((btn.id || btn.name) + '-detail');
|
||||
|
||||
doAjaxGet('<%=dsp.build_url("admin/services/filebrowser/check")%>/', {
|
||||
token: tokenStr
|
||||
}, function(json) {
|
||||
removePageNotice();
|
||||
|
||||
if (json.code) {
|
||||
msgInfo = null;
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
msgInfo = json;
|
||||
btn.disabled = false;
|
||||
btn.value = clickToDownloadText;
|
||||
btn.placeholder = clickToDownloadText;
|
||||
}
|
||||
|
||||
if (detailEl && json.version) {
|
||||
var urlNode = '<em style="color:red;"><%:The latest version:%>' + json.version + '</em>';
|
||||
if (json.url && json.url.html) {
|
||||
urlNode = '<a href="' + json.url.html + '" target="_blank">' + urlNode + '</a>';
|
||||
}
|
||||
detailEl.innerHTML = urlNode;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function doDownload(btn) {
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Downloading...%>';
|
||||
|
||||
addPageNotice();
|
||||
|
||||
var updateUrl = '<%=dsp.build_url("admin/services/filebrowser/download")%>';
|
||||
|
||||
doAjaxGet(updateUrl, {
|
||||
token: tokenStr,
|
||||
url: msgInfo && msgInfo.url ? msgInfo.url.download : ''
|
||||
}, function(json) {
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
return;
|
||||
}
|
||||
|
||||
btn.value = '<%:Unpacking...%>';
|
||||
|
||||
doAjaxGet(updateUrl, {
|
||||
token: tokenStr,
|
||||
task: 'extract',
|
||||
file: json.file
|
||||
}, function(json) {
|
||||
if (json.code) {
|
||||
removePageNotice();
|
||||
onRequestError(btn, json.error);
|
||||
return;
|
||||
}
|
||||
|
||||
btn.value = '<%:Moving...%>';
|
||||
|
||||
doAjaxGet(updateUrl, {
|
||||
token: tokenStr,
|
||||
task: 'move',
|
||||
file: json.file
|
||||
}, function(json) {
|
||||
removePageNotice();
|
||||
if (json.code) {
|
||||
onRequestError(btn, json.error);
|
||||
} else {
|
||||
onUpdateSuccess(btn);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.downloadClick = function(btn) {
|
||||
if (msgInfo === null || msgInfo === undefined) {
|
||||
checkUpdate(btn);
|
||||
} else {
|
||||
doDownload(btn);
|
||||
}
|
||||
};
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valueheader%>
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" type="button"<%=
|
||||
attr("name", cbid) ..
|
||||
attr("id", self.id or cbid) ..
|
||||
attr("value", self.inputtitle or self.title) ..
|
||||
ifattr(self.placeholder, "placeholder")
|
||||
%> />
|
||||
<span id="<%=self.id or cbid%>-detail"></span>
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(1, '<%=url([[admin]], [[nas]], [[filebrowser]], [[status]])%>', null,
|
||||
function(x, data) {
|
||||
var tb = document.getElementById('filebrowser_status');
|
||||
if (data && tb) {
|
||||
if (data.running) {
|
||||
var links = '<font color=green>Filebrowser <%:运行中%></font><input class="cbi-button mar-10" type="button" value="<%:打开管理界面%>" onclick="openClient();" />';
|
||||
tb.innerHTML = links;
|
||||
} else {
|
||||
tb.innerHTML = '<font color=red>Filebrowser <%:未运行%></font>';
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function openClient() {
|
||||
var curWwwPath = window.document.location.href;
|
||||
var pathName = window.document.location.pathname;
|
||||
var pos = curWwwPath.indexOf(pathName);
|
||||
var localhostPath = curWwwPath.substring(0, pos);
|
||||
var clientPort = window.document.getElementById("cbid.filebrowser.config.port").value
|
||||
var url = localhostPath + ":" + clientPort;
|
||||
window.open(url)
|
||||
};
|
||||
//]]>
|
||||
</script>
|
||||
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
|
||||
<fieldset class="cbi-section">
|
||||
<p id="filebrowser_status">
|
||||
<em><%:Collecting data...%></em>
|
||||
</p>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
function clear_log(btn) {
|
||||
XHR.get('<%=url([[admin]], [[services]], [[filebrowser]], [[clear_log]])%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
var log_textarea = document.getElementById('log_textarea');
|
||||
log_textarea.innerHTML = "";
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[filebrowser]], [[get_log]])%>', null,
|
||||
function(x, data) {
|
||||
if(x && x.status == 200) {
|
||||
var log_textarea = document.getElementById('log_textarea');
|
||||
log_textarea.innerHTML = x.responseText;
|
||||
log_textarea.scrollTop = log_textarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<fieldset class="cbi-section" id="_log_fieldset">
|
||||
<legend>
|
||||
<%:Logs%>
|
||||
</legend>
|
||||
<input class="cbi-button cbi-input-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>" style="margin-left: 10px;">
|
||||
<textarea id="log_textarea" class="cbi-input-textarea" style="width: calc(100% - 20px); margin: 10px;" data-update="change" rows="5" wrap="off" readonly="readonly"></textarea>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,46 @@
|
||||
<%
|
||||
local dsp = require "luci.dispatcher"
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local exec_dir = uci:get("filebrowser", "@global[0]", "executable_directory") or "/tmp"
|
||||
local fb_bin = exec_dir .. "/filebrowser"
|
||||
-%>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<legend><%:Running Status%></legend>
|
||||
<fieldset class="cbi-section">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title"><%:Status%></label>
|
||||
<div class="cbi-value-field" id="_status"><p><span><%:Collecting data...%></span></p></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
(function() {
|
||||
var statusEl = document.getElementById('_status');
|
||||
XHR.poll(3, '<%=dsp.build_url("admin/services/filebrowser/status")%>', null,
|
||||
function(x, json) {
|
||||
if (x && x.status == 200 && statusEl) {
|
||||
if (json.status) {
|
||||
statusEl.innerHTML = '<p><span style="color:green;"><%:RUNNING%></span> <input type="button" class="cbi-button cbi-input-apply" value="<%:Enter interface%>" onclick="openwebui()" /></p>';
|
||||
} else {
|
||||
statusEl.innerHTML = '<p><span style="color:red;"><%:NOT RUNNING%></span></p>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.openwebui = function() {
|
||||
var url = window.location.hostname;
|
||||
var port = '<%=uci:get("filebrowser", "@global[0]", "port") or "8088"%>';
|
||||
var sslCert = '<%=uci:get("filebrowser", "@global[0]", "ssl_cert") or ""%>';
|
||||
var sslKey = '<%=uci:get("filebrowser", "@global[0]", "ssl_key") or ""%>';
|
||||
var protocol = 'http';
|
||||
if (sslCert !== '' && sslKey !== '') {
|
||||
protocol = 'https';
|
||||
}
|
||||
window.open(protocol + '://' + url + ':' + port, '_blank');
|
||||
};
|
||||
})();
|
||||
//]]></script>
|
||||
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -0,0 +1,125 @@
|
||||
msgid "File Browser"
|
||||
msgstr "文件浏览器"
|
||||
|
||||
msgid "File explorer is software that creates your own cloud that you can install on a server, point it to a path, and then access your files through a beautiful web interface. You have many features available!"
|
||||
msgstr "文件浏览器是一种创建你自己的云的软件,你可以在服务器上安装它,将它指向一个路径,然后通过一个漂亮的web界面访问你的文件。您有许多可用的特性!"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Enter interface"
|
||||
msgstr "进入界面"
|
||||
|
||||
msgid "Global Settings"
|
||||
msgstr "全局设置"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Listen address"
|
||||
msgstr "监听地址"
|
||||
|
||||
msgid "Listen port"
|
||||
msgstr "监听端口"
|
||||
|
||||
msgid "Initial username"
|
||||
msgstr "初始账户"
|
||||
|
||||
msgid "Initial password"
|
||||
msgstr "初始密码"
|
||||
|
||||
msgid "SSL cert"
|
||||
msgstr "SSL 证书"
|
||||
|
||||
msgid "SSL key"
|
||||
msgstr "SSL 私钥"
|
||||
|
||||
msgid "Database path"
|
||||
msgstr "数据库路径"
|
||||
|
||||
msgid "Root path"
|
||||
msgstr "指向路径"
|
||||
|
||||
msgid "Point to a path to access your files in the web interface, default is /root"
|
||||
msgstr "指向一个路径,可在web界面访问你的文件,默认为 /root"
|
||||
|
||||
msgid "Executable directory"
|
||||
msgstr "可执行文件存放目录"
|
||||
|
||||
msgid "The file size is large, requiring at least 32M space. It is recommended to insert a usb flash drive or hard disk, or use it in the tmp directory<br />For example, /mnt/sda1<br />For example, /tmp"
|
||||
msgstr "文件较大,至少需要32M空间。建议插入U盘或硬盘,或放入tmp目录里使用<br />例如:/mnt/sda1<br />例如:/tmp"
|
||||
|
||||
msgid "Manually download"
|
||||
msgstr "手动下载"
|
||||
|
||||
msgid "Make sure you have enough space. <br /><font style='color:red'>Be sure to fill out the executable storage directory the first time you run it, and then save the application. Then manually download, otherwise can not use!</font>"
|
||||
msgstr "请确保具有足够的空间。<br /><font style='color:red'>第一次运行务必填好项目存放目录,然后保存应用。再手动下载,否则无法使用!</font>"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Clear logs"
|
||||
msgstr "清空日志"
|
||||
|
||||
msgid "It is the latest version"
|
||||
msgstr "已是最新版本"
|
||||
|
||||
msgid "Download successful"
|
||||
msgstr "下载成功"
|
||||
|
||||
msgid "Click to download"
|
||||
msgstr "点击下载"
|
||||
|
||||
msgid "Updating..."
|
||||
msgstr "更新中"
|
||||
|
||||
msgid "Unexpected error"
|
||||
msgstr "意外错误"
|
||||
|
||||
msgid "Download, are you sure to close?"
|
||||
msgstr "正在下载,你确认要关闭吗?"
|
||||
|
||||
msgid "Downloading..."
|
||||
msgstr "下载中"
|
||||
|
||||
msgid "Unpacking..."
|
||||
msgstr "解压中"
|
||||
|
||||
msgid "Moving..."
|
||||
msgstr "移动中"
|
||||
|
||||
msgid "The latest version:"
|
||||
msgstr "最新版本:"
|
||||
|
||||
msgid "Can't determine ARCH, or ARCH not supported."
|
||||
msgstr "无法确认ARCH架构,或是不支持。"
|
||||
|
||||
msgid "Get remote version info failed."
|
||||
msgstr "获取远程版本信息失败。"
|
||||
|
||||
msgid "New version found, but failed to get new version download url."
|
||||
msgstr "发现新版本,但未能获得新版本的下载地址。"
|
||||
|
||||
msgid "Download url is required."
|
||||
msgstr "请指定下载地址。"
|
||||
|
||||
msgid "File download failed or timed out: %s"
|
||||
msgstr "文件下载失败或超时:%s"
|
||||
|
||||
msgid "File path required."
|
||||
msgstr "请指定文件路径。"
|
||||
|
||||
msgid "Can't find client in file: %s"
|
||||
msgstr "无法在文件中找到客户端:%s"
|
||||
|
||||
msgid "Client file is required."
|
||||
msgstr "请指定客户端文件。"
|
||||
|
||||
msgid "The client file is not suitable for current device."
|
||||
msgstr "客户端文件不适合当前设备。"
|
||||
|
||||
msgid "Can't move new file to path: %s"
|
||||
msgstr "无法移动新文件到:%s"
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
config global
|
||||
option address '0.0.0.0'
|
||||
option port '8088'
|
||||
option database '/etc/filebrowser.db'
|
||||
option username 'admin'
|
||||
option password 'admin'
|
||||
option ssl_cert ''
|
||||
option ssl_key ''
|
||||
option root_path '/root'
|
||||
option executable_directory '/tmp'
|
||||
option enable '0'
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
|
||||
LOG_PATH="/var/log/filebrowser.log"
|
||||
|
||||
echolog() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $1" >> $LOG_PATH
|
||||
}
|
||||
|
||||
config_t_get() {
|
||||
local index=0
|
||||
[ -n "$4" ] && index="$4"
|
||||
local ret
|
||||
ret="$(uci get "filebrowser.@$1[$index].$2" 2>/dev/null)"
|
||||
[ -z "$ret" ] && ret="$3"
|
||||
echo "$ret"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
enabled="$(config_t_get global enable 0)"
|
||||
[ "$enabled" = "0" ] && return
|
||||
|
||||
local address
|
||||
address="$(config_t_get global address 0.0.0.0)"
|
||||
local port
|
||||
port="$(config_t_get global port 8088)"
|
||||
local database
|
||||
database="$(config_t_get global database /etc/filebrowser.db)"
|
||||
local username
|
||||
username="$(config_t_get global username admin)"
|
||||
local password
|
||||
password="$(config_t_get global password admin)"
|
||||
local ssl_cert
|
||||
ssl_cert="$(config_t_get global ssl_cert '')"
|
||||
local ssl_key
|
||||
ssl_key="$(config_t_get global ssl_key '')"
|
||||
local root_path
|
||||
root_path="$(config_t_get global root_path /root)"
|
||||
local executable_directory
|
||||
executable_directory="$(config_t_get global executable_directory /tmp)"
|
||||
|
||||
[ ! -f "$executable_directory/filebrowser" ] && {
|
||||
echolog "$executable_directory/filebrowser not found, please download first"
|
||||
return 1
|
||||
}
|
||||
|
||||
local ssl_params=""
|
||||
if [ -n "$ssl_cert" ] && [ -n "$ssl_key" ]; then
|
||||
ssl_params="--ssl-cert $ssl_cert --ssl-key $ssl_key"
|
||||
fi
|
||||
|
||||
local hash_pass
|
||||
hash_pass="$("$executable_directory/filebrowser" hash "$password" 2>/dev/null)"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_PATH")"
|
||||
"$executable_directory/filebrowser" \
|
||||
-a "$address" \
|
||||
-p "$port" \
|
||||
-r "$root_path" \
|
||||
-d "$database" \
|
||||
--username "$username" \
|
||||
--password "$hash_pass" \
|
||||
$ssl_params \
|
||||
-l "$LOG_PATH" >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
local executable_directory
|
||||
executable_directory="$(config_t_get global executable_directory /tmp)"
|
||||
local fb_bin="${executable_directory}/filebrowser"
|
||||
|
||||
local pids
|
||||
pids="$(pgrep -f "$fb_bin" 2>/dev/null)"
|
||||
if [ -n "$pids" ]; then
|
||||
for pid in $pids; do
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
done
|
||||
sleep 2
|
||||
pids="$(pgrep -f "$fb_bin" 2>/dev/null)"
|
||||
[ -n "$pids" ] && kill -KILL $pids 2>/dev/null
|
||||
fi
|
||||
|
||||
rm -f "$LOG_PATH"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop_service
|
||||
sleep 1
|
||||
start_service
|
||||
}
|
||||
|
||||
start() {
|
||||
start_service
|
||||
}
|
||||
|
||||
stop() {
|
||||
stop_service
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop_service
|
||||
sleep 1
|
||||
start_service
|
||||
}
|
||||
Executable → Regular
+11
-11
@@ -1,11 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@filebrowser[-1]
|
||||
add ucitrack filebrowser
|
||||
set ucitrack.@filebrowser[-1].init=filebrowser
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@filebrowser[-1]
|
||||
add ucitrack filebrowser
|
||||
set ucitrack.@filebrowser[-1].init=filebrowser
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"admin/services/filebrowser": {
|
||||
"title": "File Browser",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "filebrowser/status"
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-filebrowser": {
|
||||
"description": "Grant UCI access for luci-app-filebrowser",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.filebrowser": [ "status" ]
|
||||
},
|
||||
"uci": [ "filebrowser" ],
|
||||
"file": {
|
||||
"/var/log/filebrowser.log": [ "read" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "filebrowser" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { popen, access } from 'fs';
|
||||
|
||||
function find_binary() {
|
||||
const uci = cursor();
|
||||
const exec_dir = uci.get('filebrowser', '@global[0]', 'executable_directory') || '/tmp';
|
||||
const fb_bin = exec_dir + '/filebrowser';
|
||||
return access(fb_bin) ? fb_bin : null;
|
||||
}
|
||||
|
||||
return {
|
||||
status: {
|
||||
call: function() {
|
||||
const fb_bin = find_binary();
|
||||
if (!fb_bin) return { status: false, version: '' };
|
||||
|
||||
const f = popen(`pgrep -f '${replace(fb_bin, "'", "'\\''")}' >/dev/null 2>&1; echo $?`);
|
||||
if (!f) return { status: false };
|
||||
const code = trim(f.read('all'));
|
||||
f.close();
|
||||
|
||||
let version = '';
|
||||
const vf = popen(`"${fb_bin}" version 2>/dev/null | head -1`);
|
||||
if (vf) {
|
||||
version = trim(vf.read('all'));
|
||||
vf.close();
|
||||
}
|
||||
|
||||
return { status: (code === '0'), version: version };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,21 +1,13 @@
|
||||
# Copyright (C) 2020 Openwrt.org
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# This is a free software, use it under GNU General Public License v3.0.
|
||||
#
|
||||
# Created By ImmortalWrt
|
||||
# https://github.com/project-openwrt
|
||||
# Copyright (C) 2025 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-gost
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=1
|
||||
LUCI_TITLE:=LuCI support for Gost
|
||||
LUCI_TITLE:=LuCI support for GOST
|
||||
LUCI_DEPENDS:=+gost
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_MAINTAINER:=ImmortalWrt
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
$(eval $(call BuildPackage,luci-app-gost))
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# luci-app-gost
|
||||
|
||||
适用于 OpenWrt/Lede 的 GOST 隧道 LuCI 控制面板插件。
|
||||
|
||||
## 简介
|
||||
|
||||
GOST (GO Simple Tunnel) 是一个用 Go 语言编写的简单安全隧道工具,支持多种协议和转发方式。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **隧道服务**: 支持多种协议的隧道转发
|
||||
- **配置管理**: 支持配置文件和命令行参数两种方式
|
||||
- **状态监控**: 实时显示服务运行状态
|
||||
- **动态参数**: 支持自定义启动参数
|
||||
|
||||
## 依赖
|
||||
|
||||
- `luci`
|
||||
- `luci-base`
|
||||
- `gost` (主程序)
|
||||
|
||||
## 安装
|
||||
|
||||
### OpenWrt Feed 方式
|
||||
|
||||
```bash
|
||||
# 添加 feed
|
||||
echo 'src-git gost https://github.com/kenzok78/luci-app-gost' >> feeds.conf.default
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a -p gost
|
||||
|
||||
# 编译
|
||||
make package/luci-app-gost/compile V=s
|
||||
```
|
||||
|
||||
### 直接编译
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kenzok78/luci-app-gost.git package/luci-app-gost
|
||||
make menuconfig # 选择 LuCI -> Applications -> luci-app-gost
|
||||
make -j$(nproc) V=s
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 基本配置
|
||||
|
||||
1. 访问 OpenWrt Web 管理界面 → 服务 → GOST
|
||||
2. 启用插件
|
||||
3. 选择配置方式:
|
||||
- **配置文件**: 指定 `gost.json` 配置文件路径
|
||||
- **命令行参数**: 添加启动参数
|
||||
4. 保存并应用配置
|
||||
|
||||
### 配置说明
|
||||
|
||||
| 选项 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| 启用 | 开启/关闭 GOST 服务 | 关闭 |
|
||||
| 配置文件 | GOST 配置文件路径 | /etc/gost/gost.json |
|
||||
| 参数 | 启动命令行参数 | 无 |
|
||||
|
||||
### 配置文件示例
|
||||
|
||||
`/etc/gost/gost.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
" Serve": [
|
||||
{
|
||||
"chain": "/tcp:0.0.0.0:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
luci-app-gost/
|
||||
├── htdocs/ # Web 静态资源
|
||||
│ └── luci-static/
|
||||
│ └── resources/
|
||||
│ └── view/ # 视图模板
|
||||
│ └── gost.js
|
||||
├── root/ # 文件系统文件
|
||||
│ ├── etc/
|
||||
│ │ ├── config/ # UCI 配置
|
||||
│ │ └── init.d/ # 启动脚本
|
||||
│ └── usr/
|
||||
│ └── share/
|
||||
│ ├── luci/
|
||||
│ │ └── menu.d/ # 菜单定义
|
||||
│ └── rpcd/
|
||||
│ └── acl.d/ # ACL 权限
|
||||
├── po/ # 翻译文件
|
||||
│ ├── zh-cn/
|
||||
│ └── zh_Hans/
|
||||
└── Makefile
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 启动失败怎么办?
|
||||
|
||||
检查 gost 二进制文件是否已安装,以及配置文件路径是否正确。
|
||||
|
||||
### Q: 如何查看日志?
|
||||
|
||||
```bash
|
||||
logread -e gost
|
||||
```
|
||||
|
||||
### Q: 如何手动启动?
|
||||
|
||||
```bash
|
||||
/etc/init.d/gost start
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
Apache-2.0
|
||||
|
||||
## 来源
|
||||
|
||||
- [GOST](https://gost.run/)
|
||||
- [go-gost/gost](https://github.com/go-gost/gost)
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
/*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require view';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('gost'), {}).then(function(res) {
|
||||
let isRunning = false;
|
||||
try {
|
||||
isRunning = res['gost']['instances']['instance1']['running'];
|
||||
} catch (e) {}
|
||||
return isRunning;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning) {
|
||||
let spanTmpl = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
return spanTmpl.format(
|
||||
isRunning ? 'green' : 'red',
|
||||
_('GOST'),
|
||||
isRunning ? _('RUNNING') : _('NOT RUNNING')
|
||||
);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('gost', _('GOST'),
|
||||
_('A simple security tunnel written in Golang.'));
|
||||
|
||||
/* Bug fix: 为状态栏段指定一个不存在的 UCI 类型,避免匹配实际配置节 */
|
||||
s = m.section(form.TypedSection, '_status');
|
||||
s.anonymous = true;
|
||||
s.render = function() {
|
||||
poll.add(function() {
|
||||
return L.resolveDefault(getServiceStatus()).then(function(isRunning) {
|
||||
/* Bug fix: 重命名变量以避免遮蔽外层 'require view' 中的 view 模块 */
|
||||
let statusEl = document.getElementById('service_status');
|
||||
if (statusEl)
|
||||
statusEl.innerHTML = renderStatus(isRunning);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data…'))
|
||||
]);
|
||||
};
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'gost');
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'config_file', _('Configuration file'));
|
||||
o.value('/etc/gost/gost.json');
|
||||
o.datatype = 'path';
|
||||
|
||||
o = s.option(form.DynamicList, 'arguments', _('Arguments'));
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
let config_file = this.section.formvalue(section_id, 'config_file');
|
||||
/* Bug fix: 避免使用 ES2020 可选链 (?.) 以兼容旧版 JS 运行时 */
|
||||
let args = this.section.formvalue(section_id, 'arguments');
|
||||
|
||||
if (!config_file && (!args || !args.length))
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user