From 33290aa5c79392048faf365ef5ae0ded6dca6e62 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 22 Nov 2023 22:21:11 +0300 Subject: [PATCH] web api, orphaned connections detector, null driver -> nil driver, refactor root command --- cmd/root.go | 112 ++++++++++++------ examples/config.yml | 4 +- go.mod | 16 +++ go.sum | 55 +++++++++ internal/api/{ => plugin}/server.go | 27 ++--- internal/api/{ => plugin}/stream.go | 2 +- internal/api/web/handler/config.go | 12 ++ internal/api/web/handler/status.go | 12 ++ internal/api/web/middleware/auth.go | 31 +++++ internal/api/web/middleware/log.go | 38 ++++++ internal/api/web/middleware/log/adapter.go | 37 ++++++ internal/api/web/server.go | 57 +++++++++ internal/config/model.go | 38 +++--- internal/docker/api.go | 4 +- internal/docker/convert.go | 14 ++- internal/server/driver/construct.go | 6 +- .../server/driver/{null => nil}/driver.go | 20 ++-- internal/server/manager.go | 99 +++++++++++++--- pkg/dto/models.go | 2 +- 19 files changed, 480 insertions(+), 106 deletions(-) rename internal/api/{ => plugin}/server.go (65%) rename internal/api/{ => plugin}/stream.go (95%) create mode 100644 internal/api/web/handler/config.go create mode 100644 internal/api/web/handler/status.go create mode 100644 internal/api/web/middleware/auth.go create mode 100644 internal/api/web/middleware/log.go create mode 100644 internal/api/web/middleware/log/adapter.go create mode 100644 internal/api/web/server.go rename internal/server/driver/{null => nil}/driver.go (60%) diff --git a/cmd/root.go b/cmd/root.go index 96a16b0..79cfd5e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,12 +6,14 @@ import ( "os/signal" "syscall" - "github.com/Neur0toxine/sshpoke/internal/api" + "github.com/Neur0toxine/sshpoke/internal/api/plugin" + "github.com/Neur0toxine/sshpoke/internal/api/web" "github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/docker" "github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/server" "github.com/Neur0toxine/sshpoke/pkg/dto" + plugin2 "github.com/Neur0toxine/sshpoke/pkg/plugin" "github.com/go-playground/validator/v10" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -28,53 +30,23 @@ var rootCmd = &cobra.Command{ Short: "Expose your Docker services to the Internet via SSH.", Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`, Run: func(cmd *cobra.Command, args []string) { - go api.StartPluginAPI() - var err error ctx, cancel := context.WithCancel(context.Background()) server.DefaultManager = server.NewManager(ctx, config.Default.Servers, config.Default.DefaultServer) - docker.Default, err = docker.New(ctx) - if err != nil { - logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err) - } - - for id, item := range docker.Default.Containers() { - err := server.DefaultManager.ProcessEvent(dto.Event{ - Type: dto.EventStart, - Container: item, - }) - if err != nil { - logger.Sugar.Errorw("cannot expose container", "id", id, "error", err) - } - } - - events, err := docker.Default.Listen() - if err != nil { - logger.Sugar.Fatalf("cannot listen to docker events: %s", err) - } - - go func() { - logger.Sugar.Debug("listening for docker events...") - for event := range events { - err := server.DefaultManager.ProcessEvent(event) - if err != nil { - logger.Sugar.Errorw("cannot expose container", - "id", event.Container.ID, "error", err) - } - } - }() + runPluginServer() + runWebServer() + runDockerEventListener(ctx) + shutdown := makeShutdownFunc(cancel) linuxSig := make(chan os.Signal, 1) signal.Notify(linuxSig) for sig := range linuxSig { switch sig { case os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM: - cancel() - server.DefaultManager.WaitForShutdown() - logger.Sugar.Infof("received %s, exiting...", sig) - os.Exit(0) + shutdown(sig) default: } } + shutdown(syscall.SIGHUP) }, } @@ -116,6 +88,72 @@ func initConfig() { if err := validator.New().Struct(config.Default); err != nil { log.Fatalf("invalid configuration: %s", err) } + if config.Default.API.Web.Port == 0 { + config.Default.API.Web.Port = web.DefaultPort + } + if config.Default.API.Plugin.Port == 0 { + config.Default.API.Plugin.Port = plugin2.DefaultPort + } logger.Initialize() logger.Sugar.Debugw("configuration loaded", "config", config.Default) } + +func runPluginServer() { + port := config.Default.API.Plugin.Port + if port == 0 { + port = plugin2.DefaultPort + } + go plugin.StartServer(port, logger.Sugar.With("component", "pluginServer")) +} + +func runWebServer() { + port := config.Default.API.Web.Port + if port == 0 { + port = web.DefaultPort + } + go web.StartServer( + config.Default.API.Web.Token, port, logger.Sugar.With("component", "webServer"), config.Default.Debug) +} + +func runDockerEventListener(ctx context.Context) { + var err error + docker.Default, err = docker.New(ctx) + if err != nil { + logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err) + } + + for id, item := range docker.Default.Containers() { + err := server.DefaultManager.ProcessEvent(dto.Event{ + Type: dto.EventStart, + Container: item, + }) + if err != nil { + logger.Sugar.Errorw("cannot expose container", "id", id, "error", err) + } + } + + events, err := docker.Default.Listen() + if err != nil { + logger.Sugar.Fatalf("cannot listen to docker events: %s", err) + } + + go func() { + logger.Sugar.Debug("listening for docker events...") + for event := range events { + err := server.DefaultManager.ProcessEvent(event) + if err != nil { + logger.Sugar.Errorw("cannot expose container", + "id", event.Container.ID, "error", err) + } + } + }() +} + +func makeShutdownFunc(cancel func()) func(os.Signal) { + return func(sig os.Signal) { + cancel() + server.DefaultManager.WaitForShutdown() + logger.Sugar.Infof("received %s, exiting...", sig) + os.Exit(0) + } +} diff --git a/examples/config.yml b/examples/config.yml index 79ab758..5d0e20c 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -153,5 +153,5 @@ servers: # This token will be used by plugin while connecting to gRPC API. token: key - name: noop - # Null driver doesn't do anything. This driver will automatically be used for servers with invalid 'driver' value. - driver: null + # Nil driver doesn't do anything. This driver will automatically be used for servers with invalid 'driver' value. + driver: nil diff --git a/go.mod b/go.mod index c76879d..b5a5851 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,13 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 github.com/function61/gokit v0.0.0-20231117065306-355fe206d542 + github.com/gin-contrib/secure v0.0.1 + github.com/gin-gonic/gin v1.9.1 github.com/go-playground/validator/v10 v10.16.0 github.com/jonstacks/iomerge v0.0.0-20200607001240-c9a527e8abe8 github.com/kevinburke/ssh_config v1.2.0 github.com/mitchellh/mapstructure v1.5.0 + github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644 github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 @@ -24,33 +27,46 @@ require ( require ( github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/changkun/lockfree v0.0.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rs/cors v1.8.1 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.17.0 // indirect diff --git a/go.sum b/go.sum index bbeb8ef..adc186c 100644 --- a/go.sum +++ b/go.sum @@ -42,9 +42,15 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/changkun/lockfree v0.0.1 h1:5WefVJLglY4IHRqOQmh6Ao6wkJYaJkarshKU8VUtId4= github.com/changkun/lockfree v0.0.1/go.mod h1:3bKiaXn/iNzIPlSvSOMSVbRQUQtAp8qUAyBUtzU11s4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -81,17 +87,28 @@ github.com/function61/gokit v0.0.0-20231117065306-355fe206d542 h1:a9BTN+DOboRkVi github.com/function61/gokit v0.0.0-20231117065306-355fe206d542/go.mod h1:sJY957+7ush4oj4ElOMhUFaFIriAFNAGYzVh2tFJNy0= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/secure v0.0.1 h1:DMMx3xXDY+MLA9kzIPHksyzC5/V5J6014c/WAmdS2gQ= +github.com/gin-contrib/secure v0.0.1/go.mod h1:6kseOBFrSR3Is/kM1jDhCg/WsXAMvKJkuPvG9dGph/c= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -136,6 +153,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -164,12 +182,18 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonstacks/iomerge v0.0.0-20200607001240-c9a527e8abe8 h1:avdze4CXO+1TsCV84EH7ueX5WOc0GDjDYCyQWlC51Lo= github.com/jonstacks/iomerge v0.0.0-20200607001240-c9a527e8abe8/go.mod h1:D+xdhbGYvTi/6hHTULOhUiYwEM89FvmRfPKEms6MJsc= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -178,14 +202,24 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -204,6 +238,10 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.8.1 h1:OrP+y5H+5Md29ACTA9imbALaKHwOSUZkcizaG0LT5ow= +github.com/rs/cors v1.8.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644 h1:BBwREPixt0iE77C9z7DOenoeh5OGFrzyL1cWOp5oQTs= +github.com/rs/cors/wrapper/gin v0.0.0-20231013084403-73f81b45a644/go.mod h1:gmu40DuK3SLdKUzGOUofS3UDZwyeOUy6ZjPPuaALatw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= @@ -224,17 +262,25 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -253,6 +299,9 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.design/x/lockfree v0.0.1 h1:IHFNwZgM5bnZYWkEbzn5lWHMYr8WsRBdCJ/RBVY0xMM= golang.design/x/lockfree v0.0.1/go.mod h1:iaZUx6UgZaOdePjzI6wFd+seYMl1i0rsG8+xKvA8c4I= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -363,6 +412,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -389,7 +439,9 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -566,6 +618,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -582,5 +636,6 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/api/server.go b/internal/api/plugin/server.go similarity index 65% rename from internal/api/server.go rename to internal/api/plugin/server.go index 2aa009d..00b6e40 100644 --- a/internal/api/server.go +++ b/internal/api/plugin/server.go @@ -1,4 +1,4 @@ -package api +package plugin import ( "context" @@ -6,13 +6,11 @@ import ( "fmt" "net" - "github.com/Neur0toxine/sshpoke/internal/config" - "github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/server" "github.com/Neur0toxine/sshpoke/internal/server/driver/plugin" "github.com/Neur0toxine/sshpoke/pkg/convert" - plugin2 "github.com/Neur0toxine/sshpoke/pkg/plugin" "github.com/Neur0toxine/sshpoke/pkg/plugin/pb" + "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" @@ -22,6 +20,7 @@ var ErrUnauthorized = errors.New("unauthorized") type pluginAPI struct { pb.UnimplementedPluginServiceServer + log *zap.SugaredLogger } func (p *pluginAPI) Event(_ *emptypb.Empty, stream pb.PluginService_EventServer) error { @@ -29,13 +28,13 @@ func (p *pluginAPI) Event(_ *emptypb.Empty, stream pb.PluginService_EventServer) if pl == nil { return ErrUnauthorized } - logger.Sugar.Debugw("attached plugin event stream", "serverName", pl.Name()) + p.log.Debugw("attached plugin event stream", "serverName", pl.Name()) err := pl.Listen(stream.Context(), &Stream{stream: stream}) if err != nil { - logger.Sugar.Debugw("detached plugin event stream", "serverName", pl.Name(), "error", err) + p.log.Debugw("detached plugin event stream", "serverName", pl.Name(), "error", err) return err } - logger.Sugar.Debugw("detached plugin event stream", "serverName", pl.Name()) + p.log.Debugw("detached plugin event stream", "serverName", pl.Name()) return nil } @@ -60,20 +59,16 @@ func (p *pluginAPI) receiverForContext(ctx context.Context) plugin.Plugin { return server.DefaultManager.PluginByToken(tokens[0]) } -func StartPluginAPI() { - port := config.Default.API.Plugin.Port - if port == 0 { - port = plugin2.DefaultPort - } +func StartServer(port int, log *zap.SugaredLogger) { socket, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { - logger.Sugar.Errorf("cannot start plugin API server on port %d: %s", port, err) + log.Errorf("cannot start plugin API server on port %d: %s", port, err) return } s := grpc.NewServer() - pb.RegisterPluginServiceServer(s, &pluginAPI{}) - logger.Sugar.Debugf("starting plugin server on :%d", port) + pb.RegisterPluginServiceServer(s, &pluginAPI{log: log}) + log.Debugf("starting plugin server on :%d", port) if err := s.Serve(socket); err != nil { - logger.Sugar.Fatalf("cannot start plugin server on :%d: %s", port, err) + log.Fatalf("cannot start plugin server on :%d: %s", port, err) } } diff --git a/internal/api/stream.go b/internal/api/plugin/stream.go similarity index 95% rename from internal/api/stream.go rename to internal/api/plugin/stream.go index 249a6c6..4703e00 100644 --- a/internal/api/stream.go +++ b/internal/api/plugin/stream.go @@ -1,4 +1,4 @@ -package api +package plugin import ( "github.com/Neur0toxine/sshpoke/pkg/convert" diff --git a/internal/api/web/handler/config.go b/internal/api/web/handler/config.go new file mode 100644 index 0000000..5d4c875 --- /dev/null +++ b/internal/api/web/handler/config.go @@ -0,0 +1,12 @@ +package handler + +import ( + "net/http" + + "github.com/Neur0toxine/sshpoke/internal/config" + "github.com/gin-gonic/gin" +) + +func Config(c *gin.Context) { + c.JSON(http.StatusOK, config.Default) +} diff --git a/internal/api/web/handler/status.go b/internal/api/web/handler/status.go new file mode 100644 index 0000000..61dcb75 --- /dev/null +++ b/internal/api/web/handler/status.go @@ -0,0 +1,12 @@ +package handler + +import ( + "net/http" + + "github.com/Neur0toxine/sshpoke/internal/server" + "github.com/gin-gonic/gin" +) + +func Status(c *gin.Context) { + c.JSON(http.StatusOK, server.DefaultManager.StatusMap()) +} diff --git a/internal/api/web/middleware/auth.go b/internal/api/web/middleware/auth.go new file mode 100644 index 0000000..16461cc --- /dev/null +++ b/internal/api/web/middleware/auth.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +const ( + AuthHeader = "Authorization" + bearerPrefix = "Bearer " +) + +func Auth(token string) gin.HandlerFunc { + if token == "" { + return func(c *gin.Context) {} + } + return func(c *gin.Context) { + header := c.GetHeader(AuthHeader) + if strings.HasPrefix(header, bearerPrefix) { + header = header[len(bearerPrefix):] + } + + if header != token { + c.AbortWithStatus(http.StatusUnauthorized) + } + + c.Next() + } +} diff --git a/internal/api/web/middleware/log.go b/internal/api/web/middleware/log.go new file mode 100644 index 0000000..d03544d --- /dev/null +++ b/internal/api/web/middleware/log.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "fmt" + "net/http" + "time" + + "github.com/Neur0toxine/sshpoke/internal/api/web/middleware/log" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +func SetupLogger(zapLog *zap.SugaredLogger) gin.HandlerFunc { + gin.DefaultWriter = log.AsWriter(zapLog, zap.DebugLevel) + gin.DefaultErrorWriter = log.AsWriter(zapLog, zap.ErrorLevel) + gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { + zapLog.Debugw("route", + "method", httpMethod, + "path", absolutePath, + "handler", handlerName, + "handlerNum", nuHandlers) + } + + return func(c *gin.Context) { + start := time.Now() + c.Next() + now := time.Now() + zapLog.Debugw(fmt.Sprintf("%d %s | %s %s", + c.Writer.Status(), http.StatusText(c.Writer.Status()), c.Request.Method, c.Request.URL.String()), + "ts", now, + "latency", now.Sub(start), + "clientIp", c.ClientIP(), + "method", c.Request.Method, + "status", c.Writer.Status(), + "error", c.Errors.ByType(gin.ErrorTypePrivate).String(), + "bodyLength", c.Writer.Size()) + } +} diff --git a/internal/api/web/middleware/log/adapter.go b/internal/api/web/middleware/log/adapter.go new file mode 100644 index 0000000..31e6313 --- /dev/null +++ b/internal/api/web/middleware/log/adapter.go @@ -0,0 +1,37 @@ +package log + +import ( + "io" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type writerLogger struct { + log *zap.SugaredLogger + level zapcore.Level +} + +func AsWriter(log *zap.SugaredLogger, level zapcore.Level) io.Writer { + return &writerLogger{log: log} +} + +func (w *writerLogger) Write(p []byte) (n int, err error) { + switch w.level { + case zapcore.DebugLevel: + w.log.Debug(string(p)) + case zapcore.InfoLevel: + w.log.Info(string(p)) + case zapcore.WarnLevel: + w.log.Warn(string(p)) + case zapcore.ErrorLevel: + w.log.Error(string(p)) + case zapcore.DPanicLevel: + w.log.DPanic(string(p)) + case zapcore.PanicLevel: + w.log.Panic(string(p)) + case zapcore.FatalLevel: + w.log.Fatal(string(p)) + } + return len(p), nil +} diff --git a/internal/api/web/server.go b/internal/api/web/server.go new file mode 100644 index 0000000..dfa8c8a --- /dev/null +++ b/internal/api/web/server.go @@ -0,0 +1,57 @@ +package web + +import ( + "net" + "strconv" + + "github.com/Neur0toxine/sshpoke/internal/api/web/handler" + "github.com/Neur0toxine/sshpoke/internal/api/web/middleware" + "github.com/Neur0toxine/sshpoke/internal/logger" + "github.com/gin-contrib/secure" + "github.com/gin-gonic/gin" + cors "github.com/rs/cors/wrapper/gin" + "go.uber.org/zap" +) + +const ( + DefaultPort = 25680 + httpProto = "http://" +) + +func StartServer(token string, port int, log *zap.SugaredLogger, debug bool) { + if !debug { + gin.SetMode(gin.ReleaseMode) + } + logMiddleware := middleware.SetupLogger(log) + g := gin.New() + g.Use(logMiddleware) + g.Use(cors.New(cors.Options{ + AllowedOrigins: []string{ + httpProto + net.JoinHostPort("localhost", strconv.Itoa(port)), + httpProto + net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), + }, + AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "DELETE"}, + MaxAge: 60 * 10, + AllowCredentials: true, + })) + g.Use(secure.New(secureConfig())) + router(g, token) + err := g.Run(net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + logger.Sugar.Errorf("cannot start Web API server on port %d: %s", port, err) + } +} +func router(g *gin.Engine, token string) { + api := g.Group("/api/v1") + { + api.Use(middleware.Auth(token)) + api.GET("/config", handler.Config) + api.GET("/status", handler.Status) + } +} + +func secureConfig() secure.Config { + cfg := secure.DefaultConfig() + cfg.SSLRedirect = false + return cfg +} diff --git a/internal/config/model.go b/internal/config/model.go index de4f144..a1acc6c 100644 --- a/internal/config/model.go +++ b/internal/config/model.go @@ -11,33 +11,33 @@ import ( var Default Config type Config struct { - Debug bool `mapstructure:"debug"` - API API `mapstructure:"api"` - Docker DockerConfig `mapstructure:"docker"` - DefaultServer string `mapstructure:"default_server"` - Servers []Server `mapstructure:"servers"` + Debug bool `mapstructure:"debug" json:"debug"` + API API `mapstructure:"api" json:"api"` + Docker DockerConfig `mapstructure:"docker" json:"docker"` + DefaultServer string `mapstructure:"default_server" json:"default_server"` + Servers []Server `mapstructure:"servers" json:"servers"` } type API struct { - Web WebAPI `mapstructure:"web"` - Plugin PluginAPI `mapstructure:"plugin"` + Web WebAPI `mapstructure:"web" json:"web"` + Plugin PluginAPI `mapstructure:"plugin" json:"plugin"` } type WebAPI struct { - Port int `mapstructure:"port" validate:"gte=0,lte=65535"` - Token string `mapstructure:"token"` + Port int `mapstructure:"port" json:"port" validate:"gte=0,lte=65535"` + Token string `mapstructure:"token" json:"token"` } type PluginAPI struct { - Port int `mapstructure:"port" validate:"gte=0,lte=65535"` + Port int `mapstructure:"port" json:"port" validate:"gte=0,lte=65535"` } type DockerConfig struct { - FromEnv *bool `mapstructure:"from_env,omitempty"` - CertPath string `mapstructure:"cert_path"` - TLSVerify *bool `mapstructure:"tls_verify,omitempty"` - Host string `mapstructure:"host"` - Version string `mapstructure:"version"` + FromEnv *bool `mapstructure:"from_env,omitempty" json:"from_env"` + CertPath string `mapstructure:"cert_path" json:"cert_path,omitempty"` + TLSVerify *bool `mapstructure:"tls_verify,omitempty" json:"tls_verify,omitempty"` + Host string `mapstructure:"host" json:"host,omitempty"` + Version string `mapstructure:"version" json:"version,omitempty"` } type DriverParams map[string]interface{} @@ -47,13 +47,13 @@ type DriverType string const ( DriverSSH DriverType = "ssh" DriverPlugin DriverType = "plugin" - DriverNull DriverType = "null" + DriverNil DriverType = "nil" ) type Server struct { - Name string `mapstructure:"name" validate:"required"` - Driver DriverType `mapstructure:"driver"` - Params DriverParams `mapstructure:"params"` + Name string `mapstructure:"name" json:"name" validate:"required"` + Driver DriverType `mapstructure:"driver" json:"driver"` + Params DriverParams `mapstructure:"params" json:"params,omitempty"` } func (d DockerConfig) Opts(c *client.Client) error { diff --git a/internal/docker/api.go b/internal/docker/api.go index 8ce2fb7..9eaea31 100644 --- a/internal/docker/api.go +++ b/internal/docker/api.go @@ -50,10 +50,10 @@ func (d *Docker) Containers() map[string]dto.Container { return containers } -func (d *Docker) GetContainer(id string) (dto.Container, bool) { +func (d *Docker) GetContainer(id string, all bool) (dto.Container, bool) { container, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{ Filters: filters.NewArgs(filters.Arg("id", id)), - All: true, + All: all, }) if err != nil || len(container) != 1 { return dto.Container{}, false diff --git a/internal/docker/convert.go b/internal/docker/convert.go index baa301a..1d9ddea 100644 --- a/internal/docker/convert.go +++ b/internal/docker/convert.go @@ -3,6 +3,7 @@ package docker import ( "net" "strconv" + "strings" "github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/pkg/dto" @@ -78,7 +79,7 @@ func dockerContainerToInternal(container types.Container) (result dto.Container, return dto.Container{ ID: container.ID, - Names: container.Names, + Names: convertNames(container.Names), IP: ip, Port: uint16(port), Server: labels.Server, @@ -86,6 +87,17 @@ func dockerContainerToInternal(container types.Container) (result dto.Container, }, true } +func convertNames(src []string) []string { + if len(src) == 0 { + return nil + } + dst := make([]string, len(src)) + for i := 0; i < len(src); i++ { + dst[i] = strings.TrimLeft(src[i], "/") + } + return dst +} + func getKeyVal(m map[string]*network.EndpointSettings) *network.EndpointSettings { for _, v := range m { return v diff --git a/internal/server/driver/construct.go b/internal/server/driver/construct.go index 69490f0..d8ffe79 100644 --- a/internal/server/driver/construct.go +++ b/internal/server/driver/construct.go @@ -5,7 +5,7 @@ import ( "github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/server/driver/base" - "github.com/Neur0toxine/sshpoke/internal/server/driver/null" + "github.com/Neur0toxine/sshpoke/internal/server/driver/nil" "github.com/Neur0toxine/sshpoke/internal/server/driver/plugin" "github.com/Neur0toxine/sshpoke/internal/server/driver/ssh" ) @@ -16,9 +16,9 @@ func New(ctx context.Context, name string, driver config.DriverType, params conf return ssh.New(ctx, name, params) case config.DriverPlugin: return plugin.New(ctx, name, params) - case config.DriverNull: + case config.DriverNil: fallthrough default: - return null.New(ctx, name, params) + return nil.New(ctx, name, params) } } diff --git a/internal/server/driver/null/driver.go b/internal/server/driver/nil/driver.go similarity index 60% rename from internal/server/driver/null/driver.go rename to internal/server/driver/nil/driver.go index 3250a70..121fbe7 100644 --- a/internal/server/driver/null/driver.go +++ b/internal/server/driver/nil/driver.go @@ -1,4 +1,4 @@ -package null +package nil import ( "context" @@ -8,26 +8,26 @@ import ( "github.com/Neur0toxine/sshpoke/pkg/dto" ) -// Null driver only logs container events to debug log. It is used when user provides invalid driver type. +// Nil driver only logs container events to debug log. It is used when user provides invalid driver type. // You can use it directly, but it won't do anything, so... why bother? -type Null struct { +type Nil struct { base.Base } func New(ctx context.Context, name string, params config.DriverParams) (base.Driver, error) { - return &Null{ + return &Nil{ Base: base.New(ctx, name), }, nil } -func (d *Null) Handle(event dto.Event) error { - d.Log().Debugw("handling event with null driver", "serverName", d.Name(), "event", event) +func (d *Nil) Handle(event dto.Event) error { + d.Log().Debugw("handling event with nil driver", "serverName", d.Name(), "event", event) switch event.Type { case dto.EventStart: d.PushEventStatus(dto.EventStatus{ Type: dto.EventStart, ID: event.Container.ID, - Domain: "https://" + event.Container.ID + "null.dev", + Domain: "https://" + event.Container.ID + ".nil.dev", }) case dto.EventStop, dto.EventShutdown: d.PushEventStatus(dto.EventStatus{ @@ -38,8 +38,8 @@ func (d *Null) Handle(event dto.Event) error { return nil } -func (d *Null) Driver() config.DriverType { - return config.DriverNull +func (d *Nil) Driver() config.DriverType { + return config.DriverNil } -func (d *Null) WaitForShutdown() {} +func (d *Nil) WaitForShutdown() {} diff --git a/internal/server/manager.go b/internal/server/manager.go index f00ad00..367f6b4 100644 --- a/internal/server/manager.go +++ b/internal/server/manager.go @@ -3,7 +3,9 @@ package server import ( "context" "errors" + "strings" "sync" + "time" "github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/docker" @@ -16,14 +18,17 @@ import ( type Manager struct { rw sync.RWMutex + forwardsLock sync.Mutex servers map[string]base.Driver plugins map[string]plugin.Plugin - statusMap map[string]serverStatus + statusMap map[string]ServerStatus + forwards map[string]bool statusLock sync.RWMutex + ctx context.Context defaultServer string } -type serverStatus struct { +type ServerStatus struct { Name string `json:"name"` Connections Connections `json:"connections"` } @@ -36,8 +41,11 @@ var ( func NewManager(ctx context.Context, servers []config.Server, defaultServer string) *Manager { m := &Manager{ + ctx: ctx, servers: make(map[string]base.Driver), plugins: make(map[string]plugin.Plugin), + statusMap: make(map[string]ServerStatus), + forwards: make(map[string]bool), defaultServer: defaultServer, } for _, serverConfig := range servers { @@ -61,7 +69,9 @@ func NewManager(ctx context.Context, servers []config.Server, defaultServer stri m.plugins[pl.Token()] = pl } m.servers[serverConfig.Name] = server + m.statusMap[serverConfig.Name] = ServerStatus{Name: serverConfig.Name, Connections: make(Connections)} } + go m.runMarkAndSweepForwards() return m } @@ -79,7 +89,22 @@ func (m *Manager) ProcessEvent(event dto.Event) error { if !ok { return ErrNoSuchServer } - return srv.Handle(event) + if err := srv.Handle(event); err != nil { + return err + } + defer m.forwardsLock.Unlock() + m.forwardsLock.Lock() + switch event.Type { + case dto.EventStart: + m.forwards[m.forwardID(serverName, event.Container.ID)] = false + case dto.EventStop, dto.EventError, dto.EventShutdown: + delete(m.forwards, m.forwardID(serverName, event.Container.ID)) + } + return nil +} + +func (m *Manager) forwardID(serverName, containerID string) string { + return serverName + ":" + containerID } func (m *Manager) eventStatusCallback(serverName string) base.EventStatusCallback { @@ -91,28 +116,74 @@ func (m *Manager) eventStatusCallback(serverName string) base.EventStatusCallbac func (m *Manager) processEventStatus(serverName string, event dto.EventStatus) { logger.Sugar.Debugw("received EventStatus from server", "serverName", serverName, "eventStatus", event) - m.statusLock.RLock() - _, exists := m.statusMap[serverName] - if !exists { - return - } - m.statusLock.RUnlock() - defer m.statusLock.Unlock() - m.statusLock.Lock() - item, found := docker.Default.GetContainer(event.ID) + item, found := docker.Default.GetContainer(event.ID, true) if !found { return } + defer m.forwardsLock.Unlock() + m.forwardsLock.Lock() switch event.Type { case dto.EventStart: + defer m.statusLock.Unlock() + m.statusLock.Lock() item.Domain = event.Domain + m.forwards[m.forwardID(serverName, item.ID)] = false + m.statusMap[serverName].Connections[item.ID] = item case dto.EventStop, dto.EventShutdown, dto.EventError: + defer m.statusLock.Unlock() + m.statusLock.Lock() item.Domain = "" - + delete(m.forwards, m.forwardID(serverName, item.ID)) + delete(m.statusMap[serverName].Connections, item.ID) default: return } - m.statusMap[serverName].Connections[item.ID] = item +} + +func (m *Manager) StatusMap() map[string]ServerStatus { + defer m.statusLock.RUnlock() + m.statusLock.RLock() + return m.statusMap +} + +// runMarkAndSweepForwards runs mark-and-sweep on the started forwards every 10 seconds. +// This job is necessary because Docker sometimes forgets to notify us that containers +// were stopped (usually happens when spamming Ctrl+C after `docker compose run`). +func (m *Manager) runMarkAndSweepForwards() { + ticker := time.NewTicker(time.Second * 10) + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + m.markAndSweepForwards() + } + } +} + +// markAndSweepForwards marks stopped containers for removal on the first run and removes them from forwards later. +// This job will remove containers from forwards if Docker didn't notify us about stopping containers for some reason. +func (m *Manager) markAndSweepForwards() { + defer m.forwardsLock.Unlock() + m.forwardsLock.Lock() + + for id, state := range m.forwards { + forwardIDs := strings.Split(id, ":") + serverName, containerID := forwardIDs[0], forwardIDs[1] + _, found := docker.Default.GetContainer(containerID, false) + if found { + m.forwards[id] = false // unmark + } else { + if state { + m.processEventStatus(serverName, dto.EventStatus{ + Type: dto.EventStop, + ID: containerID, + }) + continue + } + m.forwards[id] = true // mark + } + } } func (m *Manager) PluginByToken(token string) plugin.Plugin { diff --git a/pkg/dto/models.go b/pkg/dto/models.go index 4456dfb..156084f 100644 --- a/pkg/dto/models.go +++ b/pkg/dto/models.go @@ -57,6 +57,6 @@ type Container struct { IP net.IP `json:"ip"` Port uint16 `json:"port"` Server string `json:"-"` - RemoteHost string `json:"remote_host"` + RemoteHost string `json:"remote_host,omitempty"` Domain string `json:"domain"` }