diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..59196d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +## v0.1.2 (2024-04-15) + +This release introduces a set of code optimizations, improvements to documentation, and an extension of the testing suite. This is a patch release and does not contain any breaking changes or new features. It is backwards compatible with v0.1.1. + +### Improvements + +**Code Optimization**: Refactored some parts of the codebase to enhance performance and readability. + +**Documentation**: Updated and added inline documentation and comments to make the codebase more accessible for new contributors. + +**Unit Testing**: Increased test coverage by adding new unit tests for previously untested functions. + + +## v0.1.1 (2024-04-12) +Init Project. + +## v0.1.0 (2024-04-11) +Init Project. \ No newline at end of file diff --git a/Makefile b/Makefile index 85a3564..9f3cb66 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,10 @@ vet: go vet ./... gssh: - env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags tiny-frpc -o bin/tiny-frpc ./cmd/go_ssh + env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags gssh -o bin/tiny-frpc ./cmd/frpc nssh: - env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags tiny-frpc-ssh -o bin/tiny-frpc-ssh ./cmd/native_ssh + env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags nssh -o bin/tiny-frpc-ssh ./cmd/frpc test: gotest diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index ef1d764..d5da316 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -14,8 +14,8 @@ app: gomips=$(shell echo "$(n)" | cut -d : -f 3);\ target_suffix=$${os}_$${arch};\ echo "Build $${os}-$${arch}...";\ - env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags tiny-frpc -o ./release/tiny-frpc_$${target_suffix} ./cmd/go_ssh;\ - env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags tiny-frpc-ssh -o ./release/tiny-frpc-ssh_$${target_suffix} ./cmd/native_ssh;\ + env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags gssh -o ./release/tiny-frpc_$${target_suffix} ./cmd/frpc;\ + env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags nssh -o ./release/tiny-frpc-ssh_$${target_suffix} ./cmd/frpc;\ echo "Build $${os}-$${arch} done";\ ) @mv ./release/tiny-frpc_windows_amd64 ./release/tiny-frpc_windows_amd64.exe diff --git a/README.md b/README.md index d071242..4473b4c 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,11 @@ Assuming that the domain name 'test-tiny-frpc.frps.com' is resolved to the machi > curl -v 'http://test-tiny-frpc.frps.com/' + +# Principle +![how tiny frpc works](doc/pic/architecture.png) + + # Disclaimer **This is currently a preview version. Compatibility is not guaranteed. It is presently for testing purposes only and should not be used in production environments!** \ No newline at end of file diff --git a/README_zh.md b/README_zh.md index 444194b..9f38245 100644 --- a/README_zh.md +++ b/README_zh.md @@ -88,6 +88,9 @@ HTTP 服务: 访问到内网的 HTTP 服务。 +# 原理 +![how tiny frpc works](doc/pic/architecture.png) + # 说明 diff --git a/Release.md b/Release.md index 60efee0..acc378e 100644 --- a/Release.md +++ b/Release.md @@ -1,14 +1 @@ -### Features - -Introducing version of our proxy solution, we enable reverse proxying through standard ssh protocol communicating with frps. - -We offer two binary program options for different user needs: - -The Go standalone mode: This version works independently to communicate with frps. It's built for those who favor self-sufficiency and prefer a simpler deployment process. - -The ssh native mode: This version works in conjunction with the operating system's ssh program. It is intended for those whose systems comprise a preconfigured ssh setup, or who wish to utilize the existing ssh program. -Both versions necessitate the provision of a frpc toml format configuration file to function correctly. - -This release is just the beginning. Stay tuned for more advanced features, improvements and we are always keen to hear user feedback for future developments. - -**This is currently a preview version. Compatibility is not guaranteed. It is presently for testing purposes only and should not be used in production environments!!!** \ No newline at end of file +Please refer to [CHANGELOG.md](https://github.com/gofrp/tiny-frpc/blob/main/CHANGELOG.md) for details. \ No newline at end of file diff --git a/cmd/frpc/default_runner.go b/cmd/frpc/default_runner.go new file mode 100644 index 0000000..0a9861e --- /dev/null +++ b/cmd/frpc/default_runner.go @@ -0,0 +1,24 @@ +package main + +import ( + v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" + "github.com/gofrp/tiny-frpc/pkg/model" + "github.com/gofrp/tiny-frpc/pkg/util/log" +) + +var runner model.Runner = defaultRunner{} + +type defaultRunner struct{} + +func (r defaultRunner) New(commonCfg *v1.ClientCommonConfig, pxyCfg []v1.ProxyConfigurer, vCfg []v1.VisitorConfigurer) (err error) { + log.Infof("init default runner") + return +} + +func (r defaultRunner) Run() (err error) { + return +} + +func (r defaultRunner) Close() (err error) { + return +} diff --git a/cmd/frpc/gssh_runner.go b/cmd/frpc/gssh_runner.go new file mode 100644 index 0000000..003b019 --- /dev/null +++ b/cmd/frpc/gssh_runner.go @@ -0,0 +1,96 @@ +//go:build gssh +// +build gssh + +package main + +import ( + "sync" + + "github.com/gofrp/tiny-frpc/pkg/config" + v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" + "github.com/gofrp/tiny-frpc/pkg/gssh" + "github.com/gofrp/tiny-frpc/pkg/util/log" +) + +type GoSSHRun struct { + commonCfg *v1.ClientCommonConfig + pxyCfg []v1.ProxyConfigurer + vCfg []v1.VisitorConfigurer + + wg *sync.WaitGroup + mu *sync.RWMutex + + tcs map[int]*gssh.TunnelClient +} + +func (gr *GoSSHRun) New(commonCfg *v1.ClientCommonConfig, pxyCfg []v1.ProxyConfigurer, vCfg []v1.VisitorConfigurer) error { + log.Infof("init go ssh runner") + + runner = &GoSSHRun{ + commonCfg: commonCfg, + pxyCfg: pxyCfg, + vCfg: vCfg, + + wg: new(sync.WaitGroup), + mu: &sync.RWMutex{}, + + tcs: make(map[int]*gssh.TunnelClient, 0), + } + return nil +} + +func (gr *GoSSHRun) Run() error { + params := config.ParseFRPCConfigToGoSSHParam(gr.commonCfg, gr.pxyCfg, gr.vCfg) + + log.Infof("proxy total len: %v", len(params)) + + for i, cmd := range params { + gr.wg.Add(1) + + go func(cmd config.GoSSHParam, idx int) { + defer gr.wg.Done() + + log.Infof("start to run: %v", cmd) + + tc, err := gssh.NewTunnelClient(cmd.LocalAddr, cmd.ServerAddr, cmd.SSHExtraCmd) + if err != nil { + log.Errorf("new ssh tunnel client error: %v", err) + return + } + + gr.mu.Lock() + gr.tcs[idx] = tc + gr.mu.Unlock() + + err = tc.Start() + if err != nil { + log.Errorf("cmd: %v run error: %v", cmd, err) + + gr.mu.Lock() + delete(gr.tcs, idx) + gr.mu.Unlock() + + return + } + }(cmd, i) + } + + gr.wg.Wait() + + log.Infof("stopping ssh tunnel to frps") + return nil +} + +func (gr *GoSSHRun) Close() error { + gr.mu.Lock() + defer gr.mu.Unlock() + + for _, tc := range gr.tcs { + tc.Close() + } + return nil +} + +func init() { + runner = &GoSSHRun{} +} diff --git a/cmd/native_ssh/main.go b/cmd/frpc/main.go similarity index 59% rename from cmd/native_ssh/main.go rename to cmd/frpc/main.go index e8ca984..7b19498 100644 --- a/cmd/native_ssh/main.go +++ b/cmd/frpc/main.go @@ -1,14 +1,16 @@ package main import ( - "context" "flag" "fmt" - "sync" + "os" + "os/signal" + "syscall" + "time" "github.com/gofrp/tiny-frpc/pkg/config" v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" - "github.com/gofrp/tiny-frpc/pkg/nssh" + "github.com/gofrp/tiny-frpc/pkg/model" "github.com/gofrp/tiny-frpc/pkg/util" "github.com/gofrp/tiny-frpc/pkg/util/log" "github.com/gofrp/tiny-frpc/pkg/util/version" @@ -20,7 +22,7 @@ func main() { showVersion bool ) - flag.StringVar(&cfgFilePath, "c", "frpc.toml", "Path to the configuration file") + flag.StringVar(&cfgFilePath, "c", "frpc.toml", "path to the configuration file") flag.BoolVar(&showVersion, "v", false, "version of tiny-frpc") flag.Parse() @@ -43,29 +45,28 @@ func main() { log.Infof("common cfg: %v, proxy cfg: %v, visitor cfg: %v", util.JSONEncode(cfg), util.JSONEncode(proxyCfgs), util.JSONEncode(visitorCfgs)) - sshCmds := config.ParseFRPCConfigToSSHCmd(cfg, proxyCfgs, visitorCfgs) - - log.Infof("ssh cmds len_num: %v", len(sshCmds)) - - closeCh := make(chan struct{}) - wg := new(sync.WaitGroup) - - for _, cmd := range sshCmds { - wg.Add(1) - - go func(cmd string) { - defer wg.Done() - ctx := context.Background() + err = runner.New(cfg, proxyCfgs, visitorCfgs) + if err != nil { + log.Errorf("new runner error: %v", err) + return + } - log.Infof("start to run: %v", cmd) + go handleTermSignal(runner) - task := nssh.NewCmdWrapper(ctx, cmd, closeCh) - task.ExecuteCommand(ctx) - }(cmd) + err = runner.Run() + if err != nil { + log.Errorf("run error: %v", err) + return } - wg.Wait() - close(closeCh) + time.Sleep(time.Millisecond * 10) + log.Infof("process exit...") +} - log.Infof("stopping process calling native ssh to frps, exit...") +func handleTermSignal(run model.Runner) { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + v := <-ch + log.Infof("get signal term: %v, gracefully shutdown", v) + run.Close() } diff --git a/cmd/frpc/nssh_runner.go b/cmd/frpc/nssh_runner.go new file mode 100644 index 0000000..7846e69 --- /dev/null +++ b/cmd/frpc/nssh_runner.go @@ -0,0 +1,87 @@ +//go:build nssh +// +build nssh + +package main + +import ( + "context" + "sync" + + "github.com/gofrp/tiny-frpc/pkg/config" + v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" + "github.com/gofrp/tiny-frpc/pkg/nssh" + "github.com/gofrp/tiny-frpc/pkg/util/log" +) + +type NativeSSHRun struct { + commonCfg *v1.ClientCommonConfig + pxyCfg []v1.ProxyConfigurer + vCfg []v1.VisitorConfigurer + + wg *sync.WaitGroup + mu *sync.RWMutex + + cws map[int]*nssh.CmdWrapper +} + +func (nr *NativeSSHRun) New(commonCfg *v1.ClientCommonConfig, pxyCfg []v1.ProxyConfigurer, vCfg []v1.VisitorConfigurer) error { + log.Infof("init native ssh runner") + + runner = &NativeSSHRun{ + commonCfg: commonCfg, + pxyCfg: pxyCfg, + vCfg: vCfg, + + wg: new(sync.WaitGroup), + mu: &sync.RWMutex{}, + + cws: make(map[int]*nssh.CmdWrapper, 0), + } + return nil +} + +func (nr *NativeSSHRun) Run() error { + cmdParams := config.ParseFRPCConfigToSSHCmd(nr.commonCfg, nr.pxyCfg, nr.vCfg) + + log.Infof("proxy total len: %v", len(cmdParams)) + + for i, cmd := range cmdParams { + nr.wg.Add(1) + + go func(cmd string, idx int) { + defer nr.wg.Done() + ctx := context.Background() + + log.Infof("start to run: %v", cmd) + + cmdWrapper := nssh.NewCmdWrapper(ctx, cmd) + + nr.mu.Lock() + nr.cws[idx] = cmdWrapper + nr.mu.Unlock() + + cmdWrapper.ExecuteCommand(ctx) + }(cmd, i) + } + + nr.wg.Wait() + + log.Infof("stopping native ssh tunnel to frps") + + return nil +} + +func (nr *NativeSSHRun) Close() error { + nr.mu.Lock() + defer nr.mu.Unlock() + + for _, c := range nr.cws { + c.Close() + } + + return nil +} + +func init() { + runner = &NativeSSHRun{} +} diff --git a/cmd/go_ssh/main.go b/cmd/go_ssh/main.go deleted file mode 100644 index a880bcb..0000000 --- a/cmd/go_ssh/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "sync" - - "github.com/gofrp/tiny-frpc/pkg/config" - v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" - "github.com/gofrp/tiny-frpc/pkg/gssh" - "github.com/gofrp/tiny-frpc/pkg/util" - "github.com/gofrp/tiny-frpc/pkg/util/log" - "github.com/gofrp/tiny-frpc/pkg/util/version" -) - -func main() { - var ( - cfgFilePath string - showVersion bool - ) - - flag.StringVar(&cfgFilePath, "c", "frpc.toml", "path to the configuration file") - flag.BoolVar(&showVersion, "v", false, "version of tiny-frpc") - flag.Parse() - - if showVersion { - fmt.Println(version.Full()) - return - } - - cfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFilePath, true) - if err != nil { - log.Errorf("load frpc config error: %v", err) - return - } - - _, err = v1.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs) - if err != nil { - log.Errorf("validate frpc config error: %v", err) - return - } - - log.Infof("common cfg: %v, proxy cfg: %v, visitor cfg: %v", util.JSONEncode(cfg), util.JSONEncode(proxyCfgs), util.JSONEncode(visitorCfgs)) - - goSSHParams := config.ParseFRPCConfigToGoSSHParam(cfg, proxyCfgs, visitorCfgs) - - log.Infof("ssh cmds len_num: %v", len(goSSHParams)) - - wg := new(sync.WaitGroup) - - for _, cmd := range goSSHParams { - wg.Add(1) - - go func(cmd config.GoSSHParam) { - defer wg.Done() - - log.Infof("start to run: %v", cmd) - - tc, err := gssh.NewTunnelClient(cmd.LocalAddr, cmd.ServerAddr, cmd.SSHExtraCmd) - if err != nil { - log.Errorf("new ssh tunnel client error: %v", err) - return - } - - err = tc.Start() - if err != nil { - log.Errorf("cmd: %v run error: %v", cmd, err) - return - } - }(cmd) - } - - wg.Wait() - - log.Infof("stopping process calling native ssh to frps, exit...") -} diff --git a/conf/frpc_full_example.toml b/conf/frpc_full_example.toml index 81915b5..8cf136f 100644 --- a/conf/frpc_full_example.toml +++ b/conf/frpc_full_example.toml @@ -1,12 +1,10 @@ -##################### # This configuration file is a subset of `https://github.com/fatedier/frp/blob/dev/conf/frpc_full_example.toml`, with no additional parameters added, but some parameters not supported by the command line have been trimmed. # The list of parameters supported by this file corresponds one-to-one with the parameters started under the command line in [frp ssh tunnel gateway](https://github.com/fatedier/frp?tab=readme-ov-file#ssh-tunnel-gateway). Our open-source project is aimed at parsing the configuration file into standard ssh commands. -##################### # This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues. -# your proxy name will be changed to {user}.{proxy} +# your proxy name will be changed to {user}.{user}.{proxy} user = "your_name" # A literal address or host name for IPv6 must be enclosed @@ -18,7 +16,6 @@ serverAddr = "0.0.0.0" # more info https://github.com/fatedier/frp?tab=readme-ov-file#ssh-tunnel-gateway serverPort = 2200 - # method only be "token" value, it's hard code auth.method = "token" # auth token diff --git a/doc/pic/architecture.png b/doc/pic/architecture.png new file mode 100644 index 0000000..2380322 Binary files /dev/null and b/doc/pic/architecture.png differ diff --git a/go.mod b/go.mod index a05c472..ec9aa2d 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/gofrp/tiny-frpc go 1.21 require ( - github.com/pelletier/go-toml/v2 v2.2.0 + github.com/pelletier/go-toml/v2 v2.2.1 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.22.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 78b620d..f09be45 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -14,12 +14,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/parse.go b/pkg/config/parse.go index 9cf1ad3..9475fcc 100644 --- a/pkg/config/parse.go +++ b/pkg/config/parse.go @@ -7,7 +7,6 @@ import ( "strings" v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" - "github.com/gofrp/tiny-frpc/pkg/util/log" ) type GoSSHParam struct { @@ -53,9 +52,6 @@ func ParseFRPCConfigToSSHCmd( switch pv.GetProxyType() { case v1.ProxyTypeTCP, v1.ProxyTypeHTTP, v1.ProxyTypeHTTPS, v1.ProxyTypeTCPMUX, v1.ProxyTypeSTCP: cmd := genFullSSHCmd(*cfg, pv) - - log.Infof("get cmd: %v ", cmd) - res = append(res, cmd) default: panic("invalid proxy type: " + pv.GetProxyType()) diff --git a/pkg/gssh/ssh.go b/pkg/gssh/ssh_client.go similarity index 86% rename from pkg/gssh/ssh.go rename to pkg/gssh/ssh_client.go index 93b0710..a4a8835 100644 --- a/pkg/gssh/ssh.go +++ b/pkg/gssh/ssh_client.go @@ -16,7 +16,7 @@ import ( type TunnelClient struct { localAddr string sshServer string - commands string + command string sshConn *ssh.Client ln net.Listener @@ -46,13 +46,13 @@ func publicKeyAuthFunc(kPath string) (ssh.AuthMethod, error) { return ssh.PublicKeys(signer), nil } -func NewTunnelClient(localAddr string, sshServer string, commands string) (*TunnelClient, error) { +func NewTunnelClient(localAddr string, sshServer string, command string) (*TunnelClient, error) { privateKeyPath, err := getDefaultPrivateKeyPath() if err != nil { return nil, fmt.Errorf("failed to get default private key path: %v", err) } - log.Infof("get ssh private key file: %v", privateKeyPath) + log.Infof("get ssh private key file: [%v] to communicate with frps by ssh protocol", privateKeyPath) authMethod, err := publicKeyAuthFunc(privateKeyPath) if err != nil { @@ -62,7 +62,7 @@ func NewTunnelClient(localAddr string, sshServer string, commands string) (*Tunn return &TunnelClient{ localAddr: localAddr, sshServer: sshServer, - commands: commands, + command: command, authMethod: authMethod, }, nil } @@ -92,11 +92,13 @@ func (c *TunnelClient) Start() error { } defer session.Close() - err = session.Start(c.commands) + err = session.Start(c.command) if err != nil { return err } + log.Infof("session start cmd [%v] success", c.command) + c.serveListener() return nil } @@ -117,6 +119,9 @@ func (c *TunnelClient) serveListener() { log.Errorf("ssh tunnel cient accept error: %v", err) return } + + log.Infof("accept a new connection. remote: %v, local: %v", conn.RemoteAddr().String(), conn.LocalAddr().String()) + go c.hanldeConn(conn) } } diff --git a/pkg/model/interface.go b/pkg/model/interface.go new file mode 100644 index 0000000..bc0b520 --- /dev/null +++ b/pkg/model/interface.go @@ -0,0 +1,11 @@ +package model + +import ( + v1 "github.com/gofrp/tiny-frpc/pkg/config/v1" +) + +type Runner interface { + New(commonCfg *v1.ClientCommonConfig, pxyCfg []v1.ProxyConfigurer, vCfg []v1.VisitorConfigurer) error + Run() error + Close() error +} diff --git a/pkg/nssh/cmd_wrapper.go b/pkg/nssh/cmd_wrapper.go index 844b7ef..bbc5cba 100644 --- a/pkg/nssh/cmd_wrapper.go +++ b/pkg/nssh/cmd_wrapper.go @@ -12,48 +12,38 @@ import ( ) type CmdWrapper struct { - name string - closeCh chan struct{} - command string cmd *exec.Cmd + + outputCh chan string + errCh chan error } -func NewCmdWrapper(ctx context.Context, command string, closeCh chan struct{}) *CmdWrapper { +func NewCmdWrapper(ctx context.Context, command string) *CmdWrapper { parts := strings.Fields(command) wrapper := &CmdWrapper{ cmd: exec.CommandContext(ctx, parts[0], parts[1:]...), command: command, - closeCh: closeCh, - } - go wrapper.wait() + outputCh: make(chan string), + errCh: make(chan error, 1), + } return wrapper } -func (cs *CmdWrapper) wait() { - <-cs.closeCh - cs.cmd.Wait() -} - func (cs *CmdWrapper) ExecuteCommand(ctx context.Context) { - outputCh := make(chan string) - errCh := make(chan error, 1) - defer close(outputCh) - defer close(errCh) - go func() { - for out := range outputCh { + for out := range cs.outputCh { // do not use log, use standard print to better show output fmt.Println(out) } }() go func() { - for err := range errCh { - log.Errorf("run cmd: %v error: %v", cs.command, err) + for err := range cs.errCh { + log.Errorf("run cmd: [%v] get error: %v", cs.command, err) } }() @@ -74,18 +64,18 @@ func (cs *CmdWrapper) ExecuteCommand(ctx context.Context) { } if err := cs.cmd.Start(); err != nil { - errCh <- err + cs.errCh <- err return } stdoutReader := bufio.NewReader(stdoutPipe) stderrReader := bufio.NewReader(stderrPipe) - go cs.readPipe(stdoutReader, outputCh) - go cs.readPipe(stderrReader, outputCh) + go cs.readPipe(stdoutReader, cs.outputCh) + go cs.readPipe(stderrReader, cs.outputCh) if err := cs.cmd.Wait(); err != nil { - errCh <- err + cs.errCh <- err } } @@ -101,3 +91,9 @@ func (cs *CmdWrapper) readPipe(pipe *bufio.Reader, outputCh chan<- string) { outputCh <- line } } + +func (cs *CmdWrapper) Close() { + if cs.cmd != nil && cs.cmd.Process != nil { + cs.cmd.Process.Kill() + } +} diff --git a/pkg/util/log/log_test.go b/pkg/util/log/log_test.go new file mode 100644 index 0000000..8c7994e --- /dev/null +++ b/pkg/util/log/log_test.go @@ -0,0 +1,62 @@ +package log + +import ( + "bytes" + "strings" + "testing" + "time" +) + +func TestLogger(t *testing.T) { + tests := []struct { + name string + level string + format string + values []interface{} + }{ + { + name: "INFO", + level: LevelInfo, + format: "This is an %s message", + values: []interface{}{"info"}, + }, + { + name: "WARN", + level: LevelWarn, + format: "This is an %s message", + values: []interface{}{"warning"}, + }, + { + name: "ERROR", + level: LevelError, + format: "This is an %s message", + values: []interface{}{"error"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + logger := NewLogger(&buf) + + switch tt.level { + case LevelInfo: + logger.Info(tt.format, tt.values...) + case LevelWarn: + logger.Warn(tt.format, tt.values...) + case LevelError: + logger.Error(tt.format, tt.values...) + } + + output := buf.String() + if !strings.Contains(output, tt.level) || !strings.Contains(output, "This is an") { + t.Errorf("log output should contain the level '%s' and message 'This is an', got: %s", tt.level, output) + } + + // Optionally check if the output contains a timestamp + if !strings.Contains(output, time.Now().Format(time.RFC3339)[:10]) { + t.Errorf("log output should contain a correctly formatted timestamp, got: %s", output) + } + }) + } +} diff --git a/pkg/util/misc.go b/pkg/util/misc.go index b095440..a7edce1 100644 --- a/pkg/util/misc.go +++ b/pkg/util/misc.go @@ -4,9 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "os" - "os/signal" - "syscall" "unicode" ) @@ -54,9 +51,3 @@ func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { return elseOutput } - -func handleTermSignal() { - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) - <-ch -} diff --git a/pkg/util/misc_test.go b/pkg/util/misc_test.go new file mode 100644 index 0000000..ee5163d --- /dev/null +++ b/pkg/util/misc_test.go @@ -0,0 +1,175 @@ +package util + +import ( + "reflect" + "strings" + "testing" +) + +func TestJSONEncode(t *testing.T) { + tests := []struct { + name string + args interface{} + want string + wantError bool + }{ + { + name: "normal struct", + args: struct { + Name string + Age int + }{"John", 30}, + want: `{"Name":"John","Age":30}`, + wantError: false, + }, + { + name: "invalid data", + args: make(chan int), // channels are not serializable to JSON + want: "args: {}, error: json: unsupported type: chan int", + wantError: true, + }, + { + name: "normal map", + args: map[string]interface{}{ + "hello": "world", + "age": 25, + }, + want: `{"age":25,"hello":"world"}`, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := JSONEncode(tt.args) + if (got != tt.want) != tt.wantError { + t.Errorf("JSONEncode() = %v, want %v", got, tt.want) + } + + if tt.wantError && !strings.Contains(got, "error") { + t.Errorf("Expected an error in JSONEncode() for input %v, but got none", tt.args) + } + }) + } +} + +func TestEmptyOrInt(t *testing.T) { + tests := []struct { + value int + fallback int + want int + }{ + {0, 10, 10}, + {5, 10, 5}, + } + + for _, tt := range tests { + if got := EmptyOr(tt.value, tt.fallback); !reflect.DeepEqual(got, tt.want) { + t.Errorf("EmptyOr() with input '%v': got %v, want %v", tt.value, got, tt.want) + } + } +} + +func TestEmptyOrString(t *testing.T) { + tests := []struct { + value string + fallback string + want string + }{ + {"", "fallback", "fallback"}, + {"value", "fallback", "value"}, + } + + for _, tt := range tests { + if got := EmptyOr(tt.value, tt.fallback); !reflect.DeepEqual(got, tt.want) { + t.Errorf("EmptyOr() with input '%v': got %v, want %v", tt.value, got, tt.want) + } + } +} + +func TestEmptyOrBool(t *testing.T) { + tests := []struct { + value bool + fallback bool + want bool + }{ + {false, true, true}, + {true, false, true}, + } + + for _, tt := range tests { + if got := EmptyOr(tt.value, tt.fallback); !reflect.DeepEqual(got, tt.want) { + t.Errorf("EmptyOr() with input '%v': got %v, want %v", tt.value, got, tt.want) + } + } +} + +func TestIsJSONBuffer(t *testing.T) { + tests := []struct { + buf []byte + want bool + }{ + {[]byte(`{"key": "value"}`), true}, + {[]byte(` `), false}, + {[]byte(`not JSON`), false}, + {[]byte(` {"key": "value"}`), true}, + {[]byte(`[]`), false}, + {nil, false}, + } + + for _, tt := range tests { + t.Run(string(tt.buf), func(t *testing.T) { + if got := IsJSONBuffer(tt.buf); got != tt.want { + t.Errorf("IsJSONBuffer() = %v, want %v for buf %q", got, tt.want, tt.buf) + } + }) + } +} + +func TestTernary(t *testing.T) { + tests := []struct { + name string + condition bool + ifOutput interface{} + elseOutput interface{} + want interface{} + }{ + { + name: "Condition true int", + condition: true, + ifOutput: 1, + elseOutput: 2, + want: 1, + }, + { + name: "Condition false int", + condition: false, + ifOutput: 1, + elseOutput: 2, + want: 2, + }, + { + name: "Condition true string", + condition: true, + ifOutput: "TrueValue", + elseOutput: "FalseValue", + want: "TrueValue", + }, + { + name: "Condition false string", + condition: false, + ifOutput: "TrueValue", + elseOutput: "FalseValue", + want: "FalseValue", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := Ternary(tc.condition, tc.ifOutput, tc.elseOutput) + if got != tc.want { + t.Errorf("Ternary() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/pkg/util/net_test.go b/pkg/util/net_test.go new file mode 100644 index 0000000..55a50a7 --- /dev/null +++ b/pkg/util/net_test.go @@ -0,0 +1,71 @@ +package util + +import ( + "bytes" + "testing" +) + +func TestBufferPool(t *testing.T) { + bufPool.New = func() interface{} { + return make([]byte, 512) + } + bufPool1k.New = func() interface{} { + return make([]byte, 1*1024) + } + bufPool2k.New = func() interface{} { + return make([]byte, 2*1024) + } + bufPool5k.New = func() interface{} { + return make([]byte, 5*1024) + } + bufPool16k.New = func() interface{} { + return make([]byte, 16*1024) + } + + tests := []struct { + name string + size int + wantLength int + }{ + {"Get 16k", 16 * 1024, 16 * 1024}, + {"Get 5k", 5 * 1024, 5 * 1024}, + {"Get 2k", 2 * 1024, 2 * 1024}, + {"Get 1k", 1 * 1024, 1 * 1024}, + {"Get Less than 1k", 512, 512}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := GetBuf(tt.size) + if len(buf) != tt.wantLength { + t.Errorf("Got buf length %v, want %v", len(buf), tt.wantLength) + } + capacityBefore := cap(buf) + PutBuf(buf) + buf2 := GetBuf(tt.size) + if cap(buf2) != capacityBefore { + t.Errorf("Buffer capacity changed after put/get, got %v, want %v", cap(buf2), capacityBefore) + } + }) + } +} + +func TestJoin(t *testing.T) { + c1 := &MockReadWriteCloser{Buffer: bytes.NewBuffer(make([]byte, 1024))} + c2 := &MockReadWriteCloser{Buffer: bytes.NewBuffer(make([]byte, 1024))} + + inCount, outCount := Join(c1, c2) + if inCount == 0 || outCount == 0 { + t.Fatalf("Transfer failed, got inCount = %d, outCount = %d", inCount, outCount) + } + + t.Logf("c1 = %v, c2 = %v\n", inCount, outCount) +} + +type MockReadWriteCloser struct { + *bytes.Buffer +} + +func (m *MockReadWriteCloser) Close() error { + return nil +} diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 9646185..74477c4 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -1,6 +1,6 @@ package version -var version = "0.1.1" +var version = "0.1.2" func Full() string { return version