diff --git a/client/control.go b/client/control.go index 0f48c36d..4e9a49e9 100644 --- a/client/control.go +++ b/client/control.go @@ -16,7 +16,9 @@ package client import ( "context" + "fmt" "net" + "strings" "sync/atomic" "time" @@ -167,9 +169,44 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) { // Start a new proxy handler if no error got err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error) if err != nil { - xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err) + xl.Warnf("[%s] 启动失败: %v", inMsg.ProxyName, err) } else { - xl.Infof("[%s] start proxy success", inMsg.ProxyName) + xl.Infof("[%s] 成功启动隧道", inMsg.ProxyName) + if inMsg.RemoteAddr != "" { + // Get proxy type to format access message + if status, ok := ctl.pm.GetProxyStatus(inMsg.ProxyName); ok { + proxyType := status.Type + remoteAddr := inMsg.RemoteAddr + var accessMsg string + + switch proxyType { + case "tcp", "udp", "stcp", "xtcp", "sudp", "tcpmux": + // If remoteAddr only contains port (e.g., ":8080"), prepend server address + if strings.HasPrefix(remoteAddr, ":") { + serverAddr := ctl.sessionCtx.Common.ServerAddr + remoteAddr = serverAddr + remoteAddr + } + accessMsg = fmt.Sprintf("您可通过 %s 访问您的服务", remoteAddr) + case "http", "https": + // Format as URL with protocol + protocol := proxyType + addr := remoteAddr + // Remove standard ports for cleaner URL + if proxyType == "http" && strings.HasSuffix(addr, ":80") { + addr = strings.TrimSuffix(addr, ":80") + } else if proxyType == "https" && strings.HasSuffix(addr, ":443") { + addr = strings.TrimSuffix(addr, ":443") + } + accessMsg = fmt.Sprintf("您可通过 %s://%s 访问您的服务", protocol, addr) + default: + accessMsg = fmt.Sprintf("您可通过 %s 访问您的服务", remoteAddr) + } + + xl.Infof("[%s] %s", inMsg.ProxyName, accessMsg) + } else { + xl.Infof("[%s] 您可通过 %s 访问您的服务", inMsg.ProxyName, inMsg.RemoteAddr) + } + } } } diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 4615e9a2..dd25e71e 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -159,7 +159,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) { } } if len(delPxyNames) > 0 { - xl.Infof("proxy removed: %s", delPxyNames) + xl.Infof("隧道移除: %s", delPxyNames) } addPxyNames := make([]string, 0) @@ -177,6 +177,6 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) { } } if len(addPxyNames) > 0 { - xl.Infof("proxy added: %s", addPxyNames) + xl.Infof("添加隧道: %s", addPxyNames) } } diff --git a/client/service.go b/client/service.go index b282163e..2c6e7e77 100644 --- a/client/service.go +++ b/client/service.go @@ -219,7 +219,7 @@ func (svr *Service) Run(ctx context.Context) error { if svr.ctl == nil { cancelCause := cancelErr{} _ = errors.As(context.Cause(svr.ctx), &cancelCause) - return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err) + return fmt.Errorf("登录服务器失败: %v. 启用 loginFailExit 后,将不再尝试重试", cancelCause.Err) } go svr.keepControllerWorking() @@ -320,7 +320,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) { svr.runID = loginRespMsg.RunID xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID}) - xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID) + xl.Infof("登录服务器成功, 获取 run id [%s]", loginRespMsg.RunID) return } @@ -328,10 +328,10 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE xl := xlog.FromContextSafe(svr.ctx) loginFunc := func() (bool, error) { - xl.Infof("try to connect to server...") + xl.Infof("尝试连接到服务器...") conn, connector, err := svr.login() if err != nil { - xl.Warnf("connect to server error: %v", err) + xl.Warnf("连接服务器错误: %v", err) if firstLoginExit { svr.cancel(cancelErr{Err: err}) } diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go index ef7fe67f..e8b5c0f1 100644 --- a/cmd/frpc/sub/proxy.go +++ b/cmd/frpc/sub/proxy.go @@ -92,7 +92,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm fmt.Println(err) os.Exit(1) } - err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "") + err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "", "", "") if err != nil { fmt.Println(err) os.Exit(1) @@ -123,7 +123,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client fmt.Println(err) os.Exit(1) } - err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "") + err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "", "", "") if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 1c2d8d5e..b599d46c 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -16,8 +16,11 @@ package sub import ( "context" + "encoding/base64" + "encoding/json" "fmt" "io/fs" + "net/http" "os" "os/signal" "path/filepath" @@ -34,6 +37,7 @@ import ( "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/policy/featuregate" "github.com/fatedier/frp/pkg/policy/security" + "github.com/fatedier/frp/pkg/util/banner" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" ) @@ -44,6 +48,7 @@ var ( showVersion bool strictConfigMode bool allowUnsafe []string + authToken string ) func init() { @@ -51,7 +56,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors") - + rootCmd.PersistentFlags().StringVarP(&authToken, "token", "t", "", "authentication token of frpc (LoliaFRP only)") rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", "))) } @@ -67,6 +72,16 @@ var rootCmd = &cobra.Command{ unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + // If authToken is provided, fetch config from API + if authToken != "" { + err := runClientWithToken(authToken, unsafeFeatures) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return nil + } + // If cfgDir is not empty, run multiple frpc service for each config file in cfgDir. // Note that it's only designed for testing. It's not guaranteed to be stable. if cfgDir != "" { @@ -143,7 +158,7 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro return err } - return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath) + return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath, "", "") } func startService( @@ -152,12 +167,24 @@ func startService( visitorCfgs []v1.VisitorConfigurer, unsafeFeatures *security.UnsafeFeatures, cfgFile string, + nodeName string, + tunnelRemark string, ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) + // Display banner before starting the service + banner.DisplayBanner() + + log.Infof("Nya! %s 启动中", version.Full()) + + // Display node information if available + if nodeName != "" { + log.Info("已获取到配置文件", "隧道名称", tunnelRemark, "使用节点", nodeName) + } + if cfgFile != "" { - log.Infof("start frpc service for config file [%s]", cfgFile) - defer log.Infof("frpc service for config file [%s] stopped", cfgFile) + log.Infof("启动 frpc 服务 [%s]", cfgFile) + defer log.Infof("frpc 服务 [%s] 已停止", cfgFile) } svr, err := client.NewService(client.ServiceOptions{ Common: cfg, @@ -177,3 +204,104 @@ func startService( } return svr.Run(context.Background()) } + +// APIResponse represents the response from LoliaFRP API +type APIResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Config string `json:"config"` + NodeName string `json:"node_name"` + TunnelRemark string `json:"tunnel_remark"` + } `json:"data"` +} + +func runClientWithToken(token string, unsafeFeatures *security.UnsafeFeatures) error { + // Get API server address from environment variable + apiServer := os.Getenv("LOLIA_API") + if apiServer == "" { + apiServer = "https://api.lolia.link" + } + + // Fetch config from API + url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config/%s", apiServer, token) + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to fetch config from API: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API returned status code: %d", resp.StatusCode) + } + + var apiResp APIResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + return fmt.Errorf("failed to decode API response: %v", err) + } + + if apiResp.Code != 200 { + return fmt.Errorf("API error: %s", apiResp.Msg) + } + + // Decode base64 config + configBytes, err := base64.StdEncoding.DecodeString(apiResp.Data.Config) + if err != nil { + return fmt.Errorf("failed to decode base64 config: %v", err) + } + + // Load config directly from bytes + return runClientWithConfig(configBytes, unsafeFeatures, apiResp.Data.NodeName, apiResp.Data.TunnelRemark) +} + +func runClientWithConfig(configBytes []byte, unsafeFeatures *security.UnsafeFeatures, nodeName, tunnelRemark string) error { + // Render template first + renderedBytes, err := config.RenderWithTemplate(configBytes, config.GetValues()) + if err != nil { + return fmt.Errorf("failed to render template: %v", err) + } + + var allCfg v1.ClientConfig + if err := config.LoadConfigure(renderedBytes, &allCfg, strictConfigMode); err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + cfg := &allCfg.ClientCommonConfig + proxyCfgs := make([]v1.ProxyConfigurer, 0, len(allCfg.Proxies)) + for _, c := range allCfg.Proxies { + proxyCfgs = append(proxyCfgs, c.ProxyConfigurer) + } + visitorCfgs := make([]v1.VisitorConfigurer, 0, len(allCfg.Visitors)) + for _, c := range allCfg.Visitors { + visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) + } + + // Call Complete to fill in default values + if err := cfg.Complete(); err != nil { + return fmt.Errorf("failed to complete config: %v", err) + } + + // Call Complete for all proxies to add name prefix (e.g., user.tunnel_name) + for _, c := range proxyCfgs { + c.Complete(cfg.User) + } + for _, c := range visitorCfgs { + c.Complete(cfg) + } + + if len(cfg.FeatureGates) > 0 { + if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil { + return err + } + } + + warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } + if err != nil { + return err + } + + return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, "", nodeName, tunnelRemark) +} diff --git a/go.mod b/go.mod index e23facde..d885bb2d 100644 --- a/go.mod +++ b/go.mod @@ -39,10 +39,18 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/log v0.4.2 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -51,6 +59,10 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/transport/v2 v2.2.1 // indirect @@ -60,13 +72,16 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/xorsimd v0.4.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/vishvananda/netns v0.0.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect diff --git a/go.sum b/go.sum index f73117a3..c5e5a0b3 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,25 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= +github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= @@ -26,6 +40,8 @@ github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMB github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -70,8 +86,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= @@ -109,6 +133,8 @@ github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9M github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA= github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -149,6 +175,8 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk= github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= @@ -167,6 +195,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -209,6 +239,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/util/banner/banner.go b/pkg/util/banner/banner.go new file mode 100644 index 00000000..e8cc3cff --- /dev/null +++ b/pkg/util/banner/banner.go @@ -0,0 +1,12 @@ +package banner + +import "fmt" + +func DisplayBanner() { + fmt.Println(" __ ___ __________ ____ ________ ____") + fmt.Println(" / / ____ / (_)___ _/ ____/ __ \\/ __ \\ / ____/ / / _/") + fmt.Println(" / / / __ \\/ / / __ `/ /_ / /_/ / /_/ /_____/ / / / / / ") + fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ") + fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ") + fmt.Println(" ") +} diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go index 327d4ef6..6c6c36b5 100644 --- a/pkg/util/log/log.go +++ b/pkg/util/log/log.go @@ -16,13 +16,18 @@ package log import ( "bytes" + "io" "os" + "path/filepath" + "strings" + "time" - "github.com/fatedier/golib/log" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" ) var ( - TraceLevel = log.TraceLevel + TraceLevel = log.DebugLevel DebugLevel = log.DebugLevel InfoLevel = log.InfoLevel WarnLevel = log.WarnLevel @@ -32,39 +37,156 @@ var ( var Logger *log.Logger func init() { - Logger = log.New( - log.WithCaller(true), - log.AddCallerSkip(1), - log.WithLevel(log.InfoLevel), - ) + Logger = log.NewWithOptions(os.Stderr, log.Options{ + ReportCaller: true, + ReportTimestamp: true, + TimeFormat: time.Kitchen, + Prefix: "LoliaFRP-CLI", + CallerOffset: 1, + }) + // 设置自定义样式以支持 Trace 级别 + styles := log.DefaultStyles() + styles.Levels[TraceLevel] = lipgloss.NewStyle(). + SetString("TRACE"). + Bold(true). + MaxWidth(5). + Foreground(lipgloss.Color("61")) + Logger.SetStyles(styles) } func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) { - options := []log.Option{} + var output io.Writer + var err error + if logPath == "console" { - if !disableLogColor { - options = append(options, - log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{ - Colorful: true, - }, os.Stdout)), - ) - } + output = os.Stdout } else { - writer := log.NewRotateFileWriter(log.RotateFileConfig{ - FileName: logPath, - Mode: log.RotateFileModeDaily, - MaxDays: maxDays, - }) - writer.Init() - options = append(options, log.WithOutput(writer)) + // Use rotating file writer + output, err = NewRotateFileWriter(logPath, maxDays) + if err != nil { + // Fallback to console if file creation fails + output = os.Stdout + } } level, err := log.ParseLevel(levelStr) if err != nil { level = log.InfoLevel } - options = append(options, log.WithLevel(level)) - Logger = Logger.WithOptions(options...) + + Logger = log.NewWithOptions(output, log.Options{ + ReportCaller: true, + ReportTimestamp: true, + TimeFormat: time.Kitchen, + Prefix: "LoliaFRP-CLI", + CallerOffset: 1, + Level: level, + }) +} + +// NewRotateFileWriter creates a rotating file writer +func NewRotateFileWriter(filePath string, maxDays int) (*RotateFileWriter, error) { + w := &RotateFileWriter{ + filePath: filePath, + maxDays: maxDays, + lastRotate: time.Now(), + currentDate: time.Now().Format("2006-01-02"), + } + + if err := w.openFile(); err != nil { + return nil, err + } + + return w, nil +} + +// RotateFileWriter implements io.Writer with daily rotation +type RotateFileWriter struct { + filePath string + maxDays int + file *os.File + lastRotate time.Time + currentDate string +} + +func (w *RotateFileWriter) openFile() error { + var err error + w.file, err = os.OpenFile(w.filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + return err +} + +func (w *RotateFileWriter) checkRotate() error { + now := time.Now() + currentDate := now.Format("2006-01-02") + + if currentDate != w.currentDate { + // Close current file + if w.file != nil { + w.file.Close() + } + + // Rename current file with date suffix + oldPath := w.filePath + newPath := w.filePath + "." + w.currentDate + if _, err := os.Stat(oldPath); err == nil { + os.Rename(oldPath, newPath) + } + + // Clean up old log files + w.cleanupOldLogs(now) + + // Update current date and open new file + w.currentDate = currentDate + w.lastRotate = now + return w.openFile() + } + + return nil +} + +func (w *RotateFileWriter) cleanupOldLogs(now time.Time) { + if w.maxDays <= 0 { + return + } + + cutoffDate := now.AddDate(0, 0, -w.maxDays) + + // Find and remove old log files + dir := filepath.Dir(w.filePath) + base := filepath.Base(w.filePath) + + files, _ := os.ReadDir(dir) + for _, f := range files { + if f.IsDir() { + continue + } + + name := f.Name() + if strings.HasPrefix(name, base+".") { + // Extract date from filename (base.YYYY-MM-DD) + dateStr := strings.TrimPrefix(name, base+".") + if len(dateStr) == 10 { + fileDate, err := time.Parse("2006-01-02", dateStr) + if err == nil && fileDate.Before(cutoffDate) { + os.Remove(filepath.Join(dir, name)) + } + } + } + } +} + +func (w *RotateFileWriter) Write(p []byte) (n int, err error) { + if err := w.checkRotate(); err != nil { + return 0, err + } + return w.file.Write(p) +} + +func (w *RotateFileWriter) Close() error { + if w.file != nil { + return w.file.Close() + } + return nil } func Errorf(format string, v ...any) { @@ -75,6 +197,10 @@ func Warnf(format string, v ...any) { Logger.Warnf(format, v...) } +func Info(format string, v ...any) { + Logger.Info(format, v...) +} + func Infof(format string, v ...any) { Logger.Infof(format, v...) } @@ -84,11 +210,12 @@ func Debugf(format string, v ...any) { } func Tracef(format string, v ...any) { - Logger.Tracef(format, v...) + Logger.Logf(TraceLevel, format, v...) } func Logf(level log.Level, offset int, format string, v ...any) { - Logger.Logf(level, offset, format, v...) + // charmbracelet/log doesn't support offset, so we ignore it + Logger.Logf(level, format, v...) } type WriteLogger struct { @@ -104,6 +231,8 @@ func NewWriteLogger(level log.Level, offset int) *WriteLogger { } func (w *WriteLogger) Write(p []byte) (n int, err error) { - Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n"))) + // charmbracelet/log doesn't support offset in Log + msg := string(bytes.TrimRight(p, "\n")) + Logger.Log(w.level, msg) return len(p), nil } diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index a87f5002..b0818724 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -14,7 +14,7 @@ package version -var version = "LoliaFRP 0.66.0" +var version = "LoliaFRP-CLI 0.66.0" func Full() string { return version diff --git a/pkg/util/xlog/xlog.go b/pkg/util/xlog/xlog.go index a1a58d42..88d5d72f 100644 --- a/pkg/util/xlog/xlog.go +++ b/pkg/util/xlog/xlog.go @@ -111,5 +111,5 @@ func (l *Logger) Debugf(format string, v ...any) { } func (l *Logger) Tracef(format string, v ...any) { - log.Logger.Tracef(l.prefixString+format, v...) + log.Logger.Logf(log.TraceLevel, l.prefixString+format, v...) }