Update On Sun Aug 25 20:30:21 CEST 2024

This commit is contained in:
github-action[bot]
2024-08-25 20:30:21 +02:00
parent 6a6046ae72
commit 2e77f8eb45
141 changed files with 8685 additions and 2414 deletions
+1
View File
@@ -744,3 +744,4 @@ Update On Wed Aug 21 20:32:11 CEST 2024
Update On Thu Aug 22 20:34:16 CEST 2024 Update On Thu Aug 22 20:34:16 CEST 2024
Update On Fri Aug 23 20:31:39 CEST 2024 Update On Fri Aug 23 20:31:39 CEST 2024
Update On Sat Aug 24 20:31:41 CEST 2024 Update On Sat Aug 24 20:31:41 CEST 2024
Update On Sun Aug 25 20:30:10 CEST 2024
+9
View File
@@ -1005,6 +1005,9 @@ listeners:
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理
# udp: false # 默认 true # udp: false # 默认 true
# users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []
# - username: aaa
# password: aaa
- name: http-in-1 - name: http-in-1
type: http type: http
@@ -1012,6 +1015,9 @@ listeners:
listen: 0.0.0.0 listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
# users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []
# - username: aaa
# password: aaa
- name: mixed-in-1 - name: mixed-in-1
type: mixed # HTTP(S) 和 SOCKS 代理混合 type: mixed # HTTP(S) 和 SOCKS 代理混合
@@ -1020,6 +1026,9 @@ listeners:
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
# udp: false # 默认 true # udp: false # 默认 true
# users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []
# - username: aaa
# password: aaa
- name: reidr-in-1 - name: reidr-in-1
type: redir type: redir
+2
View File
@@ -13,3 +13,5 @@ func Authenticator() auth.Authenticator {
func SetAuthenticator(au auth.Authenticator) { func SetAuthenticator(au auth.Authenticator) {
authenticator = au authenticator = au
} }
func Nil() auth.Authenticator { return nil }
+2 -4
View File
@@ -30,7 +30,7 @@ func (b *bodyWrapper) Read(p []byte) (n int, err error) {
return n, err return n, err
} }
func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) { func HandleConn(c net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser
inUserIdx := len(additions) - 1 inUserIdx := len(additions) - 1
client := newClient(c, tunnel, additions) client := newClient(c, tunnel, additions)
@@ -41,6 +41,7 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, a
conn := N.NewBufferedConn(c) conn := N.NewBufferedConn(c)
authenticator := getAuth()
keepAlive := true keepAlive := true
trusted := authenticator == nil // disable authenticate if lru is nil trusted := authenticator == nil // disable authenticate if lru is nil
lastUser := "" lastUser := ""
@@ -146,9 +147,6 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, a
} }
func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) { func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) {
if inbound.SkipAuthRemoteAddress(request.RemoteAddr) {
authenticator = nil
}
credential := parseBasicProxyAuthorization(request) credential := parseBasicProxyAuthorization(request)
if credential == "" && authenticator != nil { if credential == "" && authenticator != nil {
resp = responseWith(request, http.StatusProxyAuthRequired) resp = responseWith(request, http.StatusProxyAuthRequired)
+11 -6
View File
@@ -33,20 +33,20 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator(), additions...) return NewWithAuthenticator(addr, tunnel, authStore.Authenticator, additions...)
} }
// NewWithAuthenticate // NewWithAuthenticate
// never change type traits because it's used in CFMA // never change type traits because it's used in CFMA
func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) {
authenticator := authStore.Authenticator() getAuth := authStore.Authenticator
if !authenticate { if !authenticate {
authenticator = nil getAuth = authStore.Nil
} }
return NewWithAuthenticator(addr, tunnel, authenticator, additions...) return NewWithAuthenticator(addr, tunnel, getAuth, additions...)
} }
func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) (*Listener, error) { func NewWithAuthenticator(addr string, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
isDefault := false isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@@ -75,13 +75,18 @@ func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authe
continue continue
} }
N.TCPKeepAlive(conn) N.TCPKeepAlive(conn)
getAuth := getAuth
if isDefault { // only apply on default listener if isDefault { // only apply on default listener
if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) {
_ = conn.Close() _ = conn.Close()
continue continue
} }
if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) {
getAuth = authStore.Nil
} }
go HandleConn(conn, tunnel, authenticator, additions...) }
go HandleConn(conn, tunnel, getAuth, additions...)
} }
}() }()
+31
View File
@@ -0,0 +1,31 @@
package inbound
import (
"github.com/metacubex/mihomo/component/auth"
authStore "github.com/metacubex/mihomo/listener/auth"
)
type AuthUser struct {
Username string `inbound:"username"`
Password string `inbound:"password"`
}
type AuthUsers []AuthUser
func (a AuthUsers) GetAuth() func() auth.Authenticator {
if a != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
if len(a) == 0 {
return authStore.Nil
}
users := make([]auth.AuthUser, len(a))
for i, user := range a {
users[i] = auth.AuthUser{
User: user.Username,
Pass: user.Password,
}
}
authenticator := auth.NewAuthenticator(users)
return func() auth.Authenticator { return authenticator }
}
return authStore.Authenticator
}
+2 -1
View File
@@ -8,6 +8,7 @@ import (
type HTTPOption struct { type HTTPOption struct {
BaseOption BaseOption
Users AuthUsers `inbound:"users,omitempty"`
} }
func (o HTTPOption) Equal(config C.InboundConfig) bool { func (o HTTPOption) Equal(config C.InboundConfig) bool {
@@ -44,7 +45,7 @@ func (h *HTTP) Address() string {
// Listen implements constant.InboundListener // Listen implements constant.InboundListener
func (h *HTTP) Listen(tunnel C.Tunnel) error { func (h *HTTP) Listen(tunnel C.Tunnel) error {
var err error var err error
h.l, err = http.New(h.RawAddress(), tunnel, h.Additions()...) h.l, err = http.NewWithAuthenticator(h.RawAddress(), tunnel, h.config.Users.GetAuth(), h.Additions()...)
if err != nil { if err != nil {
return err return err
} }
+2 -1
View File
@@ -12,6 +12,7 @@ import (
type MixedOption struct { type MixedOption struct {
BaseOption BaseOption
Users AuthUsers `inbound:"users,omitempty"`
UDP bool `inbound:"udp,omitempty"` UDP bool `inbound:"udp,omitempty"`
} }
@@ -52,7 +53,7 @@ func (m *Mixed) Address() string {
// Listen implements constant.InboundListener // Listen implements constant.InboundListener
func (m *Mixed) Listen(tunnel C.Tunnel) error { func (m *Mixed) Listen(tunnel C.Tunnel) error {
var err error var err error
m.l, err = mixed.New(m.RawAddress(), tunnel, m.Additions()...) m.l, err = mixed.NewWithAuthenticator(m.RawAddress(), tunnel, m.config.Users.GetAuth(), m.Additions()...)
if err != nil { if err != nil {
return err return err
} }
+2 -1
View File
@@ -9,6 +9,7 @@ import (
type SocksOption struct { type SocksOption struct {
BaseOption BaseOption
Users AuthUsers `inbound:"users,omitempty"`
UDP bool `inbound:"udp,omitempty"` UDP bool `inbound:"udp,omitempty"`
} }
@@ -70,7 +71,7 @@ func (s *Socks) Address() string {
// Listen implements constant.InboundListener // Listen implements constant.InboundListener
func (s *Socks) Listen(tunnel C.Tunnel) error { func (s *Socks) Listen(tunnel C.Tunnel) error {
var err error var err error
if s.stl, err = socks.New(s.RawAddress(), tunnel, s.Additions()...); err != nil { if s.stl, err = socks.NewWithAuthenticator(s.RawAddress(), tunnel, s.config.Users.GetAuth(), s.Additions()...); err != nil {
return err return err
} }
if s.udp { if s.udp {
+14 -5
View File
@@ -5,6 +5,7 @@ import (
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth" authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/http"
@@ -36,6 +37,10 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator, additions...)
}
func NewWithAuthenticator(addr string, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
isDefault := false isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@@ -62,20 +67,24 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
} }
continue continue
} }
getAuth := getAuth
if isDefault { // only apply on default listener if isDefault { // only apply on default listener
if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) {
_ = c.Close() _ = c.Close()
continue continue
} }
if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) {
getAuth = authStore.Nil
} }
go handleConn(c, tunnel, additions...) }
go handleConn(c, tunnel, getAuth, additions...)
} }
}() }()
return ml, nil return ml, nil
} }
func handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func handleConn(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
N.TCPKeepAlive(conn) N.TCPKeepAlive(conn)
bufConn := N.NewBufferedConn(conn) bufConn := N.NewBufferedConn(conn)
@@ -86,10 +95,10 @@ func handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
switch head[0] { switch head[0] {
case socks4.Version: case socks4.Version:
socks.HandleSocks4(bufConn, tunnel, additions...) socks.HandleSocks4(bufConn, tunnel, getAuth, additions...)
case socks5.Version: case socks5.Version:
socks.HandleSocks5(bufConn, tunnel, additions...) socks.HandleSocks5(bufConn, tunnel, getAuth, additions...)
default: default:
http.HandleConn(bufConn, tunnel, authStore.Authenticator(), additions...) http.HandleConn(bufConn, tunnel, getAuth, additions...)
} }
} }
+17 -14
View File
@@ -6,6 +6,7 @@ import (
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth" authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks4"
@@ -35,6 +36,10 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator, additions...)
}
func NewWithAuthenticator(addr string, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
isDefault := false isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@@ -61,20 +66,24 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
} }
continue continue
} }
getAuth := getAuth
if isDefault { // only apply on default listener if isDefault { // only apply on default listener
if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) {
_ = c.Close() _ = c.Close()
continue continue
} }
if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) {
getAuth = authStore.Nil
} }
go handleSocks(c, tunnel, additions...) }
go handleSocks(c, tunnel, getAuth, additions...)
} }
}() }()
return sl, nil return sl, nil
} }
func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func handleSocks(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
N.TCPKeepAlive(conn) N.TCPKeepAlive(conn)
bufConn := N.NewBufferedConn(conn) bufConn := N.NewBufferedConn(conn)
head, err := bufConn.Peek(1) head, err := bufConn.Peek(1)
@@ -85,19 +94,16 @@ func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition)
switch head[0] { switch head[0] {
case socks4.Version: case socks4.Version:
HandleSocks4(bufConn, tunnel, additions...) HandleSocks4(bufConn, tunnel, getAuth, additions...)
case socks5.Version: case socks5.Version:
HandleSocks5(bufConn, tunnel, additions...) HandleSocks5(bufConn, tunnel, getAuth, additions...)
default: default:
conn.Close() conn.Close()
} }
} }
func HandleSocks4(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func HandleSocks4(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
authenticator := authStore.Authenticator() authenticator := getAuth()
if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) {
authenticator = nil
}
addr, _, user, err := socks4.ServerHandshake(conn, authenticator) addr, _, user, err := socks4.ServerHandshake(conn, authenticator)
if err != nil { if err != nil {
conn.Close() conn.Close()
@@ -107,11 +113,8 @@ func HandleSocks4(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition)
tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...)) tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...))
} }
func HandleSocks5(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func HandleSocks5(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
authenticator := authStore.Authenticator() authenticator := getAuth()
if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) {
authenticator = nil
}
target, command, user, err := socks5.ServerHandshake(conn, authenticator) target, command, user, err := socks5.ServerHandshake(conn, authenticator)
if err != nil { if err != nil {
conn.Close() conn.Close()
+32 -20
View File
@@ -20,6 +20,7 @@ jobs:
# Give the default GITHUB_TOKEN write permission to commit and push the # Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository. # added or changed files to the repository.
contents: write contents: write
discussions: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -48,7 +49,8 @@ jobs:
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- name: Install - name: Install
run: pnpm i run: pnpm i
- name: Install git-cliff
uses: taiki-e/install-action@git-cliff
- id: update-version - id: update-version
shell: bash shell: bash
name: Bump version name: Bump version
@@ -56,30 +58,40 @@ jobs:
run: | run: |
echo "version=$(pnpm run publish ${{ inputs.versionType }} | tail -n1)" >> $GITHUB_OUTPUT echo "version=$(pnpm run publish ${{ inputs.versionType }} | tail -n1)" >> $GITHUB_OUTPUT
git add . git add .
- name: Conventional Changelog Action - name: Generate a changelog for the new version
shell: bash
id: build-changelog id: build-changelog
uses: TriPSs/conventional-changelog-action@v5 run: |
with: touch /tmp/changelog.md
github-token: ${{ secrets.github_token }} git-cliff --config cliff.toml --verbose --strip header --unreleased --tag v${{ steps.update-version.outputs.version }} > /tmp/changelog.md
git-message: "chore(release): {version} 🤖" if [ $? -eq 0 ]; then
git-user-name: "github-actions[bot]" CONTENT=$(cat /tmp/changelog.md)
git-user-email: "41898282+github-actions[bot]@users.noreply.github.com" cat /tmp/changelog.md >> ./CHANGELOG.md
preset: "conventionalcommits" {
tag-prefix: "v" echo 'content<<EOF'
# output-file: "UPDATELOG.md" # Since v1.4.3, using conventional-changelog-cli@v2.1.0 echo "$CONTENT"
release-count: "10" echo EOF
pre-changelog-generation: ".github/conventional-changelog/pre-changelog-generation.cjs" } >> $GITHUB_OUTPUT
config-file-path: ".github/conventional-changelog/config.cjs" echo "version=${{ steps.update-version.outputs.version }}" >> $GITHUB_OUTPUT
git-branch: "main" else
echo "Failed to generate changelog"
exit 1
fi
env: env:
NYANPASU_VERSION: ${{ steps.update-version.outputs.version }} GITHUB_REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.github_token }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: bump version to v${{ steps.update-version.outputs.version }}"
commit_user_name: "github-actions[bot]"
commit_user_email: "41898282+github-actions[bot]@users.noreply.github.com"
tagging_message: "v${{ steps.update-version.outputs.version }}"
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
draft: true draft: true
body: ${{steps.build-changelog.outputs.clean_changelog}} body: ${{steps.build-changelog.outputs.content}}
name: Clash Nyanpasu v${{steps.update-version.outputs.version}} name: Clash Nyanpasu v${{steps.update-version.outputs.version}}
tag_name: ${{steps.build-changelog.outputs.tag}} tag_name: "v${{ steps.update-version.outputs.version }}"
# target_commitish: ${{ steps.tag.outputs.sha }} # target_commitish: ${{ steps.tag.outputs.sha }}
+1828 -1784
View File
File diff suppressed because it is too large Load Diff
+129 -176
View File
@@ -17,6 +17,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "aes" name = "aes"
version = "0.8.4" version = "0.8.4"
@@ -614,13 +620,11 @@ dependencies = [
[[package]] [[package]]
name = "backon" name = "backon"
version = "0.4.4" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d67782c3f868daa71d3533538e98a8e13713231969def7536e8039606fc46bf0" checksum = "33e5b65cc81d81fbb8488f36458ab4771be35a722967bbc959df28b47397e3ff"
dependencies = [ dependencies = [
"fastrand 2.1.0", "fastrand 2.1.0",
"futures-core",
"pin-project",
"tokio", "tokio",
] ]
@@ -634,7 +638,7 @@ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.7.4",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@@ -681,7 +685,7 @@ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.12.1", "itertools 0.11.0",
"lazy_static 1.5.0", "lazy_static 1.5.0",
"lazycell", "lazycell",
"log 0.4.22", "log 0.4.22",
@@ -921,9 +925,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "3.5.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@@ -932,9 +936,9 @@ dependencies = [
[[package]] [[package]]
name = "brotli-decompressor" name = "brotli-decompressor"
version = "2.5.1" version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
"alloc-stdlib", "alloc-stdlib",
@@ -1304,7 +1308,7 @@ dependencies = [
"sha2 0.10.8", "sha2 0.10.8",
"simd-json", "simd-json",
"single-instance", "single-instance",
"sysinfo 0.30.13", "sysinfo",
"sysproxy", "sysproxy",
"tauri", "tauri",
"tauri-build", "tauri-build",
@@ -1331,7 +1335,7 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
"winreg 0.52.0", "winreg 0.52.0",
"wry", "wry",
"zip 2.1.6", "zip 2.2.0",
"zip-extensions", "zip-extensions",
] ]
@@ -2312,7 +2316,7 @@ dependencies = [
"flume", "flume",
"half", "half",
"lebe", "lebe",
"miniz_oxide", "miniz_oxide 0.7.4",
"rayon-core", "rayon-core",
"smallvec", "smallvec",
"zune-inflate", "zune-inflate",
@@ -2401,12 +2405,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.31" version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide 0.8.0",
] ]
[[package]] [[package]]
@@ -2418,6 +2422,15 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.0" version = "0.11.0"
@@ -3573,15 +3586,6 @@ version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "infer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a"
dependencies = [
"cfb",
]
[[package]] [[package]]
name = "infer" name = "infer"
version = "0.13.0" version = "0.13.0"
@@ -3857,15 +3861,27 @@ dependencies = [
[[package]] [[package]]
name = "json-patch" name = "json-patch"
version = "1.4.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b" checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc"
dependencies = [ dependencies = [
"jsonptr",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
] ]
[[package]]
name = "jsonptr"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627"
dependencies = [
"fluent-uri",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "kill_tree" name = "kill_tree"
version = "0.2.4" version = "0.2.4"
@@ -4386,6 +4402,15 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.2" version = "1.0.2"
@@ -4799,7 +4824,7 @@ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
"shared_child", "shared_child",
"sysinfo 0.31.2", "sysinfo",
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
@@ -5110,9 +5135,9 @@ checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
[[package]] [[package]]
name = "oxc_allocator" name = "oxc_allocator"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20afc1d4a7b0b878d28f7b7f9f1f7ab670a7c7e8feb29101fb49eac1faa483fa" checksum = "466379b9ab2e05996bfedfae9c96753a633bb5a53aaf0898eb0e0ab09e169514"
dependencies = [ dependencies = [
"allocator-api2", "allocator-api2",
"bumpalo", "bumpalo",
@@ -5120,9 +5145,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_ast" name = "oxc_ast"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1599f878d8ac6fcc229be06426f04134f7663dcd5c71046414d8ddd12a20ad3b" checksum = "34bd4f56fe32adea489153f6d681d9ee01f0336b9b6a89f062611488d8f80797"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"num-bigint", "num-bigint",
@@ -5134,9 +5159,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_ast_macros" name = "oxc_ast_macros"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a07c44bbe07756ba25605059fa4a94543f6a75730fd8bd1105795d0b3d668d" checksum = "197b36739db0e80919e19a90785233eea5664697d4cd829bd49af34838ec43d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5145,9 +5170,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_diagnostics" name = "oxc_diagnostics"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c53d201660e8accd6e53f1af7efda36967316aa4263d0a6847c631bdd8705e0f" checksum = "2cd4bb48b9527f5825c84acb688ec1485df4a5edadc17b3582626bb49736752b"
dependencies = [ dependencies = [
"miette", "miette",
"owo-colors", "owo-colors",
@@ -5157,15 +5182,15 @@ dependencies = [
[[package]] [[package]]
name = "oxc_index" name = "oxc_index"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa1d58c483b1ec74c7219b1525648b4b6beea7ff4685b02ad74693190df43308" checksum = "bc9aa9446f6d2a64d0baa02fe20dc3d64e3e112083854b84fdacb82261be2b84"
[[package]] [[package]]
name = "oxc_parser" name = "oxc_parser"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce38833b8b0d1121779b2ceaa9aa07d4142105ccc6941f73112a8d836b87cd" checksum = "8f3432e80a58cfb38f9a138203e64d0f9a621d4c4e9d18e3e3bd870b51ce1f0e"
dependencies = [ dependencies = [
"assert-unchecked", "assert-unchecked",
"bitflags 2.6.0", "bitflags 2.6.0",
@@ -5175,6 +5200,7 @@ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
"oxc_diagnostics", "oxc_diagnostics",
"oxc_regular_expression",
"oxc_span", "oxc_span",
"oxc_syntax", "oxc_syntax",
"rustc-hash 2.0.0", "rustc-hash 2.0.0",
@@ -5182,10 +5208,24 @@ dependencies = [
] ]
[[package]] [[package]]
name = "oxc_span" name = "oxc_regular_expression"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a4f6e525a64e61bcfa256e4707e46be54f3261e0b524670d0a1c6827c28931" checksum = "8fc6d05fec98ad6cc864ba8cfe7ece2e258106059a9a57e35b02450650b06979"
dependencies = [
"oxc_allocator",
"oxc_diagnostics",
"oxc_span",
"phf 0.11.2",
"rustc-hash 2.0.0",
"unicode-id-start",
]
[[package]]
name = "oxc_span"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a862a896ac3abd269863a19d4f77302b019458d90513705c7a017b138c8449b"
dependencies = [ dependencies = [
"compact_str", "compact_str",
"miette", "miette",
@@ -5195,9 +5235,9 @@ dependencies = [
[[package]] [[package]]
name = "oxc_syntax" name = "oxc_syntax"
version = "0.24.3" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcb27d1a7e00a63e5d490fa4ee9a008d45e45db3d505ca21f0e63de2097cf743" checksum = "d50c7ea034fb12f65376cfffc8ae4bfde3cda0a1e14407f82ffba1d26431703d"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"dashmap 6.0.1", "dashmap 6.0.1",
@@ -5549,7 +5589,7 @@ dependencies = [
"crc32fast", "crc32fast",
"fdeflate", "fdeflate",
"flate2", "flate2",
"miniz_oxide", "miniz_oxide 0.7.4",
] ]
[[package]] [[package]]
@@ -6291,9 +6331,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-i18n" name = "rust-i18n"
version = "3.1.1" version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c962de4e085155073c9ea6b00d8a8049f7dadfb63c62677615a248dd7b0443" checksum = "039f57d22229db401af3458ca939300178e99e88b938573cea12b7c2b0f09724"
dependencies = [ dependencies = [
"globwalk", "globwalk",
"once_cell", "once_cell",
@@ -6305,9 +6345,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-i18n-macro" name = "rust-i18n-macro"
version = "3.1.1" version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b727e4fde7339b901ed0a0d494c6ea576b5273c9692ff18db716b147b08dc68" checksum = "dde5c022360a2e54477882843d56b6f9bcb4bc62f504b651a2f497f0028d174f"
dependencies = [ dependencies = [
"glob", "glob",
"once_cell", "once_cell",
@@ -6322,9 +6362,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-i18n-support" name = "rust-i18n-support"
version = "3.1.1" version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33410d4f5d19e193c2f34c4210c6646dcbc68cd823f147a790556e53ad7d13b7" checksum = "75d2844d36f62b5d6b66f9cf8f8cbdbbbdcdb5fd37a473a9cc2fb45fdcf485d2"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"base62", "base62",
@@ -6597,9 +6637,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.208" version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@@ -6616,9 +6656,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.208" version = "1.0.209"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6627,9 +6667,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.125" version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
dependencies = [ dependencies = [
"indexmap 2.4.0", "indexmap 2.4.0",
"itoa 1.0.11", "itoa 1.0.11",
@@ -6742,9 +6782,9 @@ dependencies = [
[[package]] [[package]]
name = "serialize-to-javascript" name = "serialize-to-javascript"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@@ -6753,13 +6793,13 @@ dependencies = [
[[package]] [[package]]
name = "serialize-to-javascript-impl" name = "serialize-to-javascript-impl"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 2.0.75",
] ]
[[package]] [[package]]
@@ -7184,44 +7224,25 @@ dependencies = [
[[package]] [[package]]
name = "sys-locale" name = "sys-locale"
version = "0.2.4" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee" checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0"
dependencies = [ dependencies = [
"js-sys",
"libc", "libc",
"wasm-bindgen",
"web-sys",
"windows-sys 0.45.0",
] ]
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.30.13" version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" checksum = "2b92e0bdf838cbc1c4c9ba14f9c97a7ec6cdcd1ae66b10e1e42775a25553f45d"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"ntapi",
"once_cell",
"rayon",
"windows 0.52.0",
]
[[package]]
name = "sysinfo"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4115055da5f572fff541dd0c4e61b0262977f453cc9fe04be83aba25a89bdab"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"memchr", "memchr",
"ntapi", "ntapi",
"rayon", "rayon",
"windows 0.57.0", "windows 0.56.0",
] ]
[[package]] [[package]]
@@ -7391,12 +7412,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "1.7.1" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "336bc661a3f3250853fa83c6e5245449ed1c26dce5dcb28bdee7efedf6278806" checksum = "0e33e3ba00a3b05eb6c57ef135781717d33728b48acf914bb05629e74d897d29"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.7", "base64 0.22.1",
"bytes", "bytes",
"cocoa 0.24.1", "cocoa 0.24.1",
"dirs-next", "dirs-next",
@@ -7413,7 +7434,7 @@ dependencies = [
"http 0.2.12", "http 0.2.12",
"ignore", "ignore",
"indexmap 1.9.3", "indexmap 1.9.3",
"infer 0.9.0", "infer",
"minisign-verify", "minisign-verify",
"nix 0.26.4", "nix 0.26.4",
"notify-rust", "notify-rust",
@@ -7456,9 +7477,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "1.5.3" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c" checksum = "d5fb5a90a64241ddb7217d3210d844149070a911e87e8a107a707a1d4973f164"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -7475,9 +7496,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "1.4.4" version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1aed706708ff1200ec12de9cfbf2582b5d8ec05f6a7293911091effbd22036b" checksum = "93a9e3f5cebf779a63bf24903e714ec91196c307d8249a0008b882424328bcda"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.21.7",
"brotli", "brotli",
@@ -7501,9 +7522,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "1.4.5" version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88f831d2973ae4f81a706a0004e67dac87f2e4439973bbe98efbd73825d8ede" checksum = "d1d0e989f54fe06c5ef0875c5e19cf96453d099a0a774d5192ab47e80471cdab"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -7530,9 +7551,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "0.14.4" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3068ed62b63dedc705558f4248c7ecbd5561f0f8050949859ea0db2326f26012" checksum = "f33fda7d213e239077fad52e96c6b734cecedb30c2382118b64f94cb5103ff3a"
dependencies = [ dependencies = [
"gtk", "gtk",
"http 0.2.12", "http 0.2.12",
@@ -7551,9 +7572,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "0.14.9" version = "0.14.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c3db170233096aa30330feadcd895bf9317be97e624458560a20e814db7955" checksum = "18c447dcd9b0f09c7dc4b752cc33e72788805bfd761fbda5692d30c48289efec"
dependencies = [ dependencies = [
"arboard", "arboard",
"cocoa 0.24.1", "cocoa 0.24.1",
@@ -7572,9 +7593,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "1.6.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2826db448309d382dac14d520f0c0a40839b87b57b977e59cf5f296b3ace6a93" checksum = "83a0c939e88d82903a0a7dfb28388b12a3c03504d6bd6086550edaa3b6d8beaa"
dependencies = [ dependencies = [
"brotli", "brotli",
"ctor", "ctor",
@@ -7582,7 +7603,7 @@ dependencies = [
"glob", "glob",
"heck 0.5.0", "heck 0.5.0",
"html5ever", "html5ever",
"infer 0.13.0", "infer",
"json-patch", "json-patch",
"kuchikiki", "kuchikiki",
"log 0.4.22", "log 0.4.22",
@@ -8865,7 +8886,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -8955,16 +8976,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.58.0" version = "0.58.0"
@@ -9006,18 +9017,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.58.0" version = "0.58.0"
@@ -9052,17 +9051,6 @@ dependencies = [
"syn 2.0.75", "syn 2.0.75",
] ]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.58.0" version = "0.58.0"
@@ -9085,17 +9073,6 @@ dependencies = [
"syn 2.0.75", "syn 2.0.75",
] ]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.75",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.58.0" version = "0.58.0"
@@ -9167,15 +9144,6 @@ dependencies = [
"windows_x86_64_msvc 0.42.2", "windows_x86_64_msvc 0.42.2",
] ]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@@ -9203,21 +9171,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.48.5" version = "0.48.5"
@@ -9850,9 +9803,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "2.1.6" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494"
dependencies = [ dependencies = [
"aes", "aes",
"arbitrary", "arbitrary",
@@ -9883,7 +9836,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "386508a00aae1d8218b9252a41f59bba739ccee3f8e420bb90bcb1c30d960d4a" checksum = "386508a00aae1d8218b9252a41f59bba739ccee3f8e420bb90bcb1c30d960d4a"
dependencies = [ dependencies = [
"zip 2.1.6", "zip 2.2.0",
] ]
[[package]] [[package]]
@@ -1,4 +1,5 @@
use std::{ use std::{
os::windows::thread,
path::Path, path::Path,
sync::atomic::{AtomicU16, Ordering}, sync::atomic::{AtomicU16, Ordering},
}; };
@@ -10,6 +11,9 @@ use interprocess::{
traits::tokio::{Listener, Stream}, traits::tokio::{Listener, Stream},
GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, ToNsName, GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, ToNsName,
}, },
os::windows::{
local_socket::ListenerOptionsExt, security_descriptor::SecurityDescriptor, ToWtf16,
},
}; };
use std::io::Result; use std::io::Result;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
@@ -85,9 +89,12 @@ pub fn listen<F: FnMut(String) + Send + 'static>(mut handler: F) -> Result<()> {
.build() .build()
.expect("failed to create tokio runtime") .expect("failed to create tokio runtime")
.block_on(async move { .block_on(async move {
let sdsf = "D:(A;;GA;;;WD)".to_wtf_16().unwrap();
let sd = SecurityDescriptor::deserialize(&sdsf).expect("Failed to deserialize SD");
let listener = ListenerOptions::new() let listener = ListenerOptions::new()
.name(name) .name(name)
.nonblocking(ListenerNonblockingMode::Both) .nonblocking(ListenerNonblockingMode::Both)
.security_descriptor(sd)
.create_tokio() .create_tokio()
.expect("Can't create listener"); .expect("Can't create listener");
@@ -127,6 +134,7 @@ pub fn listen<F: FnMut(String) + Send + 'static>(mut handler: F) -> Result<()> {
Ok(()) Ok(())
} }
#[inline(never)]
pub fn prepare(identifier: &str) { pub fn prepare(identifier: &str) {
let name: Name = identifier let name: Name = identifier
.to_ns_name::<GenericNamespaced>() .to_ns_name::<GenericNamespaced>()
@@ -137,7 +145,9 @@ pub fn prepare(identifier: &str) {
.build() .build()
.expect("failed to create tokio runtime") .expect("failed to create tokio runtime")
.block_on(async move { .block_on(async move {
if let Ok(conn) = LocalSocketStream::connect(name).await { for _ in 0..3 {
match LocalSocketStream::connect(name.clone()).await {
Ok(conn) => {
// We are the secondary instance. // We are the secondary instance.
// Prep to activate primary instance by allowing another process to take focus. // Prep to activate primary instance by allowing another process to take focus.
@@ -180,7 +190,13 @@ pub fn prepare(identifier: &str) {
} }
} }
std::process::exit(0); std::process::exit(0);
}
Err(e) => {
eprintln!("Failed to connect to local socket: {}", e);
std::thread::sleep(std::time::Duration::from_millis(1));
}
}; };
}
}); });
ID.set(identifier.to_string()) ID.set(identifier.to_string())
+8 -8
View File
@@ -32,7 +32,7 @@ ctrlc = "3.4.2"
dunce = "1.0.4" dunce = "1.0.4"
nanoid = "0.4.0" nanoid = "0.4.0"
chrono = "0.4.31" chrono = "0.4.31"
sysinfo = "0.30" sysinfo = "0.31"
sysproxy = { git = "https://github.com/zzzgydi/sysproxy-rs.git", version = "0.3" } sysproxy = { git = "https://github.com/zzzgydi/sysproxy-rs.git", version = "0.3" }
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.9" serde_yaml = "0.9"
@@ -46,6 +46,7 @@ serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
relative-path = "1.9" relative-path = "1.9"
tauri = { version = "1.5.4", features = [ tauri = { version = "1.5.4", features = [
"updater",
"fs-all", "fs-all",
"clipboard-all", "clipboard-all",
"os-all", "os-all",
@@ -55,7 +56,6 @@ tauri = { version = "1.5.4", features = [
"shell-all", "shell-all",
"system-tray", "system-tray",
"window-all", "window-all",
"updater",
] } ] }
window-vibrancy = { version = "0.5.0" } window-vibrancy = { version = "0.5.0" }
window-shadows = { version = "0.2.2" } window-shadows = { version = "0.2.2" }
@@ -76,7 +76,7 @@ rs-snowflake = "0.6"
thiserror = { workspace = true } thiserror = { workspace = true }
simd-json = "0.13.8" simd-json = "0.13.8"
runas = "1.2.0" runas = "1.2.0"
backon = "0.4.1" backon = { version = "0.5", features = ["tokio-sleep"] }
rust-i18n = "3" rust-i18n = "3"
adler = "1.0.2" adler = "1.0.2"
rfd = "0.10" # should bump to v0.14 when clarify why the rfd v0.10 from tauri breaks build rfd = "0.10" # should bump to v0.14 when clarify why the rfd v0.10 from tauri breaks build
@@ -122,11 +122,11 @@ os_pipe = "1.2.0"
whoami = "1.5.1" whoami = "1.5.1"
atomic_enum = "0.3.0" atomic_enum = "0.3.0"
boa_engine.workspace = true boa_engine.workspace = true
oxc_parser = "0.24" oxc_parser = "0.25"
oxc_allocator = "0.24" oxc_allocator = "0.25"
oxc_span = "0.24" oxc_span = "0.25"
oxc_ast = "0.24" oxc_ast = "0.25"
oxc_syntax = "0.24" oxc_syntax = "0.25"
mlua = { version = "0.9", features = [ mlua = { version = "0.9", features = [
"lua54", "lua54",
"async", "async",
@@ -91,7 +91,7 @@ pub fn migrate_home_dir_handler(target_path: &str) -> anyhow::Result<()> {
use crate::utils::{self, dirs}; use crate::utils::{self, dirs};
use anyhow::Context; use anyhow::Context;
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use std::{path::PathBuf, process::Command, str::FromStr, thread, time::Duration}; use std::{borrow::Cow, path::PathBuf, process::Command, str::FromStr, thread, time::Duration};
use sysinfo::System; use sysinfo::System;
use tauri::utils::platform::current_exe; use tauri::utils::platform::current_exe;
println!("target path {}", target_path); println!("target path {}", target_path);
@@ -129,10 +129,12 @@ pub fn migrate_home_dir_handler(target_path: &str) -> anyhow::Result<()> {
]; ];
let sys = System::new_all(); let sys = System::new_all();
'outer: for process in sys.processes().values() { 'outer: for process in sys.processes().values() {
let mut process_name = process.name(); let process_name = process.name().to_string_lossy(); // TODO: check if it's utf-8
if process_name.ends_with(".exe") { let process_name = if let Some(name) = process_name.strip_suffix(".exe") {
process_name = &process_name[..process_name.len() - 4]; // remove .exe Cow::Borrowed(name)
} } else {
process_name
};
for name in related_names.iter() { for name in related_names.iter() {
if process_name.ends_with(name) { if process_name.ends_with(name) {
println!( println!(
+32 -2
View File
@@ -10,19 +10,46 @@ All notable changes to this project will be documented in this file.\n
# template for the changelog body # template for the changelog body
# https://keats.github.io/tera/docs/#introduction # https://keats.github.io/tera/docs/#introduction
body = """ body = """
{% set whitespace = " " %}
{% if version %}\ {% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\ {% else %}\
## [unreleased] ## [unreleased]
{% endif %}\ {% endif %}\
{% for group, commits in commits | group_by(attribute="group") %} {% for group, commits in commits | filter(attribute="breaking", value=true) | group_by(attribute="group") %}
### {{ group | upper_first }} ### {{ group | upper_first }}
{% for commit in commits %} {% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
- **{{ commit.scope | trim_end}}:**{{ whitespace }}{{ commit.message | upper_first | trim_end }}\
{% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}
{% if commit.github.pr_number %} in \
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
{%- endif %}
{% endfor %}\
{% for commit in commits %}{% if not commit.scope %}
- {{ commit.message | upper_first | trim_end }}\ - {{ commit.message | upper_first | trim_end }}\
{% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%} {% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}
{% if commit.github.pr_number %} in \ {% if commit.github.pr_number %} in \
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \ [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
{%- endif %} {%- endif %}
{% else %}{%- endif -%}
{% endfor %}
{% endfor %}
{% for group, commits in commits | filter(attribute="breaking", value=false) | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
- **{{ commit.scope | trim_end}}:**{{ whitespace }}{{ commit.message | upper_first | trim_end }}\
{% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}
{% if commit.github.pr_number %} in \
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
{%- endif %}
{% endfor %}\
{% for commit in commits %}{% if not commit.scope %}
- {{ commit.message | upper_first | trim_end }}\
{% if commit.github.username %} by @{{ commit.github.username }} {% else %} by {{ commit.author.name }} {%- endif -%}
{% if commit.github.pr_number %} in \
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
{%- endif %}
{% else %}{%- endif -%}
{% endfor %} {% endfor %}
{% endfor %}\n {% endfor %}\n
@@ -68,6 +95,9 @@ filter_unconventional = false
split_commits = false split_commits = false
# regex for parsing and grouping commits # regex for parsing and grouping commits
commit_parsers = [ commit_parsers = [
{ field = "author.name", pattern = "renovate\\[bot\\]", group = "Renovate", skip = true },
{ field = "scope", pattern = "manifest", message = "^chore", skip = true },
{ field = "breaking", pattern = "true", group = "💥 Breaking Changes" },
{ message = "^feat", group = "✨ Features" }, { message = "^feat", group = "✨ Features" },
{ message = "^fix", group = "🐛 Bug Fixes" }, { message = "^fix", group = "🐛 Bug Fixes" },
{ message = "^doc", group = "📚 Documentation" }, { message = "^doc", group = "📚 Documentation" },
@@ -18,6 +18,6 @@
"swr": "2.2.5" "swr": "2.2.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "18.3.3" "@types/react": "18.3.4"
} }
} }
@@ -25,12 +25,12 @@
"ahooks": "3.8.1", "ahooks": "3.8.1",
"allotment": "1.20.2", "allotment": "1.20.2",
"country-code-emoji": "2.3.0", "country-code-emoji": "2.3.0",
"dayjs": "1.11.12", "dayjs": "1.11.13",
"framer-motion": "12.0.0-alpha.0", "framer-motion": "12.0.0-alpha.0",
"i18next": "23.14.0", "i18next": "23.14.0",
"jotai": "2.9.3", "jotai": "2.9.3",
"material-react-table": "2.13.1", "material-react-table": "2.13.1",
"monaco-editor": "0.50.0", "monaco-editor": "0.51.0",
"mui-color-input": "3.0.0", "mui-color-input": "3.0.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@@ -47,9 +47,9 @@
}, },
"devDependencies": { "devDependencies": {
"@emotion/babel-plugin": "11.12.0", "@emotion/babel-plugin": "11.12.0",
"@emotion/react": "11.13.0", "@emotion/react": "11.13.3",
"@iconify/json": "2.2.238", "@iconify/json": "2.2.241",
"@types/react": "18.3.3", "@types/react": "18.3.4",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "4.3.1", "@vitejs/plugin-react": "4.3.1",
"@vitejs/plugin-react-swc": "3.7.0", "@vitejs/plugin-react-swc": "3.7.0",
@@ -59,7 +59,7 @@
"tailwindcss-textshadow": "2.1.3", "tailwindcss-textshadow": "2.1.3",
"unplugin-auto-import": "0.18.2", "unplugin-auto-import": "0.18.2",
"unplugin-icons": "0.19.2", "unplugin-icons": "0.19.2",
"vite": "5.4.1", "vite": "5.4.2",
"vite-plugin-monaco-editor": "1.1.3", "vite-plugin-monaco-editor": "1.1.3",
"vite-plugin-sass-dts": "1.3.25", "vite-plugin-sass-dts": "1.3.25",
"vite-plugin-svgr": "4.2.0", "vite-plugin-svgr": "4.2.0",
@@ -1,8 +1,11 @@
import { version } from "~/package.json"; import { version } from "~/package.json";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useSetAtom } from "jotai";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LogoSvg from "@/assets/image/logo.svg?react"; import LogoSvg from "@/assets/image/logo.svg?react";
import { UpdaterManifestAtom } from "@/store/updater";
import { formatError } from "@/utils";
import { message } from "@/utils/notification"; import { message } from "@/utils/notification";
import LoadingButton from "@mui/lab/LoadingButton"; import LoadingButton from "@mui/lab/LoadingButton";
import { import {
@@ -45,7 +48,7 @@ export const SettingNyanpasuVersion = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu(); const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu();
const setUpdaterManifest = useSetAtom(UpdaterManifestAtom);
const onCheckUpdate = useLockFn(async () => { const onCheckUpdate = useLockFn(async () => {
try { try {
setLoading(true); setLoading(true);
@@ -58,16 +61,16 @@ export const SettingNyanpasuVersion = () => {
type: "info", type: "info",
}); });
} else { } else {
message(`New Version: ${info.manifest?.version}`, { setUpdaterManifest(info.manifest || null);
title: t("New Version"),
type: "info",
});
} }
} catch (e) { } catch (e) {
message("Update check failed. Please verify your network connection.", { message(
`Update check failed. Please verify your network connection.\n\n${formatError(e)}`,
{
title: t("Error"), title: t("Error"),
type: "error", type: "error",
}); },
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -0,0 +1,25 @@
import { useAtom } from "jotai";
import { lazy, Suspense, useState } from "react";
import { UpdaterManifestAtom } from "@/store/updater";
const UpdaterDialog = lazy(() => import("./updater-dialog"));
export const UpdaterDialogWrapper = () => {
const [open, setOpen] = useState(true);
const [manifest, setManifest] = useAtom(UpdaterManifestAtom);
if (!manifest) return null;
return (
<Suspense fallback={null}>
<UpdaterDialog
open={open}
onClose={() => {
setOpen(false);
setManifest(null);
}}
manifest={manifest}
/>
</Suspense>
);
};
export default UpdaterDialogWrapper;
@@ -0,0 +1,44 @@
.UpdaterDialog {
.MarkdownContent {
h1 {
@apply pb-2 text-3xl font-bold;
}
h2 {
@apply pb-2 text-2xl font-bold;
}
h3 {
@apply pb-2 text-xl font-bold;
}
h4 {
@apply text-lg font-bold;
}
h5 {
@apply text-base font-bold;
}
h6 {
@apply text-sm font-bold;
}
p,
li {
@apply text-base;
}
a {
@apply text-blue-500 underline underline-offset-2;
}
ul {
@apply list-inside list-disc pb-4;
}
ol {
@apply list-inside list-decimal;
}
}
}
@@ -0,0 +1,5 @@
declare const classNames: {
readonly UpdaterDialog: "UpdaterDialog";
readonly MarkdownContent: "MarkdownContent";
};
export default classNames;
@@ -0,0 +1,100 @@
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useSetAtom } from "jotai";
import { lazy, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { UpdaterIgnoredAtom } from "@/store/updater";
import { formatError } from "@/utils";
import { message } from "@/utils/notification";
import { BaseDialog, BaseDialogProps, cn } from "@nyanpasu/ui";
import { relaunch } from "@tauri-apps/api/process";
import { open as openThat } from "@tauri-apps/api/shell";
import { installUpdate, type UpdateManifest } from "@tauri-apps/api/updater";
import styles from "./updater-dialog.module.scss";
const Markdown = lazy(() => import("react-markdown"));
export interface UpdaterDialogProps extends Omit<BaseDialogProps, "title"> {
manifest: UpdateManifest;
}
export default function UpdaterDialog({
open,
manifest,
onClose,
...others
}: UpdaterDialogProps) {
const { t } = useTranslation();
const setUpdaterIgnore = useSetAtom(UpdaterIgnoredAtom);
const handleUpdate = useLockFn(async () => {
try {
// Install the update. This will also restart the app on Windows!
await installUpdate();
// On macOS and Linux you will need to restart the app manually.
// You could use this step to display another confirmation dialog.
await relaunch();
} catch (e) {
console.error(e);
message(formatError(e), { type: "error", title: t("Error") });
}
});
return (
<BaseDialog
{...others}
title={t("updater.title")}
open={open}
onClose={() => {
setUpdaterIgnore(manifest.version); // TODO: control this behavior
onClose?.();
}}
onOk={handleUpdate}
close={t("updater.close")}
ok={t("updater.update")}
divider
>
<div
className={cn(
"xs:min-w-[90vw] sm:min-w-[50vw] md:min-w-[33.3vw]",
styles.UpdaterDialog,
)}
>
<div className="flex items-center gap-3 px-2 py-2">
<span className="text-xl font-bold">{manifest.version}</span>
<span className="text-xs text-slate-500">
{dayjs(manifest.date).format("YYYY-MM-DD HH:mm:ss")}
</span>
</div>
<div
className={cn("h-[50vh] overflow-y-auto p-4", styles.MarkdownContent)}
>
<Suspense fallback={<div>{t("loading")}</div>}>
<Markdown
components={{
a(props) {
const { children, node, ...rest } = props;
return (
<a
{...rest}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openThat(node.properties.href);
}}
>
{children}
</a>
);
},
}}
>
{manifest.body}
</Markdown>
</Suspense>
</div>
</div>
</BaseDialog>
);
}
@@ -0,0 +1,23 @@
import { useAtomValue, useSetAtom } from "jotai";
import { useMount } from "react-use";
import { UpdaterIgnoredAtom, UpdaterManifestAtom } from "@/store/updater";
import { useNyanpasu } from "@nyanpasu/interface";
import { checkUpdate } from "@tauri-apps/api/updater";
export default function useUpdater() {
const { nyanpasuConfig } = useNyanpasu();
const updaterIgnored = useAtomValue(UpdaterIgnoredAtom);
const setUpdaterManifest = useSetAtom(UpdaterManifestAtom);
useMount(() => {
const run = async () => {
if (nyanpasuConfig?.enable_auto_check_update) {
const info = await checkUpdate();
if (info?.shouldUpdate && updaterIgnored !== info.manifest?.version) {
setUpdaterManifest(info.manifest || null);
}
}
};
run().catch(console.error);
});
}
@@ -171,5 +171,10 @@
"Proxy takeover Status": "Proxy takeover Status", "Proxy takeover Status": "Proxy takeover Status",
"Subscription Expires In": "Expires {{time}}", "Subscription Expires In": "Expires {{time}}",
"Subscription Updated At": "Updated at {{time}}", "Subscription Updated At": "Updated at {{time}}",
"Select file to import or leave blank to touch new one.": "Select file to import or leave blank to touch new one." "Select file to import or leave blank to touch new one.": "Select file to import or leave blank to touch new one.",
"updater": {
"title": "New version available",
"close": "Ignore",
"update": "Update Now"
}
} }
@@ -154,5 +154,10 @@
"system_proxy": "Системный прокси", "system_proxy": "Системный прокси",
"Subscription Expires In": "Подписка истекает через {{time}}", "Subscription Expires In": "Подписка истекает через {{time}}",
"Subscription Updated At": "Подписка обновлена {{time}}", "Subscription Updated At": "Подписка обновлена {{time}}",
"Select file to import or leave blank to touch new one.": "Выберите файл для импорта или оставьте пустым, чтобы создать новый." "Select file to import or leave blank to touch new one.": "Выберите файл для импорта или оставьте пустым, чтобы создать новый.",
"updater": {
"title": "Доступно обновление",
"close": "Закрыть",
"update": "Обновить"
}
} }
@@ -173,5 +173,10 @@
"Proxy takeover Status": "代理接管状态", "Proxy takeover Status": "代理接管状态",
"Subscription Expires In": "{{time}}到期", "Subscription Expires In": "{{time}}到期",
"Subscription Updated At": "{{time}}更新", "Subscription Updated At": "{{time}}更新",
"Select file to import or leave blank to touch new one.": "选择文件导入或留空新建。" "Select file to import or leave blank to touch new one.": "选择文件导入或留空新建。",
"updater": {
"title": "发现新版本",
"close": "忽略",
"update": "立即更新"
}
} }
@@ -11,6 +11,8 @@ import {
useCustomTheme, useCustomTheme,
} from "@/components/layout/use-custom-theme"; } from "@/components/layout/use-custom-theme";
import LogProvider from "@/components/logs/log-provider"; import LogProvider from "@/components/logs/log-provider";
import UpdaterDialog from "@/components/updater/updater-dialog-wrapper";
import useUpdater from "@/hooks/use-updater";
import { atomIsDrawer } from "@/store"; import { atomIsDrawer } from "@/store";
import { classNames } from "@/utils"; import { classNames } from "@/utils";
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
@@ -35,6 +37,8 @@ export default function App() {
const [isDrawer, setIsDrawer] = useAtom(atomIsDrawer); const [isDrawer, setIsDrawer] = useAtom(atomIsDrawer);
useUpdater();
useEffect(() => { useEffect(() => {
setIsDrawer(breakpoint === "sm" || breakpoint === "xs"); setIsDrawer(breakpoint === "sm" || breakpoint === "xs");
}, [breakpoint, setIsDrawer]); }, [breakpoint, setIsDrawer]);
@@ -65,6 +69,7 @@ export default function App() {
<MutationProvider /> <MutationProvider />
<NoticeProvider /> <NoticeProvider />
<SchemeProvider /> <SchemeProvider />
<UpdaterDialog />
<AppContainer isDrawer={isDrawer}> <AppContainer isDrawer={isDrawer}>
<PageTransition <PageTransition
@@ -0,0 +1,10 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { UpdateManifest } from "@tauri-apps/api/updater";
export const UpdaterIgnoredAtom = atomWithStorage(
"updaterIgnored",
null as string | null,
);
export const UpdaterManifestAtom = atom<UpdateManifest | null>(null);
+3 -3
View File
@@ -22,7 +22,7 @@
"@radix-ui/react-scroll-area": "1.1.0", "@radix-ui/react-scroll-area": "1.1.0",
"@tauri-apps/api": "1.6.0", "@tauri-apps/api": "1.6.0",
"@types/d3": "7.4.3", "@types/d3": "7.4.3",
"@types/react": "18.3.3", "@types/react": "18.3.4",
"@vitejs/plugin-react": "4.3.1", "@vitejs/plugin-react": "4.3.1",
"ahooks": "3.8.1", "ahooks": "3.8.1",
"d3": "7.9.0", "d3": "7.9.0",
@@ -31,11 +31,11 @@
"react-error-boundary": "4.0.13", "react-error-boundary": "4.0.13",
"react-i18next": "15.0.1", "react-i18next": "15.0.1",
"react-use": "17.5.1", "react-use": "17.5.1",
"vite": "5.4.1", "vite": "5.4.2",
"vite-tsconfig-paths": "5.0.1" "vite-tsconfig-paths": "5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@emotion/react": "11.13.0", "@emotion/react": "11.13.3",
"@types/d3-interpolate-path": "2.0.3", "@types/d3-interpolate-path": "2.0.3",
"clsx": "2.1.1", "clsx": "2.1.1",
"d3-interpolate-path": "2.3.0", "d3-interpolate-path": "2.3.0",
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1, "manifest_version": 1,
"latest": { "latest": {
"mihomo": "v1.18.7", "mihomo": "v1.18.7",
"mihomo_alpha": "alpha-16c95fc", "mihomo_alpha": "alpha-27bcb26",
"clash_rs": "v0.2.0", "clash_rs": "v0.2.0",
"clash_premium": "2023-09-05-gdcc8d87" "clash_premium": "2023-09-05-gdcc8d87"
}, },
@@ -36,5 +36,5 @@
"darwin-x64": "clash-darwin-amd64-n{}.gz" "darwin-x64": "clash-darwin-amd64-n{}.gz"
} }
}, },
"updated_at": "2024-08-23T22:20:19.190Z" "updated_at": "2024-08-24T22:19:55.791Z"
} }
+3 -3
View File
@@ -97,14 +97,14 @@
"stylelint-config-standard": "36.0.1", "stylelint-config-standard": "36.0.1",
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-order": "6.0.4", "stylelint-order": "6.0.4",
"stylelint-scss": "6.5.0", "stylelint-scss": "6.5.1",
"tailwindcss": "3.4.10", "tailwindcss": "3.4.10",
"tsx": "4.17.1", "tsx": "4.18.0",
"typescript": "5.5.4" "typescript": "5.5.4"
}, },
"packageManager": "pnpm@9.8.0", "packageManager": "pnpm@9.8.0",
"engines": { "engines": {
"node": "22.6.0" "node": "22.7.0"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
+232 -233
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@
"octokit": "4.0.2", "octokit": "4.0.2",
"picocolors": "1.0.1", "picocolors": "1.0.1",
"tar": "7.4.3", "tar": "7.4.3",
"telegram": "2.23.10", "telegram": "2.24.11",
"undici": "6.19.8" "undici": "6.19.8"
} }
} }
+28 -12
View File
@@ -85,6 +85,7 @@ type serverConfigObfs struct {
type serverConfigTLS struct { type serverConfigTLS struct {
Cert string `mapstructure:"cert"` Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"` Key string `mapstructure:"key"`
SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict"
} }
type serverConfigACME struct { type serverConfigACME struct {
@@ -291,30 +292,45 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")} return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")}
} }
if c.TLS != nil { if c.TLS != nil {
// SNI guard
var sniGuard utils.SNIGuardFunc
switch strings.ToLower(c.TLS.SNIGuard) {
case "", "dns-san":
sniGuard = utils.SNIGuardDNSSAN
case "strict":
sniGuard = utils.SNIGuardStrict
case "disable":
sniGuard = nil
default:
return configError{Field: "tls.sniGuard", Err: errors.New("unsupported SNI guard")}
}
// Local TLS cert // Local TLS cert
if c.TLS.Cert == "" || c.TLS.Key == "" { if c.TLS.Cert == "" || c.TLS.Key == "" {
return configError{Field: "tls", Err: errors.New("empty cert or key path")} return configError{Field: "tls", Err: errors.New("empty cert or key path")}
} }
certLoader := &utils.LocalCertificateLoader{
CertFile: c.TLS.Cert,
KeyFile: c.TLS.Key,
SNIGuard: sniGuard,
}
// Try loading the cert-key pair here to catch errors early // Try loading the cert-key pair here to catch errors early
// (e.g. invalid files or insufficient permissions) // (e.g. invalid files or insufficient permissions)
certPEMBlock, err := os.ReadFile(c.TLS.Cert) err := certLoader.InitializeCache()
if err != nil { if err != nil {
return configError{Field: "tls.cert", Err: err} var pathErr *os.PathError
if errors.As(err, &pathErr) {
if pathErr.Path == c.TLS.Cert {
return configError{Field: "tls.cert", Err: pathErr}
} }
keyPEMBlock, err := os.ReadFile(c.TLS.Key) if pathErr.Path == c.TLS.Key {
if err != nil { return configError{Field: "tls.key", Err: pathErr}
return configError{Field: "tls.key", Err: err}
} }
_, err = tls.X509KeyPair(certPEMBlock, keyPEMBlock) }
if err != nil { return configError{Field: "tls", Err: err}
return configError{Field: "tls", Err: fmt.Errorf("invalid cert-key pair: %w", err)}
} }
// Use GetCertificate instead of Certificates so that // Use GetCertificate instead of Certificates so that
// users can update the cert without restarting the server. // users can update the cert without restarting the server.
hyConfig.TLSConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate
cert, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)
return &cert, err
}
} else { } else {
// ACME // ACME
dataDir := c.ACME.Dir dataDir := c.ACME.Dir
+1
View File
@@ -28,6 +28,7 @@ func TestServerConfig(t *testing.T) {
TLS: &serverConfigTLS{ TLS: &serverConfigTLS{
Cert: "some.crt", Cert: "some.crt",
Key: "some.key", Key: "some.key",
SNIGuard: "strict",
}, },
ACME: &serverConfigACME{ ACME: &serverConfigACME{
Domains: []string{ Domains: []string{
+1
View File
@@ -8,6 +8,7 @@ obfs:
tls: tls:
cert: some.crt cert: some.crt
key: some.key key: some.key
sniGuard: strict
acme: acme:
domains: domains:
+198
View File
@@ -0,0 +1,198 @@
package utils
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
"sync"
"sync/atomic"
"time"
)
type LocalCertificateLoader struct {
CertFile string
KeyFile string
SNIGuard SNIGuardFunc
lock sync.Mutex
cache atomic.Pointer[localCertificateCache]
}
type SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error
// localCertificateCache holds the certificate and its mod times.
// this struct is designed to be read-only.
//
// to update the cache, use LocalCertificateLoader.makeCache and
// update the LocalCertificateLoader.cache field.
type localCertificateCache struct {
certificate *tls.Certificate
certModTime time.Time
keyModTime time.Time
}
func (l *LocalCertificateLoader) InitializeCache() error {
l.lock.Lock()
defer l.lock.Unlock()
cache, err := l.makeCache()
if err != nil {
return err
}
l.cache.Store(cache)
return nil
}
func (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := l.getCertificateWithCache()
if err != nil {
return nil, err
}
if l.SNIGuard == nil {
return cert, nil
}
err = l.SNIGuard(info, cert)
if err != nil {
return nil, err
}
return cert, nil
}
func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) {
fi, err := os.Stat(l.CertFile)
if err != nil {
err = fmt.Errorf("failed to stat certificate file: %w", err)
return
}
certModTime = fi.ModTime()
fi, err = os.Stat(l.KeyFile)
if err != nil {
err = fmt.Errorf("failed to stat key file: %w", err)
return
}
keyModTime = fi.ModTime()
return
}
func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) {
c := &localCertificateCache{}
c.certModTime, c.keyModTime, err = l.checkModTime()
if err != nil {
return
}
cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)
if err != nil {
return
}
c.certificate = &cert
if c.certificate.Leaf == nil {
// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23
c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return
}
}
cache = c
return
}
func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {
cache := l.cache.Load()
certModTime, keyModTime, terr := l.checkModTime()
if terr != nil {
if cache != nil {
// use cache when file is temporarily unavailable
return cache.certificate, nil
}
return nil, terr
}
if cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) {
// cache is up-to-date
return cache.certificate, nil
}
if cache != nil {
if !l.lock.TryLock() {
// another goroutine is updating the cache
return cache.certificate, nil
}
} else {
l.lock.Lock()
}
defer l.lock.Unlock()
if l.cache.Load() != cache {
// another goroutine updated the cache
return l.cache.Load().certificate, nil
}
newCache, err := l.makeCache()
if err != nil {
if cache != nil {
// use cache when loading failed
return cache.certificate, nil
}
return nil, err
}
l.cache.Store(newCache)
return newCache.certificate, nil
}
// getNameFromClientHello returns a normalized form of hello.ServerName.
// If hello.ServerName is empty (i.e. client did not use SNI), then the
// associated connection's local address is used to extract an IP address.
//
// ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838
func getNameFromClientHello(hello *tls.ClientHelloInfo) string {
normalizedName := func(serverName string) string {
return strings.ToLower(strings.TrimSpace(serverName))
}
localIPFromConn := func(c net.Conn) string {
if c == nil {
return ""
}
localAddr := c.LocalAddr().String()
ip, _, err := net.SplitHostPort(localAddr)
if err != nil {
ip = localAddr
}
if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 {
ip = ip[:scopeIDStart]
}
return ip
}
if name := normalizedName(hello.ServerName); name != "" {
return name
}
return localIPFromConn(hello.Conn)
}
func SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
if len(cert.Leaf.DNSNames) == 0 {
return nil
}
return SNIGuardStrict(info, cert)
}
func SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
hostname := getNameFromClientHello(info)
err := cert.Leaf.VerifyHostname(hostname)
if err != nil {
return fmt.Errorf("sni guard: %w", err)
}
return nil
}
@@ -0,0 +1,139 @@
package utils
import (
"crypto/tls"
"log"
"net/http"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
testListen = "127.82.39.147:12947"
testCAFile = "./testcerts/ca"
testCertFile = "./testcerts/cert"
testKeyFile = "./testcerts/key"
)
func TestCertificateLoaderPathError(t *testing.T) {
assert.NoError(t, os.RemoveAll(testCertFile))
assert.NoError(t, os.RemoveAll(testKeyFile))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
err := loader.InitializeCache()
var pathErr *os.PathError
assert.ErrorAs(t, err, &pathErr)
}
func TestCertificateLoaderFullChain(t *testing.T) {
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.Error(t, runTestTLSClient("unmatched-sni.example.com"))
assert.Error(t, runTestTLSClient(""))
assert.NoError(t, runTestTLSClient("example.com"))
}
func TestCertificateLoaderNoSAN(t *testing.T) {
assert.NoError(t, generateTestCertificate(nil, "selfsign"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardDNSSAN,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.NoError(t, runTestTLSClient(""))
}
func TestCertificateLoaderReplaceCertificate(t *testing.T) {
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.NoError(t, runTestTLSClient("example.com"))
assert.Error(t, runTestTLSClient("2.example.com"))
assert.NoError(t, generateTestCertificate([]string{"2.example.com"}, "fullchain"))
assert.Error(t, runTestTLSClient("example.com"))
assert.NoError(t, runTestTLSClient("2.example.com"))
}
func generateTestCertificate(dnssan []string, certType string) error {
args := []string{
"certloader_test_gencert.py",
"--ca", testCAFile,
"--cert", testCertFile,
"--key", testKeyFile,
"--type", certType,
}
if len(dnssan) > 0 {
args = append(args, "--dnssan", strings.Join(dnssan, ","))
}
cmd := exec.Command("python", args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Failed to generate test certificate: %s", out)
return err
}
return nil
}
func runTestTLSClient(sni string) error {
args := []string{
"certloader_test_tlsclient.py",
"--server", testListen,
"--ca", testCAFile,
}
if sni != "" {
args = append(args, "--sni", sni)
}
cmd := exec.Command("python", args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Failed to run test TLS client: %s", out)
return err
}
return nil
}
@@ -0,0 +1,134 @@
import argparse
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
def create_key():
return ec.generate_private_key(ec.SECP256R1())
def create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None):
serial_number = x509.random_serial_number()
not_valid_before = datetime.datetime.now(datetime.UTC)
not_valid_after = not_valid_before + datetime.timedelta(days=365)
subject_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')),
x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')),
])
issuer_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')),
x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject_name)
builder = builder.issuer_name(issuer_name)
builder = builder.public_key(public_key)
builder = builder.serial_number(serial_number)
builder = builder.not_valid_before(not_valid_before)
builder = builder.not_valid_after(not_valid_after)
if cert_type == 'root':
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None), critical=True
)
elif cert_type == 'intermediate':
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0), critical=True
)
elif cert_type == 'leaf':
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None), critical=True
)
else:
raise ValueError(f'Invalid cert_type: {cert_type}')
if dns_san:
builder = builder.add_extension(
x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]),
critical=False
)
return builder.sign(private_key=private_key, algorithm=hashes.SHA256())
def main():
parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.')
parser.add_argument('--ca', required=True,
help='Path to write the X509 CA certificate in PEM format')
parser.add_argument('--cert', required=True,
help='Path to write the X509 certificate in PEM format')
parser.add_argument('--key', required=True,
help='Path to write the private key in PEM format')
parser.add_argument('--dnssan', required=False, default=None,
help='Comma-separated list of DNS SANs')
parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'],
help='Type of certificate to generate')
args = parser.parse_args()
key = create_key()
public_key = key.public_key()
if args.type == 'selfsign':
subject = {"C": "ZZ", "O": "Certificate", "CN": "Certificate"}
cert = create_certificate(
cert_type='root',
subject=subject,
issuer=subject,
private_key=key,
public_key=public_key,
dns_san=args.dnssan)
with open(args.ca, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
with open(args.cert, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
with open(args.key, 'wb') as f:
f.write(
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
elif args.type == 'fullchain':
ca_key = create_key()
ca_public_key = ca_key.public_key()
ca_subject = {"C": "ZZ", "O": "Root CA", "CN": "Root CA"}
ca_cert = create_certificate(
cert_type='root',
subject=ca_subject,
issuer=ca_subject,
private_key=ca_key,
public_key=ca_public_key)
intermediate_key = create_key()
intermediate_public_key = intermediate_key.public_key()
intermediate_subject = {"C": "ZZ", "O": "Intermediate CA", "CN": "Intermediate CA"}
intermediate_cert = create_certificate(
cert_type='intermediate',
subject=intermediate_subject,
issuer=ca_subject,
private_key=ca_key,
public_key=intermediate_public_key)
leaf_subject = {"C": "ZZ", "O": "Leaf Certificate", "CN": "Leaf Certificate"}
cert = create_certificate(
cert_type='leaf',
subject=leaf_subject,
issuer=intermediate_subject,
private_key=intermediate_key,
public_key=public_key,
dns_san=args.dnssan)
with open(args.ca, 'wb') as f:
f.write(ca_cert.public_bytes(Encoding.PEM))
with open(args.cert, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
f.write(intermediate_cert.public_bytes(Encoding.PEM))
with open(args.key, 'wb') as f:
f.write(
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
if __name__ == "__main__":
main()
@@ -0,0 +1,60 @@
import argparse
import ssl
import socket
import sys
def check_tls(server, ca_cert, sni, alpn):
try:
host, port = server.split(":")
port = int(port)
if ca_cert:
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert)
context.check_hostname = sni is not None
context.verify_mode = ssl.CERT_REQUIRED
else:
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if alpn:
context.set_alpn_protocols([p for p in alpn.split(",")])
with socket.create_connection((host, port)) as sock:
with context.wrap_socket(sock, server_hostname=sni) as ssock:
# Verify handshake and certificate
print(f'Connected to {ssock.version()} using {ssock.cipher()}')
print(f'Server certificate validated and details: {ssock.getpeercert()}')
print("OK")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
def main():
parser = argparse.ArgumentParser(description="Test TLS Server")
parser.add_argument("--server", required=True,
help="Server address to test (e.g., 127.1.2.3:8443)")
parser.add_argument("--ca", required=False, default=None,
help="CA certificate file used to validate the server certificate"
"Omit to use insecure connection")
parser.add_argument("--sni", required=False, default=None,
help="SNI to send in ClientHello")
parser.add_argument("--alpn", required=False, default='h2',
help="ALPN to send in ClientHello")
args = parser.parse_args()
exit_status = check_tls(
server=args.server,
ca_cert=args.ca,
sni=args.sni,
alpn=args.alpn)
sys.exit(exit_status)
if __name__ == "__main__":
main()
@@ -0,0 +1,3 @@
# This directory is used for certificate generation in certloader_test.go
/*
!/.gitignore
+11
View File
@@ -0,0 +1,11 @@
blinker==1.8.2
cffi==1.17.0
click==8.1.7
cryptography==43.0.0
Flask==3.0.3
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
pycparser==2.22
PySocks==1.7.1
Werkzeug==3.0.4
+9
View File
@@ -1005,6 +1005,9 @@ listeners:
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理
# udp: false # 默认 true # udp: false # 默认 true
# users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []
# - username: aaa
# password: aaa
- name: http-in-1 - name: http-in-1
type: http type: http
@@ -1012,6 +1015,9 @@ listeners:
listen: 0.0.0.0 listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
# users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []
# - username: aaa
# password: aaa
- name: mixed-in-1 - name: mixed-in-1
type: mixed # HTTP(S) 和 SOCKS 代理混合 type: mixed # HTTP(S) 和 SOCKS 代理混合
@@ -1020,6 +1026,9 @@ listeners:
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
# udp: false # 默认 true # udp: false # 默认 true
# users: # 如果不填写users项,则遵从全局authentication设置,如果填写会忽略全局设置, 如想跳过该入站的验证可填写 users: []
# - username: aaa
# password: aaa
- name: reidr-in-1 - name: reidr-in-1
type: redir type: redir
+2
View File
@@ -13,3 +13,5 @@ func Authenticator() auth.Authenticator {
func SetAuthenticator(au auth.Authenticator) { func SetAuthenticator(au auth.Authenticator) {
authenticator = au authenticator = au
} }
func Nil() auth.Authenticator { return nil }
+2 -4
View File
@@ -30,7 +30,7 @@ func (b *bodyWrapper) Read(p []byte) (n int, err error) {
return n, err return n, err
} }
func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) { func HandleConn(c net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser additions = append(additions, inbound.Placeholder) // Add a placeholder for InUser
inUserIdx := len(additions) - 1 inUserIdx := len(additions) - 1
client := newClient(c, tunnel, additions) client := newClient(c, tunnel, additions)
@@ -41,6 +41,7 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, a
conn := N.NewBufferedConn(c) conn := N.NewBufferedConn(c)
authenticator := getAuth()
keepAlive := true keepAlive := true
trusted := authenticator == nil // disable authenticate if lru is nil trusted := authenticator == nil // disable authenticate if lru is nil
lastUser := "" lastUser := ""
@@ -146,9 +147,6 @@ func HandleConn(c net.Conn, tunnel C.Tunnel, authenticator auth.Authenticator, a
} }
func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) { func authenticate(request *http.Request, authenticator auth.Authenticator) (resp *http.Response, user string) {
if inbound.SkipAuthRemoteAddress(request.RemoteAddr) {
authenticator = nil
}
credential := parseBasicProxyAuthorization(request) credential := parseBasicProxyAuthorization(request)
if credential == "" && authenticator != nil { if credential == "" && authenticator != nil {
resp = responseWith(request, http.StatusProxyAuthRequired) resp = responseWith(request, http.StatusProxyAuthRequired)
+11 -6
View File
@@ -33,20 +33,20 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator(), additions...) return NewWithAuthenticator(addr, tunnel, authStore.Authenticator, additions...)
} }
// NewWithAuthenticate // NewWithAuthenticate
// never change type traits because it's used in CFMA // never change type traits because it's used in CFMA
func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) { func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) {
authenticator := authStore.Authenticator() getAuth := authStore.Authenticator
if !authenticate { if !authenticate {
authenticator = nil getAuth = authStore.Nil
} }
return NewWithAuthenticator(addr, tunnel, authenticator, additions...) return NewWithAuthenticator(addr, tunnel, getAuth, additions...)
} }
func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authenticator, additions ...inbound.Addition) (*Listener, error) { func NewWithAuthenticator(addr string, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
isDefault := false isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@@ -75,13 +75,18 @@ func NewWithAuthenticator(addr string, tunnel C.Tunnel, authenticator auth.Authe
continue continue
} }
N.TCPKeepAlive(conn) N.TCPKeepAlive(conn)
getAuth := getAuth
if isDefault { // only apply on default listener if isDefault { // only apply on default listener
if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) { if !inbound.IsRemoteAddrDisAllowed(conn.RemoteAddr()) {
_ = conn.Close() _ = conn.Close()
continue continue
} }
if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) {
getAuth = authStore.Nil
} }
go HandleConn(conn, tunnel, authenticator, additions...) }
go HandleConn(conn, tunnel, getAuth, additions...)
} }
}() }()
+31
View File
@@ -0,0 +1,31 @@
package inbound
import (
"github.com/metacubex/mihomo/component/auth"
authStore "github.com/metacubex/mihomo/listener/auth"
)
type AuthUser struct {
Username string `inbound:"username"`
Password string `inbound:"password"`
}
type AuthUsers []AuthUser
func (a AuthUsers) GetAuth() func() auth.Authenticator {
if a != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
if len(a) == 0 {
return authStore.Nil
}
users := make([]auth.AuthUser, len(a))
for i, user := range a {
users[i] = auth.AuthUser{
User: user.Username,
Pass: user.Password,
}
}
authenticator := auth.NewAuthenticator(users)
return func() auth.Authenticator { return authenticator }
}
return authStore.Authenticator
}
+2 -1
View File
@@ -8,6 +8,7 @@ import (
type HTTPOption struct { type HTTPOption struct {
BaseOption BaseOption
Users AuthUsers `inbound:"users,omitempty"`
} }
func (o HTTPOption) Equal(config C.InboundConfig) bool { func (o HTTPOption) Equal(config C.InboundConfig) bool {
@@ -44,7 +45,7 @@ func (h *HTTP) Address() string {
// Listen implements constant.InboundListener // Listen implements constant.InboundListener
func (h *HTTP) Listen(tunnel C.Tunnel) error { func (h *HTTP) Listen(tunnel C.Tunnel) error {
var err error var err error
h.l, err = http.New(h.RawAddress(), tunnel, h.Additions()...) h.l, err = http.NewWithAuthenticator(h.RawAddress(), tunnel, h.config.Users.GetAuth(), h.Additions()...)
if err != nil { if err != nil {
return err return err
} }
+2 -1
View File
@@ -12,6 +12,7 @@ import (
type MixedOption struct { type MixedOption struct {
BaseOption BaseOption
Users AuthUsers `inbound:"users,omitempty"`
UDP bool `inbound:"udp,omitempty"` UDP bool `inbound:"udp,omitempty"`
} }
@@ -52,7 +53,7 @@ func (m *Mixed) Address() string {
// Listen implements constant.InboundListener // Listen implements constant.InboundListener
func (m *Mixed) Listen(tunnel C.Tunnel) error { func (m *Mixed) Listen(tunnel C.Tunnel) error {
var err error var err error
m.l, err = mixed.New(m.RawAddress(), tunnel, m.Additions()...) m.l, err = mixed.NewWithAuthenticator(m.RawAddress(), tunnel, m.config.Users.GetAuth(), m.Additions()...)
if err != nil { if err != nil {
return err return err
} }
+2 -1
View File
@@ -9,6 +9,7 @@ import (
type SocksOption struct { type SocksOption struct {
BaseOption BaseOption
Users AuthUsers `inbound:"users,omitempty"`
UDP bool `inbound:"udp,omitempty"` UDP bool `inbound:"udp,omitempty"`
} }
@@ -70,7 +71,7 @@ func (s *Socks) Address() string {
// Listen implements constant.InboundListener // Listen implements constant.InboundListener
func (s *Socks) Listen(tunnel C.Tunnel) error { func (s *Socks) Listen(tunnel C.Tunnel) error {
var err error var err error
if s.stl, err = socks.New(s.RawAddress(), tunnel, s.Additions()...); err != nil { if s.stl, err = socks.NewWithAuthenticator(s.RawAddress(), tunnel, s.config.Users.GetAuth(), s.Additions()...); err != nil {
return err return err
} }
if s.udp { if s.udp {
+14 -5
View File
@@ -5,6 +5,7 @@ import (
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth" authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/http"
@@ -36,6 +37,10 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator, additions...)
}
func NewWithAuthenticator(addr string, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
isDefault := false isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@@ -62,20 +67,24 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
} }
continue continue
} }
getAuth := getAuth
if isDefault { // only apply on default listener if isDefault { // only apply on default listener
if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) {
_ = c.Close() _ = c.Close()
continue continue
} }
if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) {
getAuth = authStore.Nil
} }
go handleConn(c, tunnel, additions...) }
go handleConn(c, tunnel, getAuth, additions...)
} }
}() }()
return ml, nil return ml, nil
} }
func handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func handleConn(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
N.TCPKeepAlive(conn) N.TCPKeepAlive(conn)
bufConn := N.NewBufferedConn(conn) bufConn := N.NewBufferedConn(conn)
@@ -86,10 +95,10 @@ func handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
switch head[0] { switch head[0] {
case socks4.Version: case socks4.Version:
socks.HandleSocks4(bufConn, tunnel, additions...) socks.HandleSocks4(bufConn, tunnel, getAuth, additions...)
case socks5.Version: case socks5.Version:
socks.HandleSocks5(bufConn, tunnel, additions...) socks.HandleSocks5(bufConn, tunnel, getAuth, additions...)
default: default:
http.HandleConn(bufConn, tunnel, authStore.Authenticator(), additions...) http.HandleConn(bufConn, tunnel, getAuth, additions...)
} }
} }
+17 -14
View File
@@ -6,6 +6,7 @@ import (
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/auth"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
authStore "github.com/metacubex/mihomo/listener/auth" authStore "github.com/metacubex/mihomo/listener/auth"
"github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks4"
@@ -35,6 +36,10 @@ func (l *Listener) Close() error {
} }
func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) { func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
return NewWithAuthenticator(addr, tunnel, authStore.Authenticator, additions...)
}
func NewWithAuthenticator(addr string, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) (*Listener, error) {
isDefault := false isDefault := false
if len(additions) == 0 { if len(additions) == 0 {
isDefault = true isDefault = true
@@ -61,20 +66,24 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
} }
continue continue
} }
getAuth := getAuth
if isDefault { // only apply on default listener if isDefault { // only apply on default listener
if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) { if !inbound.IsRemoteAddrDisAllowed(c.RemoteAddr()) {
_ = c.Close() _ = c.Close()
continue continue
} }
if inbound.SkipAuthRemoteAddr(c.RemoteAddr()) {
getAuth = authStore.Nil
} }
go handleSocks(c, tunnel, additions...) }
go handleSocks(c, tunnel, getAuth, additions...)
} }
}() }()
return sl, nil return sl, nil
} }
func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func handleSocks(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
N.TCPKeepAlive(conn) N.TCPKeepAlive(conn)
bufConn := N.NewBufferedConn(conn) bufConn := N.NewBufferedConn(conn)
head, err := bufConn.Peek(1) head, err := bufConn.Peek(1)
@@ -85,19 +94,16 @@ func handleSocks(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition)
switch head[0] { switch head[0] {
case socks4.Version: case socks4.Version:
HandleSocks4(bufConn, tunnel, additions...) HandleSocks4(bufConn, tunnel, getAuth, additions...)
case socks5.Version: case socks5.Version:
HandleSocks5(bufConn, tunnel, additions...) HandleSocks5(bufConn, tunnel, getAuth, additions...)
default: default:
conn.Close() conn.Close()
} }
} }
func HandleSocks4(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func HandleSocks4(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
authenticator := authStore.Authenticator() authenticator := getAuth()
if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) {
authenticator = nil
}
addr, _, user, err := socks4.ServerHandshake(conn, authenticator) addr, _, user, err := socks4.ServerHandshake(conn, authenticator)
if err != nil { if err != nil {
conn.Close() conn.Close()
@@ -107,11 +113,8 @@ func HandleSocks4(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition)
tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...)) tunnel.HandleTCPConn(inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4, additions...))
} }
func HandleSocks5(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { func HandleSocks5(conn net.Conn, tunnel C.Tunnel, getAuth func() auth.Authenticator, additions ...inbound.Addition) {
authenticator := authStore.Authenticator() authenticator := getAuth()
if inbound.SkipAuthRemoteAddr(conn.RemoteAddr()) {
authenticator = nil
}
target, command, user, err := socks5.ServerHandshake(conn, authenticator) target, command, user, err := socks5.ServerHandshake(conn, authenticator)
if err != nil { if err != nil {
conn.Close() conn.Close()
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,14 @@
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Design Theme
LUCI_DEPENDS:=
PKG_VERSION:=5.8.0-20240106
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,107 @@
<div align="center">
<h1 align="center">
LuCI design theme for OpenWrt
</h1>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/gngpp/luci-theme-design?style=flat&a=1" alt="">
</a>
<a href="https://github.com/gngpp/luci-theme-design/pulls">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="">
</a><a href="https://github.com/gngpp/luci-theme-design/issues/new">
<img src="https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat">
</a><a href="https://github.com/gngpp/luci-theme-design/releases">
<img src="https://img.shields.io/github/release/gngpp/luci-theme-design.svg?style=flat">
</a><a href="hhttps://github.com/gngpp/luci-theme-design/releases">
<img src="https://img.shields.io/github/downloads/gngpp/luci-theme-design/total?style=flat&?">
</a>
</div>
<br>
<br>简体中文 | [English](README_en.md)
# luci-theme-design
luci-theme-design 是一个针对移动端和PC端的沉浸式WebApp体验和优化的OpenWrt LuCI主题
- **luci-theme-design**基于luci-theme-neobird二次开发, 适用于[lede](https://github.com/coolsnowwolf/lede)
- main支持lede源码的lua版本
- js分支开始由[papagaye744](https://github.com/papagaye744)维护
- 你可以使用[插件](https://github.com/gngpp/luci-app-design-config)定义一些设置
- 支持更改主题深色/浅色模式
- 支持显示/隐藏导航栏
- 支持更换常用的代理图标
- 感谢 [JetBrains](https://www.jetbrains.com/) 提供的非商业开源软件开发授权!
<a href="https://www.jetbrains.com/?from=gnet" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
### 主要特点
- 适配移动端响应式优化,适合手机端做为WebApp使用
- 修改和优化了很多插件显示,完善的设备icon图标,视觉统一
- 简洁的登录界面,底部导航栏,类App的沉浸式体验
- 适配深色模式,适配系统自动切换,插件式自定义模式
- 支持插件式配置主题
- 流畅度比肩bootstrap
### 体验WebApp方法
- 在移动端(iOS/iPadOS、Android谷歌)浏览器打开设置管理,添加到主屏幕即可。
### 优化
- 修复安装package提示信息背景泛白
- 优化菜单折叠和缩放
- 优化显示网口down状态显示图标
- 优化logo显示
- 新增各设备状态图标显示
- 更换logo显示为字体"OpenWrt",支持以主机名显示logo
- 修复部分插件显示bug
- 修复vssr状态bar
- 修复诸多bug
- 修复兼容部分插件样式
- 修复aliyundrive-webdav样式
- 修复vssr在iOS/iPadOS WebApp模式下显示异常
- 修复openclash插件在iOS/iPadOS WebApp 模式下env(safe-area-inset-bottom) = 0
- 优化菜单hover action状态分辨
- 支持luci-app-wizard向导菜单
- Update header box-shadow style
- Update uci-change overflow
- Fix nlbw component
- 支持QWRT(QSDK)、iStore向导导航
- 适配OpenWrt 21/22
...
### 编译
```
git clone https://github.com/kenzok78/luci-theme-design.git package/luci-theme-design
make menuconfig # choose LUCI->Theme->Luci-theme-design
make V=s
```
### Q&A
- 有bug欢迎提issue
- 主题个人配色可能会不符合大众胃口,欢迎提配色建议
### 预览
<details> <summary>iOS</summary>
<img src="./preview/webapp_home.PNG"/>
<img src="./preview/webapp_vssr.PNG"/>
</details>
<details> <summary>iPadOS</summary>
<img src="./preview/IMG_0328.PNG"/>
<img src="./preview/IMG_0329.PNG"/>
</details>
<img src="./preview/login.png"/>
<img src="./preview/login1.png"/>
<img src="./preview/page.png"/>
<img src="./preview/home.png"/>
<img src="./preview/light.png"/>
<img src="./preview/home1.png"/>
<img src="./preview/wifi.png"/>
<img src="./preview/iface.png"/>
<img src="./preview/firewall.png"/>
@@ -0,0 +1,98 @@
<div align="center">
<h1 align="center">
LuCI design theme for OpenWrt
</h1>
<a href="/LICENSE">
<img src="https://img.shields.io/github/license/gngpp/luci-theme-design?style=flat&a=1" alt="">
</a>
<a href="https://github.com/gngpp/luci-theme-design/pulls">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="">
</a><a href="https://github.com/gngpp/luci-theme-design/issues/new">
<img src="https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat">
</a><a href="https://github.com/gngpp/luci-theme-design/releases">
<img src="https://img.shields.io/github/release/gngpp/luci-theme-design.svg?style=flat">
</a><a href="hhttps://github.com/gngpp/luci-theme-design/releases">
<img src="https://img.shields.io/github/downloads/gngpp/luci-theme-design/total?style=flat">
</a>
</div>
<br>
<br>English | [简体中文](README.md)
# luci-theme-design
### luci-theme-design is an OpenWrt LuCI theme for immersive WebApp experience and optimization on mobile and PC
- **luci-theme-design** based on luci-theme-neobird, for [lede](https://github.com/coolsnowwolf/lede) / [OpenWrt](https://github.com/openwrt/ openwrt)
- The default branch only supports the lua version of the lede source code. If you use openwrt 21/22, please pull the [js](https://github.com/gngpp/luci-theme-design/tree/js) version (development stage).
- You can define some settings using [plugin](https://github.com/gngpp/luci-app-design-config)
- Support changing theme dark/light mode
- Support show/hide navigation bar
- Support replacing commonly used proxy icons
### If you find it useful, please click a star, your support is the driving force for my long-term updates, thank you.
- Thanks for non-commercial open source development authorization by [JetBrains](https://www.jetbrains.com/)!
<a href="https://www.jetbrains.com/?from=gnet" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
### Release version
- Lua version select 5.x version
- JS version select 6.x version
### Features
- Adapt to the responsive optimization of the mobile terminal, suitable for use as a WebApp on the mobile terminal
- Modified and optimized the display of many plug-ins, improved icon icons, and unified visuals as much as possible
- Simple login interface, bottom navigation bar, immersive app-like experience
- Adapt to dark mode, adapt to system automatic switching, support custom mode
- Adapt to openwrt 21/22, lede
### Experience WebApp method
- Open the settings management in the mobile browser (iOS/iPadOS, Android Google) and add it to the home screen.
- If the SSL certificate is not used, iOS/iPadOS will display the menu bar at the top of the browser after opening a new page for security reasons.
### Optimization
- Optimize menu collapsing and zooming
- Optimized to display network port down state display icon
- Support QWRT (QSDK), iStore wizard navigation
- Adapt to OpenWrt 21/22
- Adapt to linkease series icons
### Compile
```
git clone https://github.com/gngpp/luci-theme-design.git package/luci-theme-design
make menuconfig # choose LUCI->Theme->Luci-theme-design
make V=s
```
### Q&A
- The resource interface icon is not perfect. If you have the ability to draw a picture, you are welcome to pr, but please make sure it is consistent with the existing icon color style
- If there is a bug, please raise an issue
- The theme's personal color matching may not meet the public's appetite, welcome to provide color matching suggestions
### Preview
<details> <summary>iOS</summary>
<img src="./preview/webapp_home.PNG"/>
<img src="./preview/webapp_vssr.PNG"/>
</details>
<details> <summary>iPadOS</summary>
<img src="./preview/IMG_0328.PNG"/>
<img src="./preview/IMG_0329.PNG"/>
</details>
<img src="./preview/login.png"/>
<img src="./preview/login1.png"/>
<img src="./preview/page.png"/>
<img src="./preview/home.png"/>
<img src="./preview/light.png"/>
<img src="./preview/home1.png"/>
<img src="./preview/wifi.png"/>
<img src="./preview/iface.png"/>
<img src="./preview/firewall.png"/>
@@ -0,0 +1,266 @@
/*
* xhr.js - XMLHttpRequest helper class
* (c) 2008-2010 Jo-Philipp Wich
*/
XHR = function()
{
this.reinit = function()
{
if (window.XMLHttpRequest) {
this._xmlHttp = new XMLHttpRequest();
}
else if (window.ActiveXObject) {
this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
}
else {
alert("xhr.js: XMLHttpRequest is not supported by this browser!");
}
}
this.busy = function() {
if (!this._xmlHttp)
return false;
switch (this._xmlHttp.readyState)
{
case 1:
case 2:
case 3:
return true;
default:
return false;
}
}
this.abort = function() {
if (this.busy())
this._xmlHttp.abort();
}
this.get = function(url,data,callback)
{
this.reinit();
var xhr = this._xmlHttp;
var code = this._encode(data);
url = location.protocol + '//' + location.host + url;
if (code)
if (url.substr(url.length-1,1) == '&')
url += code;
else
url += '?' + code;
xhr.open('GET', url, true);
xhr.onreadystatechange = function()
{
if (xhr.readyState == 4) {
var json = null;
if (xhr.getResponseHeader("Content-Type") == "application/json") {
try {
json = eval('(' + xhr.responseText + ')');
}
catch(e) {
json = null;
}
}
callback(xhr, json);
}
}
xhr.send(null);
}
this.post = function(url,data,callback)
{
this.reinit();
var xhr = this._xmlHttp;
var code = this._encode(data);
xhr.onreadystatechange = function()
{
if (xhr.readyState == 4)
callback(xhr);
}
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(code);
}
this.cancel = function()
{
this._xmlHttp.onreadystatechange = function(){};
this._xmlHttp.abort();
}
this.send_form = function(form,callback,extra_values)
{
var code = '';
for (var i = 0; i < form.elements.length; i++)
{
var e = form.elements[i];
if (e.options)
{
code += (code ? '&' : '') +
form.elements[i].name + '=' + encodeURIComponent(
e.options[e.selectedIndex].value
);
}
else if (e.length)
{
for (var j = 0; j < e.length; j++)
if (e[j].name) {
code += (code ? '&' : '') +
e[j].name + '=' + encodeURIComponent(e[j].value);
}
}
else
{
code += (code ? '&' : '') +
e.name + '=' + encodeURIComponent(e.value);
}
}
if (typeof extra_values == 'object')
for (var key in extra_values)
code += (code ? '&' : '') +
key + '=' + encodeURIComponent(extra_values[key]);
return(
(form.method == 'get')
? this.get(form.getAttribute('action'), code, callback)
: this.post(form.getAttribute('action'), code, callback)
);
}
this._encode = function(obj)
{
obj = obj ? obj : { };
obj['_'] = Math.random();
if (typeof obj == 'object')
{
var code = '';
var self = this;
for (var k in obj)
code += (code ? '&' : '') +
k + '=' + encodeURIComponent(obj[k]);
return code;
}
return obj;
}
}
XHR.get = function(url, data, callback)
{
(new XHR()).get(url, data, callback);
}
XHR.poll = function(interval, url, data, callback)
{
if (isNaN(interval) || interval < 1)
interval = 5;
if (!XHR._q)
{
XHR._t = 0;
XHR._q = [ ];
XHR._r = function() {
for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i])
{
if (!(XHR._t % e.interval) && !e.xhr.busy())
e.xhr.get(e.url, e.data, e.callback);
}
XHR._t++;
};
}
XHR._q.push({
interval: interval,
callback: callback,
url: url,
data: data,
xhr: new XHR()
});
XHR.run();
}
XHR.halt = function()
{
if (XHR._i)
{
/* show & set poll indicator */
try {
document.getElementById('xhr_poll_status').style.display = '';
document.getElementById('xhr_poll_status_on').style.display = 'none';
document.getElementById('xhr_poll_status_off').style.display = '';
document.getElementById('notice_status').style.marginRight = '30px'
} catch(e) { }
window.clearInterval(XHR._i);
XHR._i = null;
}
}
XHR.run = function()
{
if (XHR._r && !XHR._i)
{
/* show & set poll indicator */
try {
document.getElementById('xhr_poll_status').style.display = '';
document.getElementById('xhr_poll_status_on').style.display = '';
document.getElementById('xhr_poll_status_off').style.display = 'none';
document.getElementById('notice_status').style.marginRight = '30px'
} catch(e) { }
/* kick first round manually to prevent one second lag when setting up
* the poll interval */
XHR._r();
XHR._i = window.setInterval(XHR._r, 1000);
}
}
XHR.running = function()
{
return !!(XHR._r && XHR._i);
}
;(function ($) {
// Fixed status realtime table overflow style
function settingsStatusRealtimeOverflow() {
if (self.location.pathname.includes("status/realtime")) {
const nodeStatusRealtime = $('.node-status-realtime');
const selectorValues = ['bandwidth', 'wifirate', 'wireless'];
// .node-status-realtime embed[src="/luci-static/resources/bandwidth.svg"] + div + br + table
// .node-status-realtime embed[src="/luci-static/resources/wifirate.svg"] + div + br + table
// .node-status-realtime embed[src="/luci-static/resources/wireless.svg"] + div + br + table
for (let i = 0; i < selectorValues.length; i++) {
const value = selectorValues[i];
const target = nodeStatusRealtime.find(`embed[src="/luci-static/resources/${value}.svg"] + div + br + table`);
if (target.length) {
target.wrap('<div style="overflow-x: auto;"></div>');
}
}
}
}
$(document).ready(() => {
settingsStatusRealtimeOverflow();
});
})(jQuery);
@@ -0,0 +1,247 @@
/**
* Material is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI
*
* luci-theme-argon
* Copyright 2023 gngpp <gngppz@gmail.com>
*
* Have a bug? Please create an issue here on GitHub!
* https://github.com/LuttyYang/luci-theme-material/issues
*
* luci-theme-bootstrap:
* Copyright 2008 Steven Barth <steven@midlink.org>
* Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
* Copyright 2012 David Menting <david@nut-bolt.nl>
*
* MUI:
* https://github.com/muicss/mui
*
* luci-theme-material:
* https://github.com/LuttyYang/luci-theme-material/
*
*
* Licensed to the public under the Apache License 2.0
*/
(function ($) {
$(".main > .loading").fadeOut();
/**
* trim text, Remove spaces, wrap
* @param text
* @returns {string}
*/
function trimText(text) {
return text.replace(/[ \t\n\r]+/g, " ");
}
var lastNode = undefined;
var mainNodeName = undefined;
var nodeUrl = "";
(function (node) {
if (node[0] == "admin") {
luciLocation = [node[1], node[2]];
} else {
luciLocation = node;
}
for (var i in luciLocation) {
nodeUrl += luciLocation[i];
if (i != luciLocation.length - 1) {
nodeUrl += "/";
}
}
})(luciLocation);
/**
* get the current node by Burl (primary)
* @returns {boolean} success?
*/
function getCurrentNodeByUrl() {
if (!$('body').hasClass('logged-in')) {
luciLocation = ["Main", "Login"];
return true;
}
const urlReg = new RegExp(nodeUrl + "$")
var ret = false;
$(".main > .main-left > .nav > .slide > .active").next(".slide-menu").stop(true).slideUp("fast");
$(".main > .main-left > .nav > .slide > .menu").removeClass("active");
$(".main > .main-left > .nav > .slide > .menu").each(function () {
var ulNode = $(this);
ulNode.next().find("a").each(function () {
var that = $(this);
var href = that.attr("href");
if (urlReg.test(href)) {
ulNode.click();
ulNode.next(".slide-menu").stop(true, true);
lastNode = that.parent();
lastNode.addClass("active");
ret = true;
return true;
}
});
});
return ret;
}
/**
* menu click
*/
$(".main > .main-left > .nav > .slide > .menu").click(function () {
var ul = $(this).next(".slide-menu");
var menu = $(this);
if (!menu.hasClass("exit")) {
$(".main > .main-left > .nav > .slide > .active").next(".slide-menu").stop(true).slideUp("fast");
$(".main > .main-left > .nav > .slide > .menu").removeClass("active");
if (!ul.is(":visible")) {
menu.addClass("active");
ul.addClass("active");
ul.stop(true).slideDown("fast");
} else {
ul.stop(true).slideUp("fast", function () {
menu.removeClass("active");
ul.removeClass("active");
});
}
return false;
}
});
/**
* hook menu click and add the hash
*/
$(".main > .main-left > .nav > .slide > .slide-menu > li > a").click(function () {
if (lastNode != undefined)
lastNode.removeClass("active");
$(this).parent().addClass("active");
$(".main > .loading").fadeIn("fast");
return true;
});
/**
* fix menu click
*/
$(".main > .main-left > .nav > .slide > .slide-menu > li").click(function () {
if (lastNode != undefined)
lastNode.removeClass("active");
$(this).addClass("active");
$(".main > .loading").fadeIn("fast");
window.location = $($(this).find("a")[0]).attr("href");
return false;
});
/**
* get current node and open it
*/
if (getCurrentNodeByUrl()) {
mainNodeName = "node-" + luciLocation[0] + "-" + luciLocation[1];
mainNodeName = mainNodeName.replace(/[ \t\n\r\/]+/g, "_").toLowerCase();
$("body").addClass(mainNodeName);
}
/**
* hook other "A Label" and add hash to it.
*/
$("#maincontent > .container").find("a").each(function () {
var that = $(this);
var onclick = that.attr("onclick");
if (onclick == undefined || onclick == "") {
that.click(function () {
var href = that.attr("href");
if (href.indexOf("#") == -1) {
$(".main > .loading").fadeIn("fast");
return true;
}
});
}
});
/**
* Sidebar expand
*/
var showSide = false;
$(".showSide").click(function () {
if (showSide) {
$(".darkMask").stop(true).fadeOut("fast");
$(".main-left").stop(true).animate({
width: "0"
}, "fast");
$(".main-right").css("overflow-y", "auto");
$("header>.container>.brand").css("padding", "0 4.5rem")
showSide = false;
} else {
$(".darkMask").stop(true).fadeIn("fast");
$(".main-left").stop(true).animate({
width: "18rem"
}, "fast");
$(".main-right").css("overflow-y", "hidden");
$(".showSide").css("display", "none");
$("header").css("box-shadow", "18rem 2px 4px rgb(0 0 0 / 8%)")
$("header>.container>.brand").css("padding", '0rem')
showSide = true;
}
});
$(".darkMask").click(function () {
if (showSide) {
$(".darkMask").stop(true).fadeOut("fast");
$(".main-left").stop(true).animate({
width: "0"
}, "fast");
$(".main-right").css("overflow-y", "auto");
$(".showSide").css("display", "");
$("header").css("box-shadow", "0 2px 4px rgb(0 0 0 / 8%)")
$("header>.container>.brand").css("padding", "0 4.5rem")
showSide = false;
}
});
$(window).resize(function () {
if ($(window).width() > 992) {
showSide = false;
$(".showSide").css("display", "");
$(".main-left").css("width", "");
$(".darkMask").stop(true);
$(".darkMask").css("display", "none");
$("header").css("box-shadow", "18rem 2px 4px rgb(0 0 0 / 8%)")
$("header>.container>.brand").css("padding", '0rem')
} else {
$("header").css("box-shadow", "0 2px 4px rgb(0 0 0 / 8%)")
$("header>.container>.brand").css("padding", "0 4.5rem")
}
if (showSide) {
$("header").css("box-shadow", "18rem 2px 4px rgb(0 0 0 / 8%)")
$("header>.container>.brand").css("padding", '0rem')
}
});
$(".main-right").focus();
$(".main-right").blur();
$("input").attr("size", "0");
if (mainNodeName != undefined) {
switch (mainNodeName) {
case "node-status-system_log":
case "node-status-kernel_log":
$("#syslog").focus(function () {
$("#syslog").blur();
$(".main-right").focus();
$(".main-right").blur();
});
break;
case "node-status-firewall":
var button = $(".node-status-firewall > .main fieldset li > a");
button.addClass("cbi-button cbi-button-reset a-to-btn");
break;
case "node-system-reboot":
var button = $(".node-system-reboot > .main > .main-right p > a");
button.addClass("cbi-button cbi-input-reset a-to-btn");
break;
}
}
})(jQuery);
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe20a;" glyph-name="expand_less" d="M512 596.667l256-256-60-60-196 196-196-196-60 60z" />
<glyph unicode="&#xe20b;" glyph-name="expand_more" d="M708 572.667l60-60-256-256-256 256 60 60 196-196z" />
<glyph unicode="&#xe20e;" glyph-name="menu" d="M128 682.667h768v-86h-768v86zM128 384.667v84h768v-84h-768zM128 170.667v86h768v-86h-768z" />
<glyph unicode="&#xe291;" glyph-name="favorite" d="M512 28.667l-62 56q-106 96-154 142t-107 114-81 123-22 113q0 98 67 166t167 68q116 0 192-90 76 90 192 90 100 0 167-68t67-166q0-78-52-162t-113-146-199-186z" />
<glyph unicode="&#xe603;" glyph-name="spinner9" d="M512 960c-278.748 0-505.458-222.762-511.848-499.974 5.92 241.864 189.832 435.974 415.848 435.974 229.75 0 416-200.576 416-448 0-53.020 42.98-96 96-96s96 42.98 96 96c0 282.77-229.23 512-512 512zM512-64c278.748 0 505.458 222.762 511.848 499.974-5.92-241.864-189.832-435.974-415.848-435.974-229.75 0-416 200.576-416 448 0 53.020-42.98 96-96 96s-96-42.98-96-96c0-282.77 229.23-512 512-512z" />
<glyph unicode="&#xf059;" glyph-name="question-circle" horiz-adv-x="878" d="M512 164.571v109.714q0 8-5.143 13.143t-13.143 5.143h-109.714q-8 0-13.143-5.143t-5.143-13.143v-109.714q0-8 5.143-13.143t13.143-5.143h109.714q8 0 13.143 5.143t5.143 13.143zM658.286 548.571q0 50.286-31.714 93.143t-79.143 66.286-97.143 23.429q-138.857 0-212-121.714-8.571-13.714 4.571-24l75.429-57.143q4-3.429 10.857-3.429 9.143 0 14.286 6.857 30.286 38.857 49.143 52.571 19.429 13.714 49.143 13.714 27.429 0 48.857-14.857t21.429-33.714q0-21.714-11.429-34.857t-38.857-25.714q-36-16-66-49.429t-30-71.714v-20.571q0-8 5.143-13.143t13.143-5.143h109.714q8 0 13.143 5.143t5.143 13.143q0 10.857 12.286 28.286t31.143 28.286q18.286 10.286 28 16.286t26.286 20 25.429 27.429 16 34.571 7.143 46.286zM877.714 438.857q0-119.429-58.857-220.286t-159.714-159.714-220.286-58.857-220.286 58.857-159.714 159.714-58.857 220.286 58.857 220.286 159.714 159.714 220.286 58.857 220.286-58.857 159.714-159.714 58.857-220.286z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Some files were not shown because too many files have changed in this diff Show More