diff --git a/cmd/q3rcon-proxy/main.go b/cmd/q3rcon-proxy/main.go index f5c4388..56a8b55 100644 --- a/cmd/q3rcon-proxy/main.go +++ b/cmd/q3rcon-proxy/main.go @@ -1,75 +1,105 @@ package main import ( + "context" "fmt" "os" "strings" "time" - log "github.com/sirupsen/logrus" - "github.com/onyx-and-iris/q3rcon-proxy/pkg/udpproxy" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v3" ) -func main() { - loglevel := os.Getenv("Q3RCON_LOGLEVEL") - if loglevel == "" { - loglevel = "info" - } - level, err := log.ParseLevel(loglevel) - if err != nil { - fmt.Fprintf(os.Stderr, "Invalid log level: %s\n", loglevel) - os.Exit(1) - } - log.SetLevel(level) - - proxyHost := os.Getenv("Q3RCON_PROXY_HOST") - if proxyHost == "" { - proxyHost = "0.0.0.0" - } - - targetHost := os.Getenv("Q3RCON_TARGET_HOST") - if targetHost == "" { - targetHost = "127.0.0.1" - } - - proxies := os.Getenv("Q3RCON_TARGET_PORTS") - if proxies == "" { - log.Fatal("env Q3RCON_TARGET_PORTS required") - } - - sessionTimeout, err := getEnvInt("Q3RCON_SESSION_TIMEOUT") - if err != nil { - log.Fatalf("unable to parse Q3RCON_SESSION_TIMEOUT: %s", err.Error()) - } - if sessionTimeout == 0 { - sessionTimeout = 20 - } - - for _, proxy := range strings.Split(proxies, ";") { - go start(proxyHost, targetHost, proxy, sessionTimeout) - } - - <-make(chan struct{}) +type proxyConfig struct { + proxyHost string + targetHost string + portsMapping []string + sessionTimeout int } -func start(proxyHost, targetHost, ports string, sessionTimeout int) { - proxyPort, targetPort := func() (string, string) { - x := strings.Split(ports, ":") - return x[0], x[1] - }() +func main() { + cmd := &cli.Command{ + Name: "q3rcon-proxy", + Usage: "A Quake 3 RCON proxy server", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "proxy-host", + Value: "0.0.0.0", + Usage: "Proxy host address", + Sources: cli.EnvVars("Q3RCON_PROXY_HOST"), + }, + &cli.StringFlag{ + Name: "target-host", + Value: "127.0.0.1", + Usage: "Target host address", + Sources: cli.EnvVars("Q3RCON_TARGET_HOST"), + }, + &cli.StringFlag{ + Name: "ports-mapping", + Usage: "Proxy and target ports (proxy:target)", + Sources: cli.EnvVars("Q3RCON_PORTS_MAPPING"), + Required: true, + }, + &cli.IntFlag{ + Name: "session-timeout", + Value: 20, + Usage: "Session timeout in minutes", + Sources: cli.EnvVars("Q3RCON_SESSION_TIMEOUT"), + }, + &cli.StringFlag{ + Name: "loglevel", + Value: "info", + Usage: "Log level (trace, debug, info, warn, error, fatal, panic)", + Sources: cli.EnvVars("Q3RCON_LOGLEVEL"), + }, + }, + Action: func(_ context.Context, cmd *cli.Command) error { + errChan := make(chan error) - hostAddr := fmt.Sprintf("%s:%s", proxyHost, proxyPort) - proxyAddr := fmt.Sprintf("%s:%s", targetHost, targetPort) + for mapping := range strings.SplitSeq(cmd.String("ports-mapping"), ";") { + cfg := proxyConfig{ + proxyHost: cmd.String("proxy-host"), + targetHost: cmd.String("target-host"), + portsMapping: strings.Split(mapping, ":"), + sessionTimeout: cmd.Int("session-timeout"), + } + + go initProxy(cfg, errChan) + } + + // We don't expect to receive any errors from the channels, but if we do, we log and return early. + for err := range errChan { + if err != nil { + log.Errorf("Error: %v", err) + return err + } + } + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} + +func initProxy(cfg proxyConfig, errChan chan error) { + proxyPort, targetPort := cfg.portsMapping[0], cfg.portsMapping[1] + + hostAddr := fmt.Sprintf("%s:%s", cfg.proxyHost, proxyPort) + proxyAddr := fmt.Sprintf("%s:%s", cfg.targetHost, targetPort) c, err := udpproxy.New( hostAddr, proxyAddr, - udpproxy.WithSessionTimeout(time.Duration(sessionTimeout)*time.Minute)) + udpproxy.WithSessionTimeout(time.Duration(cfg.sessionTimeout)*time.Minute)) if err != nil { - log.Fatal(err) + errChan <- fmt.Errorf("failed to create proxy: %w", err) + return } log.Printf("q3rcon-proxy initialized: [proxy] (%s) [target] (%s)", hostAddr, proxyAddr) - log.Fatal(c.ListenAndServe()) + errChan <- c.ListenAndServe() } diff --git a/go.mod b/go.mod index f718b72..1534161 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.24.0 toolchain go1.24.1 -require github.com/sirupsen/logrus v1.9.3 +require ( + github.com/sirupsen/logrus v1.9.3 + github.com/urfave/cli/v3 v3.3.2 +) require golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index 090ed2b..6a6f38e 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,15 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= +github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=