Update On Sun Aug 25 20:30:21 CEST 2024
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
|
|
||||||
type MixedOption struct {
|
type MixedOption struct {
|
||||||
BaseOption
|
BaseOption
|
||||||
UDP bool `inbound:"udp,omitempty"`
|
Users AuthUsers `inbound:"users,omitempty"`
|
||||||
|
UDP bool `inbound:"udp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o MixedOption) Equal(config C.InboundConfig) bool {
|
func (o MixedOption) Equal(config C.InboundConfig) bool {
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
|
|
||||||
type SocksOption struct {
|
type SocksOption struct {
|
||||||
BaseOption
|
BaseOption
|
||||||
UDP bool `inbound:"udp,omitempty"`
|
Users AuthUsers `inbound:"users,omitempty"`
|
||||||
|
UDP bool `inbound:"udp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o SocksOption) Equal(config C.InboundConfig) bool {
|
func (o SocksOption) Equal(config C.InboundConfig) bool {
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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,50 +145,58 @@ 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 {
|
||||||
// We are the secondary instance.
|
match LocalSocketStream::connect(name.clone()).await {
|
||||||
// Prep to activate primary instance by allowing another process to take focus.
|
Ok(conn) => {
|
||||||
|
// We are the secondary instance.
|
||||||
|
// Prep to activate primary instance by allowing another process to take focus.
|
||||||
|
|
||||||
// A workaround to allow AllowSetForegroundWindow to succeed - press a key.
|
// A workaround to allow AllowSetForegroundWindow to succeed - press a key.
|
||||||
// This was originally used by Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=837796
|
// This was originally used by Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=837796
|
||||||
// dummy_keypress();
|
// dummy_keypress();
|
||||||
|
|
||||||
// let primary_instance_pid = conn.peer_pid().unwrap_or(ASFW_ANY);
|
// let primary_instance_pid = conn.peer_pid().unwrap_or(ASFW_ANY);
|
||||||
// unsafe {
|
// unsafe {
|
||||||
// let success = AllowSetForegroundWindow(primary_instance_pid) != 0;
|
// let success = AllowSetForegroundWindow(primary_instance_pid) != 0;
|
||||||
// if !success {
|
// if !success {
|
||||||
// log::warn!("AllowSetForegroundWindow failed.");
|
// log::warn!("AllowSetForegroundWindow failed.");
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
let (socket_rx, mut socket_tx) = conn.split();
|
let (socket_rx, mut socket_tx) = conn.split();
|
||||||
let mut socket_rx = socket_rx.as_tokio_async_read();
|
let mut socket_rx = socket_rx.as_tokio_async_read();
|
||||||
let url = std::env::args().nth(1).expect("URL not provided");
|
let url = std::env::args().nth(1).expect("URL not provided");
|
||||||
socket_tx
|
socket_tx
|
||||||
.write_all(url.as_bytes())
|
.write_all(url.as_bytes())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to write to socket");
|
.expect("Failed to write to socket");
|
||||||
socket_tx
|
socket_tx
|
||||||
.write_all(b"\n")
|
.write_all(b"\n")
|
||||||
.await
|
.await
|
||||||
.expect("Failed to write to socket");
|
.expect("Failed to write to socket");
|
||||||
socket_tx.flush().await.expect("Failed to flush socket");
|
socket_tx.flush().await.expect("Failed to flush socket");
|
||||||
|
|
||||||
let mut reader = BufReader::new(&mut socket_rx);
|
let mut reader = BufReader::new(&mut socket_rx);
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
if let Err(e) = reader.read_line(&mut buf).await {
|
if let Err(e) = reader.read_line(&mut buf).await {
|
||||||
eprintln!("Error reading from connection: {}", e);
|
eprintln!("Error reading from connection: {}", e);
|
||||||
}
|
}
|
||||||
buf.pop();
|
buf.pop();
|
||||||
dummy_keypress();
|
dummy_keypress();
|
||||||
let pid = buf.parse::<u32>().unwrap_or(ASFW_ANY);
|
let pid = buf.parse::<u32>().unwrap_or(ASFW_ANY);
|
||||||
unsafe {
|
unsafe {
|
||||||
let success = AllowSetForegroundWindow(pid) != 0;
|
let success = AllowSetForegroundWindow(pid) != 0;
|
||||||
if !success {
|
if !success {
|
||||||
eprintln!("AllowSetForegroundWindow failed.");
|
eprintln!("AllowSetForegroundWindow failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
}
|
Err(e) => {
|
||||||
std::process::exit(0);
|
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())
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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.message | upper_first | trim_end }}\
|
- **{{ 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.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 %}
|
||||||
|
{% 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 %}
|
||||||
|
{% 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(
|
||||||
title: t("Error"),
|
`Update check failed. Please verify your network connection.\n\n${formatError(e)}`,
|
||||||
type: "error",
|
{
|
||||||
});
|
title: t("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);
|
||||||
@@ -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,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,9 @@ 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) {
|
||||||
keyPEMBlock, err := os.ReadFile(c.TLS.Key)
|
if pathErr.Path == c.TLS.Cert {
|
||||||
if err != nil {
|
return configError{Field: "tls.cert", Err: pathErr}
|
||||||
return configError{Field: "tls.key", Err: err}
|
}
|
||||||
}
|
if pathErr.Path == c.TLS.Key {
|
||||||
_, err = tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
return configError{Field: "tls.key", Err: pathErr}
|
||||||
if err != nil {
|
}
|
||||||
return configError{Field: "tls", Err: fmt.Errorf("invalid cert-key pair: %w", err)}
|
}
|
||||||
|
return configError{Field: "tls", Err: 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
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ 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{
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
|
|
||||||
type MixedOption struct {
|
type MixedOption struct {
|
||||||
BaseOption
|
BaseOption
|
||||||
UDP bool `inbound:"udp,omitempty"`
|
Users AuthUsers `inbound:"users,omitempty"`
|
||||||
|
UDP bool `inbound:"udp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o MixedOption) Equal(config C.InboundConfig) bool {
|
func (o MixedOption) Equal(config C.InboundConfig) bool {
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
|
|
||||||
type SocksOption struct {
|
type SocksOption struct {
|
||||||
BaseOption
|
BaseOption
|
||||||
UDP bool `inbound:"udp,omitempty"`
|
Users AuthUsers `inbound:"users,omitempty"`
|
||||||
|
UDP bool `inbound:"udp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o SocksOption) Equal(config C.InboundConfig) bool {
|
func (o SocksOption) Equal(config C.InboundConfig) bool {
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
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=" " horiz-adv-x="512" d="" />
|
||||||
|
<glyph unicode="" glyph-name="expand_less" d="M512 596.667l256-256-60-60-196 196-196-196-60 60z" />
|
||||||
|
<glyph unicode="" glyph-name="expand_more" d="M708 572.667l60-60-256-256-256 256 60 60 196-196z" />
|
||||||
|
<glyph unicode="" glyph-name="menu" d="M128 682.667h768v-86h-768v86zM128 384.667v84h768v-84h-768zM128 170.667v86h768v-86h-768z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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 |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 9.1 KiB |