From be92c5a569d08c68b920e3591339e8f7b2262525 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 12:11:08 -0500 Subject: [PATCH 01/45] chore: Slims up shell.nix. --- shell.nix | 6 ------ 1 file changed, 6 deletions(-) diff --git a/shell.nix b/shell.nix index 7078f6e..e8f1a3b 100644 --- a/shell.nix +++ b/shell.nix @@ -4,17 +4,11 @@ buildInputs = with pkgs; [ # api/ nodejs - openssl - lsof go gopls ]; shellHook = '' - export PRISMA_QUERY_ENGINE_BINARY=${pkgs.prisma-engines}/bin/query-engine - export PRISMA_QUERY_ENGINE_LIBRARY=${pkgs.prisma-engines}/lib/libquery_engine.node - export PRISMA_SCHEMA_ENGINE_BINARY=${pkgs.prisma-engines}/bin/schema-engine - source init.sh ''; } From 201007f7a03f06256b826de9de1bd1c8441ad9f2 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 15:26:48 -0500 Subject: [PATCH 02/45] chore: Make lookup fields in the API omit when they're empty. This makes the API more accurate to the previous API's responses. --- backend/api/controllers/v1/backends/lookup.go | 4 ++-- backend/api/controllers/v1/proxies/lookup.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/api/controllers/v1/backends/lookup.go b/backend/api/controllers/v1/backends/lookup.go index fda1a8e..a68e183 100644 --- a/backend/api/controllers/v1/backends/lookup.go +++ b/backend/api/controllers/v1/backends/lookup.go @@ -27,9 +27,9 @@ type SanitizedBackend struct { Name string `json:"name"` BackendID uint `json:"id"` OwnerID uint `json:"ownerID"` - Description *string `json:"description"` + Description *string `json:"description,omitempty"` Backend string `json:"backend"` - BackendParameters *string `json:"connectionDetails"` + BackendParameters *string `json:"connectionDetails,omitempty"` Logs []string `json:"logs"` } diff --git a/backend/api/controllers/v1/proxies/lookup.go b/backend/api/controllers/v1/proxies/lookup.go index c290e27..f370e79 100644 --- a/backend/api/controllers/v1/proxies/lookup.go +++ b/backend/api/controllers/v1/proxies/lookup.go @@ -29,7 +29,7 @@ type ProxyLookupRequest struct { type SanitizedProxy struct { Id uint `json:"id"` Name string `json:"name"` - Description *string `json:"description"` + Description *string `json:"description,omitempty"` Protcol string `json:"protocol"` SourceIP string `json:"sourceIP"` SourcePort uint16 `json:"sourcePort"` From 49db323e81962d13cf7e4313e9a500fa0285abd2 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 15:37:32 -0500 Subject: [PATCH 03/45] chore: Prepare for frontend support by moving the Go module files. This moves the Go module files to the root of the project and fixes all of the imports. --- backend/api/backendruntime/runtime.go | 4 ++-- backend/api/backup.go | 2 +- backend/api/controllers/v1/backends/create.go | 10 +++++----- backend/api/controllers/v1/backends/lookup.go | 8 ++++---- backend/api/controllers/v1/backends/remove.go | 8 ++++---- backend/api/controllers/v1/proxies/connections.go | 10 +++++----- backend/api/controllers/v1/proxies/create.go | 10 +++++----- backend/api/controllers/v1/proxies/lookup.go | 6 +++--- backend/api/controllers/v1/proxies/remove.go | 10 +++++----- backend/api/controllers/v1/proxies/start.go | 10 +++++----- backend/api/controllers/v1/proxies/stop.go | 10 +++++----- backend/api/controllers/v1/users/create.go | 6 +++--- backend/api/controllers/v1/users/login.go | 4 ++-- backend/api/controllers/v1/users/lookup.go | 6 +++--- backend/api/controllers/v1/users/refresh.go | 4 ++-- backend/api/controllers/v1/users/remove.go | 6 +++--- backend/api/jwtcore/jwt.go | 2 +- backend/api/main.go | 14 +++++++------- backend/api/permissions/permission_nodes.go | 2 +- backend/backendutil/application.go | 2 +- backend/backendutil/structure.go | 2 +- backend/dummybackend/main.go | 4 ++-- backend/externalbackendlauncher/main.go | 4 ++-- backend/sshbackend/main.go | 4 ++-- backend/go.mod => go.mod | 0 backend/go.sum => go.sum | 0 migration-entrypoint.sh | 2 +- 27 files changed, 75 insertions(+), 75 deletions(-) rename backend/go.mod => go.mod (100%) rename backend/go.sum => go.sum (100%) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index 0f871f8..df9ceb6 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -8,8 +8,8 @@ import ( "os/exec" "time" - "git.terah.dev/imterah/hermes/backendlauncher" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/backendlauncher" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" ) diff --git a/backend/api/backup.go b/backend/api/backup.go index 1d4ebd8..98b83ec 100644 --- a/backend/api/backup.go +++ b/backend/api/backup.go @@ -9,7 +9,7 @@ import ( "os" "strings" - "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/dbcore" "github.com/charmbracelet/log" "github.com/go-playground/validator/v10" "github.com/urfave/cli/v2" diff --git a/backend/api/controllers/v1/backends/create.go b/backend/api/controllers/v1/backends/create.go index 2650fb5..03c3509 100644 --- a/backend/api/controllers/v1/backends/create.go +++ b/backend/api/controllers/v1/backends/create.go @@ -6,11 +6,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/backends/lookup.go b/backend/api/controllers/v1/backends/lookup.go index a68e183..9819df7 100644 --- a/backend/api/controllers/v1/backends/lookup.go +++ b/backend/api/controllers/v1/backends/lookup.go @@ -6,10 +6,10 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/backends/remove.go b/backend/api/controllers/v1/backends/remove.go index e1ea599..f9dbd8c 100644 --- a/backend/api/controllers/v1/backends/remove.go +++ b/backend/api/controllers/v1/backends/remove.go @@ -4,10 +4,10 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/proxies/connections.go b/backend/api/controllers/v1/proxies/connections.go index 68e3639..9f1affa 100644 --- a/backend/api/controllers/v1/proxies/connections.go +++ b/backend/api/controllers/v1/proxies/connections.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/proxies/create.go b/backend/api/controllers/v1/proxies/create.go index 40cea76..1ac0bbc 100644 --- a/backend/api/controllers/v1/proxies/create.go +++ b/backend/api/controllers/v1/proxies/create.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/proxies/lookup.go b/backend/api/controllers/v1/proxies/lookup.go index f370e79..5f40698 100644 --- a/backend/api/controllers/v1/proxies/lookup.go +++ b/backend/api/controllers/v1/proxies/lookup.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/proxies/remove.go b/backend/api/controllers/v1/proxies/remove.go index c1b68f3..4e476dd 100644 --- a/backend/api/controllers/v1/proxies/remove.go +++ b/backend/api/controllers/v1/proxies/remove.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/proxies/start.go b/backend/api/controllers/v1/proxies/start.go index ff3e65c..2647277 100644 --- a/backend/api/controllers/v1/proxies/start.go +++ b/backend/api/controllers/v1/proxies/start.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/proxies/stop.go b/backend/api/controllers/v1/proxies/stop.go index c3562a5..474228a 100644 --- a/backend/api/controllers/v1/proxies/stop.go +++ b/backend/api/controllers/v1/proxies/stop.go @@ -4,11 +4,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/users/create.go b/backend/api/controllers/v1/users/create.go index 92cb543..e92858a 100644 --- a/backend/api/controllers/v1/users/create.go +++ b/backend/api/controllers/v1/users/create.go @@ -9,9 +9,9 @@ import ( "github.com/go-playground/validator/v10" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - permissionHelper "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" diff --git a/backend/api/controllers/v1/users/login.go b/backend/api/controllers/v1/users/login.go index 1db7a81..b6fd586 100644 --- a/backend/api/controllers/v1/users/login.go +++ b/backend/api/controllers/v1/users/login.go @@ -6,8 +6,8 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/users/lookup.go b/backend/api/controllers/v1/users/lookup.go index 2432470..04782e5 100644 --- a/backend/api/controllers/v1/users/lookup.go +++ b/backend/api/controllers/v1/users/lookup.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/users/refresh.go b/backend/api/controllers/v1/users/refresh.go index 1bb9c1f..59fcf80 100644 --- a/backend/api/controllers/v1/users/refresh.go +++ b/backend/api/controllers/v1/users/refresh.go @@ -5,8 +5,8 @@ import ( "net/http" "time" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/controllers/v1/users/remove.go b/backend/api/controllers/v1/users/remove.go index 7edcf81..5446755 100644 --- a/backend/api/controllers/v1/users/remove.go +++ b/backend/api/controllers/v1/users/remove.go @@ -4,9 +4,9 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/permissions" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" diff --git a/backend/api/jwtcore/jwt.go b/backend/api/jwtcore/jwt.go index 44c0f05..ed2ec56 100644 --- a/backend/api/jwtcore/jwt.go +++ b/backend/api/jwtcore/jwt.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - "git.terah.dev/imterah/hermes/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/dbcore" "github.com/golang-jwt/jwt/v5" ) diff --git a/backend/api/main.go b/backend/api/main.go index 54e83d5..c233d06 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -9,13 +9,13 @@ import ( "path/filepath" "strings" - "git.terah.dev/imterah/hermes/api/backendruntime" - "git.terah.dev/imterah/hermes/api/controllers/v1/backends" - "git.terah.dev/imterah/hermes/api/controllers/v1/proxies" - "git.terah.dev/imterah/hermes/api/controllers/v1/users" - "git.terah.dev/imterah/hermes/api/dbcore" - "git.terah.dev/imterah/hermes/api/jwtcore" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/api/backendruntime" + "git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends" + "git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies" + "git.terah.dev/imterah/hermes/backend/api/controllers/v1/users" + "git.terah.dev/imterah/hermes/backend/api/dbcore" + "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/urfave/cli/v2" diff --git a/backend/api/permissions/permission_nodes.go b/backend/api/permissions/permission_nodes.go index 812fafa..d682d69 100644 --- a/backend/api/permissions/permission_nodes.go +++ b/backend/api/permissions/permission_nodes.go @@ -1,6 +1,6 @@ package permissions -import "git.terah.dev/imterah/hermes/api/dbcore" +import "git.terah.dev/imterah/hermes/backend/api/dbcore" var DefaultPermissionNodes []string = []string{ "routes.add", diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index 2631209..5b99b2d 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -5,7 +5,7 @@ import ( "net" "os" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" ) diff --git a/backend/backendutil/structure.go b/backend/backendutil/structure.go index 5bdcfa4..3b97466 100644 --- a/backend/backendutil/structure.go +++ b/backend/backendutil/structure.go @@ -1,6 +1,6 @@ package backendutil -import "git.terah.dev/imterah/hermes/commonbackend" +import "git.terah.dev/imterah/hermes/backend/commonbackend" type BackendInterface interface { StartBackend(arguments []byte) (bool, error) diff --git a/backend/dummybackend/main.go b/backend/dummybackend/main.go index 893944d..92d88f5 100644 --- a/backend/dummybackend/main.go +++ b/backend/dummybackend/main.go @@ -3,8 +3,8 @@ package main import ( "os" - "git.terah.dev/imterah/hermes/backendutil" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/backendutil" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" ) diff --git a/backend/externalbackendlauncher/main.go b/backend/externalbackendlauncher/main.go index aa5e8ad..4c66c6a 100644 --- a/backend/externalbackendlauncher/main.go +++ b/backend/externalbackendlauncher/main.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "git.terah.dev/imterah/hermes/backendlauncher" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/backendlauncher" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/urfave/cli/v2" ) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index 963c1bd..e96ed5e 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -9,8 +9,8 @@ import ( "strings" "sync" - "git.terah.dev/imterah/hermes/backendutil" - "git.terah.dev/imterah/hermes/commonbackend" + "git.terah.dev/imterah/hermes/backend/backendutil" + "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/go-playground/validator/v10" "golang.org/x/crypto/ssh" diff --git a/backend/go.mod b/go.mod similarity index 100% rename from backend/go.mod rename to go.mod diff --git a/backend/go.sum b/go.sum similarity index 100% rename from backend/go.sum rename to go.sum diff --git a/migration-entrypoint.sh b/migration-entrypoint.sh index c1eb766..a00c408 100755 --- a/migration-entrypoint.sh +++ b/migration-entrypoint.sh @@ -40,7 +40,7 @@ echo "Restored backup. If this restore fails after the database has wiped, get a echo "copy the backup contents into the container (base64 decoded) at '/tmp/db.json.gz'," echo "and rerun /app/entrypoint.sh." echo "" -echo "If further issues continue, open an issue at 'https://git.terah.dev/imterah/hermes'." +echo "If further issues continue, open an issue at 'https://git.terah.dev/imterah/hermes/backend'." echo "If the migration succeeded, congratulations!" sleep 10000 From fd4d6bfd65a4398be5efdc76a5aef32111020d46 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 15:41:05 -0500 Subject: [PATCH 04/45] fix: Fixes rare errors regarding Nix shell not finding the initialization script. --- shell.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index e8f1a3b..17c5989 100644 --- a/shell.nix +++ b/shell.nix @@ -9,6 +9,8 @@ ]; shellHook = '' - source init.sh + if [ -f init.sh ]; then + source init.sh + fi ''; } From aaacdfd5f40e47960bec3fe1af60c7d208fdd158 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 15:43:21 -0500 Subject: [PATCH 05/45] chore: Remove all legacy code. --- .forgejo/workflows/release.yml | 15 +- MigrationDockerfile | 25 - backend-legacy/Dockerfile | 15 - backend-legacy/dev.env | 7 - backend-legacy/docker-entrypoint.sh | 12 - backend-legacy/eslint.config.js | 19 - backend-legacy/package-lock.json | 3302 ----------------- backend-legacy/package.json | 38 - .../20240421200334_init/migration.sql | 53 - .../migration.sql | 8 - .../migration.sql | 8 - .../migration.sql | 2 - .../prisma/migrations/migration_lock.toml | 3 - backend-legacy/prisma/schema.prisma | 54 - backend-legacy/src/tools/exportDBContents.ts | 52 - backend-legacy/tsconfig.json | 22 - migration-entrypoint.sh | 46 - sshfrontend/.gitignore | 133 - sshfrontend/Dockerfile | 14 - sshfrontend/LICENSE | 28 - sshfrontend/README.md | 2 - sshfrontend/docker-entrypoint.sh | 8 - sshfrontend/eslint.config.js | 19 - sshfrontend/package-lock.json | 2564 ------------- sshfrontend/package.json | 34 - sshfrontend/src/commands.ts | 65 - sshfrontend/src/commands/backends.ts | 519 --- sshfrontend/src/commands/connections.ts | 504 --- sshfrontend/src/commands/users.ts | 215 -- sshfrontend/src/copyID.ts | 48 - sshfrontend/src/index.ts | 242 -- sshfrontend/src/libs/patchCommander.ts | 108 - sshfrontend/src/libs/readFromKeyboard.ts | 109 - sshfrontend/tsconfig.json | 26 - 34 files changed, 2 insertions(+), 8317 deletions(-) delete mode 100644 MigrationDockerfile delete mode 100644 backend-legacy/Dockerfile delete mode 100644 backend-legacy/dev.env delete mode 100644 backend-legacy/docker-entrypoint.sh delete mode 100644 backend-legacy/eslint.config.js delete mode 100644 backend-legacy/package-lock.json delete mode 100644 backend-legacy/package.json delete mode 100644 backend-legacy/prisma/migrations/20240421200334_init/migration.sql delete mode 100644 backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql delete mode 100644 backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql delete mode 100644 backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql delete mode 100644 backend-legacy/prisma/migrations/migration_lock.toml delete mode 100644 backend-legacy/prisma/schema.prisma delete mode 100644 backend-legacy/src/tools/exportDBContents.ts delete mode 100644 backend-legacy/tsconfig.json delete mode 100755 migration-entrypoint.sh delete mode 100644 sshfrontend/.gitignore delete mode 100644 sshfrontend/Dockerfile delete mode 100644 sshfrontend/LICENSE delete mode 100644 sshfrontend/README.md delete mode 100755 sshfrontend/docker-entrypoint.sh delete mode 100644 sshfrontend/eslint.config.js delete mode 100644 sshfrontend/package-lock.json delete mode 100644 sshfrontend/package.json delete mode 100644 sshfrontend/src/commands.ts delete mode 100644 sshfrontend/src/commands/backends.ts delete mode 100644 sshfrontend/src/commands/connections.ts delete mode 100644 sshfrontend/src/commands/users.ts delete mode 100644 sshfrontend/src/copyID.ts delete mode 100644 sshfrontend/src/index.ts delete mode 100644 sshfrontend/src/libs/patchCommander.ts delete mode 100644 sshfrontend/src/libs/readFromKeyboard.ts delete mode 100644 sshfrontend/tsconfig.json diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 6c2496d..e60aacf 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -32,23 +32,12 @@ jobs: username: imterah password: ${{secrets.ACTIONS_PACKAGES_DEPL_KEY}} - - name: Build Docker images + - name: Build Docker image run: | docker build ./backend --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME - docker build -t ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME -f MigrationDockerfile . - docker build ./sshfrontend --tag ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME - - - name: Upload all Docker images + - name: Upload Docker image run: | docker tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME ghcr.io/imterah/hermes:latest docker push ghcr.io/imterah/hermes:$GITHUB_REF_NAME docker push ghcr.io/imterah/hermes:latest - - docker tag ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME ghcr.io/imterah/hermes-backend-migration:latest - docker push ghcr.io/imterah/hermes-backend-migration:$GITHUB_REF_NAME - docker push ghcr.io/imterah/hermes-backend-migration:latest - - docker tag ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME ghcr.io/imterah/hermes-lom:latest - docker push ghcr.io/imterah/hermes-lom:$GITHUB_REF_NAME - docker push ghcr.io/imterah/hermes-lom:latest diff --git a/MigrationDockerfile b/MigrationDockerfile deleted file mode 100644 index 1b71a21..0000000 --- a/MigrationDockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM golang:latest AS build -WORKDIR /build -COPY backend /build -RUN cd api; go build . -FROM node:22.11.0-bookworm AS run -LABEL org.opencontainers.image.source="https://git.terah.dev/imterah/nextnet" -COPY migration-entrypoint.sh /app/entrypoint.sh -COPY backend-legacy/src /app/legacy/src -COPY backend-legacy/prisma /app/legacy/prisma -COPY backend-legacy/tsconfig.json /app/legacy/ -COPY backend-legacy/package.json /app/legacy/ -COPY backend-legacy/package-lock.json /app/legacy/ -WORKDIR /app/legacy -RUN apt update -RUN apt install postgresql -y -RUN npm install --save-dev -RUN npm run build -RUN rm out/**/*.ts out/**/*.map -RUN rm -rf src -RUN npm prune --production -WORKDIR /app/modern -COPY --from=build /build/api/api /app/modern/hermes -RUN echo "{}" >> /app/modern/backends.json -WORKDIR /app -ENTRYPOINT sh entrypoint.sh diff --git a/backend-legacy/Dockerfile b/backend-legacy/Dockerfile deleted file mode 100644 index 8a1df00..0000000 --- a/backend-legacy/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:22.11.0-bookworm -LABEL org.opencontainers.image.source="https://github.com/greysoh/nextnet" -WORKDIR /app/ -COPY src /app/src -COPY prisma /app/prisma -COPY docker-entrypoint.sh /app/ -COPY tsconfig.json /app/ -COPY package.json /app/ -COPY package-lock.json /app/ -RUN npm install --save-dev -RUN npm run build -RUN rm out/**/*.ts out/**/*.map -RUN rm -rf src -RUN npm prune --production -ENTRYPOINT sh docker-entrypoint.sh diff --git a/backend-legacy/dev.env b/backend-legacy/dev.env deleted file mode 100644 index 750a474..0000000 --- a/backend-legacy/dev.env +++ /dev/null @@ -1,7 +0,0 @@ -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema - -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - -DATABASE_URL="postgresql://nextnet:nextnet@localhost:5432/nextnet?schema=nextnet" \ No newline at end of file diff --git a/backend-legacy/docker-entrypoint.sh b/backend-legacy/docker-entrypoint.sh deleted file mode 100644 index b76e0e1..0000000 --- a/backend-legacy/docker-entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -export NODE_ENV="production" - -if [[ "$DATABASE_URL" == "" ]]; then - export DATABASE_URL="postgresql://$POSTGRES_USERNAME:$POSTGRES_PASSWORD@nextnet-postgres:5432/$POSTGRES_DB?schema=nextnet" -fi - -echo "Welcome to NextNet." -echo "Running database migrations..." -npx prisma migrate deploy -echo "Starting application..." -npm start diff --git a/backend-legacy/eslint.config.js b/backend-legacy/eslint.config.js deleted file mode 100644 index 8cd9d68..0000000 --- a/backend-legacy/eslint.config.js +++ /dev/null @@ -1,19 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; - -export default [ - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - - { - languageOptions: { - globals: globals.node, - }, - - rules: { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "off", - }, - }, -]; diff --git a/backend-legacy/package-lock.json b/backend-legacy/package-lock.json deleted file mode 100644 index d43e4f7..0000000 --- a/backend-legacy/package-lock.json +++ /dev/null @@ -1,3302 +0,0 @@ -{ - "name": "nextnet", - "version": "1.1.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nextnet", - "version": "1.1.2", - "license": "BSD-3-Clause", - "dependencies": { - "@fastify/websocket": "^11.0.1", - "@prisma/client": "^6.0.0", - "bcrypt": "^5.1.1", - "fastify": "^5.1.0", - "node-ssh": "^13.2.0" - }, - "devDependencies": { - "@eslint/js": "^9.16.0", - "@types/bcrypt": "^5.0.2", - "@types/node": "^22.10.1", - "@types/ssh2": "^1.15.1", - "@types/ws": "^8.5.13", - "eslint": "^9.16.0", - "globals": "^15.12.0", - "nodemon": "^3.1.7", - "pino-pretty": "^13.0.0", - "prettier": "^3.4.1", - "prisma": "^5.22.0", - "typescript": "^5.7.2", - "typescript-eslint": "^8.16.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", - "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", - "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/@fastify/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", - "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", - "license": "MIT" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", - "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", - "license": "MIT", - "dependencies": { - "fast-json-stringify": "^6.0.0" - } - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", - "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/@fastify/websocket": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.0.1.tgz", - "integrity": "sha512-44yam5+t1I9v09hWBYO+ezV88+mb9Se2BjgERtzB/68+0mGeTfFkjBeDBe2y+ZdiPpeO2rhevhdnfrBm5mqH+Q==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.3", - "fastify-plugin": "^5.0.0", - "ws": "^8.16.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@prisma/client": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.1.tgz", - "integrity": "sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0" - } - }, - "node_modules/@types/bcrypt": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", - "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/ssh2": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", - "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18" - } - }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.67", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", - "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/ssh2/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/avvio": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", - "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", - "license": "MIT", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cpu-features": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", - "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.19.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", - "@eslint/plugin-kit": "^0.2.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stringify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", - "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.1.1", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-deep-equal": "^3.1.3", - "fast-uri": "^2.3.0", - "json-schema-ref-resolver": "^1.0.1", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-json-stringify/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "license": "BSD-3-Clause" - }, - "node_modules/fast-json-stringify/node_modules/fast-uri": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", - "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", - "license": "MIT" - }, - "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "license": "BSD-3-Clause" - }, - "node_modules/fastify": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.1.0.tgz", - "integrity": "sha512-0SdUC5AoiSgMSc2Vxwv3WyKzyGMDJRAW/PgNsK1kZrnkO6MeqUIW9ovVg9F2UGIqtIcclYMyeJa4rK6OZc7Jxg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^4.0.0", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^6.0.0", - "find-my-way": "^9.0.0", - "light-my-request": "^6.0.0", - "pino": "^9.0.0", - "process-warning": "^4.0.0", - "proxy-addr": "^2.0.7", - "rfdc": "^1.3.1", - "secure-json-parse": "^2.7.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/fastify-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", - "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-my-way": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", - "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", - "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "dev": true, - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-ref-resolver": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", - "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/light-my-request": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.3.0.tgz", - "integrity": "sha512-bWTAPJmeWQH5suJNYwG0f5cs0p6ho9e6f1Ppoxv5qMosY+s9Ir2+ZLvvHcgA7VTDop4zl/NCHhOVVqU+kd++Ow==", - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "license": "MIT", - "optional": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-ssh": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.0.tgz", - "integrity": "sha512-7vsKR2Bbs66th6IWCy/7SN4MSwlVt+G6QrHB631BjRUM8/LmvDugtYhi0uAmgvHS/+PVurfNBOmELf30rm0MZg==", - "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "make-dir": "^3.1.0", - "sb-promise-queue": "^2.1.0", - "sb-scandir": "^3.1.0", - "shell-escape": "^0.2.0", - "ssh2": "^1.14.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pino": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", - "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^4.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-pretty": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", - "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", - "fast-safe-stringify": "^2.1.1", - "help-me": "^5.0.0", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pump": "^3.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^4.0.1", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/engines": "5.22.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=16.13" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - } - }, - "node_modules/process-warning": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", - "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex2": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", - "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", - "license": "MIT", - "dependencies": { - "ret": "~0.5.0" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sb-promise-queue": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.0.tgz", - "integrity": "sha512-zwq4YuP1FQFkGx2Q7GIkZYZ6PqWpV+bg0nIO1sJhWOyGyhqbj0MsTvK6lCFo5TQwX5pZr6SCQ75e8PCDCuNvkg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/sb-scandir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.0.tgz", - "integrity": "sha512-70BVm2xz9jn94zSQdpvYrEG101/UV9TVGcfWr9T5iob3QhCK4lYXeculfBqPGFv3XTeKgx4dpWyYIDeZUqo4kg==", - "license": "MIT", - "dependencies": { - "sb-promise-queue": "^2.1.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-escape": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", - "integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==", - "license": "MIT" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/ssh2": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", - "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.10", - "nan": "^2.20.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", - "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/backend-legacy/package.json b/backend-legacy/package.json deleted file mode 100644 index 2f8dba9..0000000 --- a/backend-legacy/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "nextnet", - "version": "1.1.2", - "description": "Yet another dashboard to manage portforwarding technologies", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc", - "start": "cd out && node --enable-source-maps index.js", - "dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\"" - }, - "keywords": [], - "author": "greysoh", - "license": "BSD-3-Clause", - "devDependencies": { - "@eslint/js": "^9.16.0", - "@types/bcrypt": "^5.0.2", - "@types/node": "^22.10.1", - "@types/ssh2": "^1.15.1", - "@types/ws": "^8.5.13", - "eslint": "^9.16.0", - "globals": "^15.12.0", - "nodemon": "^3.1.7", - "pino-pretty": "^13.0.0", - "prettier": "^3.4.1", - "prisma": "^5.22.0", - "typescript": "^5.7.2", - "typescript-eslint": "^8.16.0" - }, - "dependencies": { - "@fastify/websocket": "^11.0.1", - "@prisma/client": "^6.0.0", - "bcrypt": "^5.1.1", - "fastify": "^5.1.0", - "node-ssh": "^13.2.0" - } -} diff --git a/backend-legacy/prisma/migrations/20240421200334_init/migration.sql b/backend-legacy/prisma/migrations/20240421200334_init/migration.sql deleted file mode 100644 index 17e7104..0000000 --- a/backend-legacy/prisma/migrations/20240421200334_init/migration.sql +++ /dev/null @@ -1,53 +0,0 @@ --- CreateTable -CREATE TABLE "DesinationProvider" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "backend" TEXT NOT NULL, - "connectionDetails" TEXT NOT NULL, - - CONSTRAINT "DesinationProvider_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ForwardRule" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "sourceIP" TEXT NOT NULL, - "sourcePort" INTEGER NOT NULL, - "destIP" TEXT NOT NULL, - "destPort" INTEGER NOT NULL, - "destProviderID" INTEGER NOT NULL, - "enabled" BOOLEAN NOT NULL, - - CONSTRAINT "ForwardRule_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Permission" ( - "id" SERIAL NOT NULL, - "permission" TEXT NOT NULL, - "has" BOOLEAN NOT NULL, - "userID" INTEGER NOT NULL, - - CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "email" TEXT NOT NULL, - "name" TEXT NOT NULL, - "password" TEXT NOT NULL, - "rootToken" TEXT, - "isRootServiceAccount" BOOLEAN, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- AddForeignKey -ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userID_fkey" FOREIGN KEY ("userID") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql b/backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql deleted file mode 100644 index a673c64..0000000 --- a/backend-legacy/prisma/migrations/20240421210417_fix_remove_destip/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `destIP` on the `ForwardRule` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "ForwardRule" DROP COLUMN "destIP"; diff --git a/backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql b/backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql deleted file mode 100644 index a0a108f..0000000 --- a/backend-legacy/prisma/migrations/20240425125737_fix_adds_protocol_field/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `protocol` to the `ForwardRule` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "ForwardRule" ADD COLUMN "protocol" TEXT NOT NULL; diff --git a/backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql b/backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql deleted file mode 100644 index 5af7c52..0000000 --- a/backend-legacy/prisma/migrations/20240505233740_feature_adds_username_support/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "username" TEXT; diff --git a/backend-legacy/prisma/migrations/migration_lock.toml b/backend-legacy/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/backend-legacy/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/backend-legacy/prisma/schema.prisma b/backend-legacy/prisma/schema.prisma deleted file mode 100644 index 486f3e3..0000000 --- a/backend-legacy/prisma/schema.prisma +++ /dev/null @@ -1,54 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model DesinationProvider { - id Int @id @default(autoincrement()) - - name String - description String? - backend String - connectionDetails String -} - -model ForwardRule { - id Int @id @default(autoincrement()) - - name String - description String? - protocol String - sourceIP String - sourcePort Int - destPort Int - destProviderID Int - enabled Boolean -} - -model Permission { - id Int @id @default(autoincrement()) - - permission String - has Boolean - user User @relation(fields: [userID], references: [id]) - userID Int -} - -model User { - id Int @id @default(autoincrement()) - - email String @unique - username String? // NOT optional in the API, but just for backwards compat - name String - password String // Will be hashed using bcrypt - rootToken String? - isRootServiceAccount Boolean? - permissions Permission[] -} \ No newline at end of file diff --git a/backend-legacy/src/tools/exportDBContents.ts b/backend-legacy/src/tools/exportDBContents.ts deleted file mode 100644 index 20c31bc..0000000 --- a/backend-legacy/src/tools/exportDBContents.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createReadStream, createWriteStream } from "node:fs"; -import { Readable, pipeline } from "node:stream"; -import { createGzip } from "node:zlib"; -import process from "node:process"; - -import { PrismaClient } from "@prisma/client"; - -const gzip = createGzip(); - -if (process.argv.length <= 2) { - console.error( - "Missing arguments! Usage: node ./out/tools/exportDBContents.js exportPath.json.gz", - ); - process.exit(1); -} - -console.log("Initializing Database..."); -const prisma = new PrismaClient(); -console.log("Initialized Database."); - -console.log("Getting all destinationProviders..."); -const destinationProviders = await prisma.desinationProvider.findMany(); - -console.log("Getting all forwardRules..."); -const forwardRules = await prisma.forwardRule.findMany(); - -console.log("Getting all permissions..."); -const allPermissions = await prisma.permission.findMany(); - -console.log("Getting all users..."); -const users = await prisma.user.findMany(); - -const masterList = JSON.stringify({ - destinationProviders, - forwardRules, - allPermissions, - users, -}); - -const source = new Readable(); -source.push(masterList); -source.push(null); - -const destination = createWriteStream(process.argv[2]); - -pipeline(source, gzip, destination, err => { - if (err) { - console.error("Failed to compress JSON data:", err); - } else { - console.log("Sucesfully saved DB contents."); - } -}); diff --git a/backend-legacy/tsconfig.json b/backend-legacy/tsconfig.json deleted file mode 100644 index d584b3b..0000000 --- a/backend-legacy/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "module": "es2022", - "moduleResolution": "node", - - "outDir": "./out", - "rootDir": "./src", - - "strict": true, - "esModuleInterop": true, - "sourceMap": true, - - "declaration": true, - "declarationMap": true, - - "strictPropertyInitialization": false, - }, - - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/migration-entrypoint.sh b/migration-entrypoint.sh deleted file mode 100755 index a00c408..0000000 --- a/migration-entrypoint.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -echo "Welcome to the Hermes migration assistant." - -if [ ! -f "/tmp/db.json.gz" ]; then - echo "Exporting database contents..." - cd /app/legacy - node out/tools/exportDBContents.js /tmp/db.json.gz - BACKUP_EXIT_CODE=$? - - if [ $BACKUP_EXIT_CODE -ne 0 ]; then - echo "Failed to export database contents!" - exit 1 - fi - - echo "!! IMPORTANT !!" - echo "Database backup contents below:" - echo "==== BEGIN BACKUP ====" - cat /tmp/db.json.gz | base64 - echo "==== END BACKUP ====" - echo "When copying, do NOT copy the BEGIN and END sections." -fi - -echo "Wiping old database..." -cat >> /tmp/wipe.sql << EOF -CREATE DATABASE temp; -\c temp -DROP DATABASE $HERMES_MIGRATE_POSTGRES_DATABASE; -CREATE DATABASE $HERMES_MIGRATE_POSTGRES_DATABASE; -\c nextnet -DROP DATABASE temp; -EOF -psql "$HERMES_POSTGRES_DSN" < /tmp/wipe.sql -rm -rf /tmp/wipe.sql -echo "Restoring backup..." - -cd /app/modern -./hermes -b ./backends.json import --bp /tmp/db.json.gz - -echo "Restored backup. If this restore fails after the database has wiped, get a shell into the container," -echo "copy the backup contents into the container (base64 decoded) at '/tmp/db.json.gz'," -echo "and rerun /app/entrypoint.sh." -echo "" -echo "If further issues continue, open an issue at 'https://git.terah.dev/imterah/hermes/backend'." -echo "If the migration succeeded, congratulations!" - -sleep 10000 diff --git a/sshfrontend/.gitignore b/sshfrontend/.gitignore deleted file mode 100644 index 37df925..0000000 --- a/sshfrontend/.gitignore +++ /dev/null @@ -1,133 +0,0 @@ -# Output -out - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* diff --git a/sshfrontend/Dockerfile b/sshfrontend/Dockerfile deleted file mode 100644 index df64739..0000000 --- a/sshfrontend/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:22.11.0-bookworm -LABEL org.opencontainers.image.source="https://github.com/greysoh/nextnet" -WORKDIR /app/ -COPY src /app/src -COPY tsconfig.json /app/ -COPY package.json /app/ -COPY package-lock.json /app/ -COPY docker-entrypoint.sh /app/ -RUN npm install --save-dev -RUN npm run build -RUN rm out/**/*.ts out/**/*.map -RUN rm -rf src -RUN npm prune --production -ENTRYPOINT sh docker-entrypoint.sh diff --git a/sshfrontend/LICENSE b/sshfrontend/LICENSE deleted file mode 100644 index 8914588..0000000 --- a/sshfrontend/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2024, Greyson - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/sshfrontend/README.md b/sshfrontend/README.md deleted file mode 100644 index 16cb312..0000000 --- a/sshfrontend/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# NextNet LOM -Lights Out Management, NextNet style \ No newline at end of file diff --git a/sshfrontend/docker-entrypoint.sh b/sshfrontend/docker-entrypoint.sh deleted file mode 100755 index 36942f1..0000000 --- a/sshfrontend/docker-entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -export NODE_ENV="production" - -if [[ "$SERVER_BASE_URL" == "" ]]; then - export SERVER_BASE_URL="http://nextnet-api:3000/" -fi - -npm start diff --git a/sshfrontend/eslint.config.js b/sshfrontend/eslint.config.js deleted file mode 100644 index 0afc6f7..0000000 --- a/sshfrontend/eslint.config.js +++ /dev/null @@ -1,19 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; - -export default [ - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - - { - languageOptions: { - globals: globals.node, - }, - - rules: { - "@typescript-eslint/no-explicit-any": "off", - "no-constant-condition": "warn", - }, - }, -]; diff --git a/sshfrontend/package-lock.json b/sshfrontend/package-lock.json deleted file mode 100644 index ec26875..0000000 --- a/sshfrontend/package-lock.json +++ /dev/null @@ -1,2564 +0,0 @@ -{ - "name": "nextnet-lom", - "version": "1.1.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nextnet-lom", - "version": "1.1.2", - "license": "BSD-3-Clause", - "dependencies": { - "axios": "^1.7.8", - "commander": "^12.1.0", - "patch-package": "^8.0.0", - "ssh2": "^1.16.0", - "string-argv": "^0.3.2" - }, - "devDependencies": { - "@eslint/js": "^9.16.0", - "@types/node": "^22.10.1", - "@types/ssh2": "^1.15.1", - "@types/yargs": "^17.0.33", - "eslint": "^9.16.0", - "globals": "^15.12.0", - "nodemon": "^3.1.7", - "typescript": "^5.7.2", - "typescript-eslint": "^8.16.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@types/ssh2": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", - "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18" - } - }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.67", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", - "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/ssh2/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.16.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "license": "BSD-2-Clause" - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/axios": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", - "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/cpu-features": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", - "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.19.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", - "@eslint/plugin-kit": "^0.2.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-yarn-workspace-root": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "license": "Apache-2.0", - "dependencies": { - "micromatch": "^4.0.2" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", - "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", - "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", - "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", - "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "isarray": "^2.0.5", - "jsonify": "^0.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "license": "Public Domain", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "license": "MIT", - "optional": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/patch-package": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", - "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", - "license": "MIT", - "dependencies": { - "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^4.1.2", - "ci-info": "^3.7.0", - "cross-spawn": "^7.0.3", - "find-yarn-workspace-root": "^2.0.0", - "fs-extra": "^9.0.0", - "json-stable-stringify": "^1.0.2", - "klaw-sync": "^6.0.0", - "minimist": "^1.2.6", - "open": "^7.4.2", - "rimraf": "^2.6.3", - "semver": "^7.5.3", - "slash": "^2.0.0", - "tmp": "^0.0.33", - "yaml": "^2.2.2" - }, - "bin": { - "patch-package": "index.js" - }, - "engines": { - "node": ">=14", - "npm": ">5" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ssh2": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", - "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.10", - "nan": "^2.20.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/sshfrontend/package.json b/sshfrontend/package.json deleted file mode 100644 index 66a2a78..0000000 --- a/sshfrontend/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "nextnet-lom", - "version": "1.1.2", - "description": "Lights Out Management, NextNet style", - "main": "index.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc", - "start": "cd out && node --enable-source-maps index.js", - "dev": "nodemon --watch src --ext ts,js,mjs,json --exec \"tsc && cd out && node --enable-source-maps index.js\"" - }, - "keywords": [], - "author": "greysoh", - "license": "BSD-3-Clause", - "devDependencies": { - "@eslint/js": "^9.16.0", - "@types/node": "^22.10.1", - "@types/ssh2": "^1.15.1", - "@types/yargs": "^17.0.33", - "eslint": "^9.16.0", - "globals": "^15.12.0", - "nodemon": "^3.1.7", - "typescript": "^5.7.2", - "typescript-eslint": "^8.16.0" - }, - "dependencies": { - "axios": "^1.7.8", - "commander": "^12.1.0", - "patch-package": "^8.0.0", - "ssh2": "^1.16.0", - "string-argv": "^0.3.2" - } -} diff --git a/sshfrontend/src/commands.ts b/sshfrontend/src/commands.ts deleted file mode 100644 index bb06314..0000000 --- a/sshfrontend/src/commands.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Axios } from "axios"; - -import { run as connection } from "./commands/connections.js"; -import { run as backends } from "./commands/backends.js"; -import { run as users } from "./commands/users.js"; - -export type PrintLine = (...str: unknown[]) => void; -export type KeyboardRead = (disableEcho?: boolean) => Promise; - -type Command = ( - args: string[], - println: PrintLine, - axios: Axios, - apiKey: string, - keyboardRead: KeyboardRead, -) => Promise; - -type Commands = { - name: string; - description: string; - run: Command; -}[]; - -export const commands: Commands = [ - { - name: "help", - description: "Prints help", - async run(_args: string[], printf: PrintLine) { - commands.forEach(command => { - printf(`${command.name}: ${command.description}\n`); - }); - - printf( - "\nRun a command of your choosing with --help to see more options.\n", - ); - }, - }, - { - name: "clear", - description: "Clears screen", - async run(_args: string[], printf: PrintLine) { - printf("\x1B[2J\x1B[3J\x1B[H"); - }, - }, - { - name: "conn", - description: "Manages connections for NextNet", - run: connection, - }, - { - name: "user", - description: "Manages users for NextNet", - run: users, - }, - { - name: "backend", - description: "Manages backends for NextNet", - run: backends, - }, - { - name: "back", - description: "(alias) Manages backends for NextNet", - run: backends, - }, -]; diff --git a/sshfrontend/src/commands/backends.ts b/sshfrontend/src/commands/backends.ts deleted file mode 100644 index 0930bbb..0000000 --- a/sshfrontend/src/commands/backends.ts +++ /dev/null @@ -1,519 +0,0 @@ -import type { Axios } from "axios"; - -import { SSHCommand } from "../libs/patchCommander.js"; -import type { PrintLine, KeyboardRead } from "../commands.js"; - -type BackendLookupSuccess = { - success: boolean; - data: { - id: number; - - name: string; - description: string; - backend: string; - connectionDetails?: string; - logs: string[]; - }[]; -}; - -const addRequiredOptions = { - ssh: ["sshKey", "username", "host"], - - passyfire: ["host"], -}; - -export async function run( - argv: string[], - println: PrintLine, - axios: Axios, - token: string, - readKeyboard: KeyboardRead, -) { - const program = new SSHCommand(println); - program.description("Manages backends for NextNet"); - program.version("v1.0.0"); - - const addBackend = new SSHCommand(println, "add"); - - addBackend.description("Adds a backend"); - addBackend.argument("", "Name of the backend"); - - addBackend.argument( - "", - "Provider of the backend (ex. passyfire, ssh)", - ); - - addBackend.option( - "-d, --description ", - "Description for the backend", - ); - - addBackend.option( - "-f, --force-custom-parameters", - "If turned on, this forces you to use custom parameters", - ); - - addBackend.option( - "-c, --custom-parameters ", - "Custom parameters. Use this if the backend you're using isn't native to SSH yet, or if you manually turn on -f.", - ); - - // SSH provider - addBackend.option( - "-k, --ssh-key ", - "(SSH) SSH private key to use to authenticate with the server", - ); - - addBackend.option( - "-u, --username ", - "(SSH, PassyFire) Username to authenticate with. With PassyFire, it's the username you create", - ); - - addBackend.option( - "-h, --host ", - "(SSH, PassyFire) Host to connect to. With PassyFire, it's what you listen on", - ); - - // PassyFire provider - addBackend.option( - "-pe, --is-proxied", - "(PassyFire) Specify if you're behind a proxy or not so we can get the right IP", - ); - - addBackend.option( - "-pp, --proxied-port ", - "(PassyFire) If you're behind a proxy, and the port is different, specify the port to return", - ); - - addBackend.option("-g, --guest", "(PassyFire) Enable the guest user"); - - addBackend.option( - "-ua, --user-ask", - "(PassyFire) Ask what users you want to create", - ); - - addBackend.option( - "-p, --password ", - "(PassyFire) What password you want to use for the primary user", - ); - - addBackend.action( - async ( - name: string, - provider: string, - options: { - description?: string; - forceCustomParameters?: boolean; - customParameters?: string; - - // SSH (mostly) - sshKey?: string; - username?: string; - host?: string; - - // PassyFire (mostly) - isProxied?: boolean; - proxiedPort?: string; - guest?: boolean; - userAsk?: boolean; - password?: string; - }, - ) => { - // @ts-expect-error: Yes it can index for what we need it to do. - const isUnsupportedPlatform: boolean = !addRequiredOptions[provider]; - - if (isUnsupportedPlatform) { - println( - "WARNING: Platform is not natively supported by the LOM yet!\n", - ); - } - - let connectionDetails: string = ""; - - if (options.forceCustomParameters || isUnsupportedPlatform) { - if (typeof options.customParameters != "string") { - return println( - "ERROR: You are missing the custom parameters option!\n", - ); - } - - connectionDetails = options.customParameters; - } else if (provider == "ssh") { - for (const argument of addRequiredOptions["ssh"]) { - // @ts-expect-error: No. - const hasArgument = options[argument]; - - if (!hasArgument) { - return println("ERROR: Missing argument '%s'\n", argument); - } - } - - const unstringifiedArguments: { - ip?: string; - port?: number; - username?: string; - privateKey?: string; - } = {}; - - if (options.host) { - const sourceSplit: string[] = options.host.split(":"); - - const sourceIP: string = sourceSplit[0]; - const sourcePort: number = - sourceSplit.length >= 2 ? parseInt(sourceSplit[1]) : 22; - - unstringifiedArguments.ip = sourceIP; - unstringifiedArguments.port = sourcePort; - } - - unstringifiedArguments.username = options.username; - unstringifiedArguments.privateKey = options.sshKey?.replaceAll( - "\\n", - "\n", - ); - - connectionDetails = JSON.stringify(unstringifiedArguments); - } else if (provider == "passyfire") { - for (const argument of addRequiredOptions["passyfire"]) { - // @ts-expect-error: No. - const hasArgument = options[argument]; - - if (!hasArgument) { - return println("ERROR: Missing argument '%s'\n", argument); - } - } - - const unstringifiedArguments: { - ip?: string; - port?: number; - publicPort?: number; - isProxied?: boolean; - users: { - username: string; - password: string; - }[]; - } = { - users: [], - }; - - if (options.guest) { - unstringifiedArguments.users.push({ - username: "guest", - password: "guest", - }); - } - - if (options.username) { - if (!options.password) { - return println("Password must not be left blank\n"); - } - - unstringifiedArguments.users.push({ - username: options.username, - password: options.password, - }); - } - - if (options.userAsk) { - let shouldContinueAsking: boolean = true; - - while (shouldContinueAsking) { - println("Creating a user.\nUsername: "); - const username = await readKeyboard(); - - let passwordConfirmOne = "a"; - let passwordConfirmTwo = "b"; - - println("\n"); - - while (passwordConfirmOne != passwordConfirmTwo) { - println("Password: "); - passwordConfirmOne = await readKeyboard(true); - - println("\nConfirm password: "); - passwordConfirmTwo = await readKeyboard(true); - - println("\n"); - - if (passwordConfirmOne != passwordConfirmTwo) { - println("Passwords do not match! Try again.\n\n"); - } - } - - unstringifiedArguments.users.push({ - username, - password: passwordConfirmOne, - }); - - println("\nShould we continue creating users? (y/n) "); - shouldContinueAsking = (await readKeyboard()) - .toLowerCase() - .trim() - .startsWith("y"); - - println("\n\n"); - } - } - - if (unstringifiedArguments.users.length == 0) { - return println( - "No users will be created with your current arguments! You must have users set up.\n", - ); - } - - unstringifiedArguments.isProxied = Boolean(options.isProxied); - - if (options.proxiedPort) { - unstringifiedArguments.publicPort = parseInt( - options.proxiedPort ?? "", - ); - - if (Number.isNaN(unstringifiedArguments.publicPort)) { - println("UID (%s) is not a number.\n", options.proxiedPort); - return; - } - } - - if (options.host) { - const sourceSplit: string[] = options.host.split(":"); - - if (sourceSplit.length != 2) { - return println( - "Source could not be splitted down (are you missing the ':' in the source to specify port?)\n", - ); - } - - const sourceIP: string = sourceSplit[0]; - const sourcePort: number = parseInt(sourceSplit[1]); - - if (Number.isNaN(sourcePort)) { - println("UID (%s) is not a number.\n", sourcePort); - return; - } - - unstringifiedArguments.ip = sourceIP; - unstringifiedArguments.port = sourcePort; - } - - connectionDetails = JSON.stringify(unstringifiedArguments); - } - - const response = await axios.post("/api/v1/backends/create", { - token, - - name, - description: options.description, - backend: provider, - - connectionDetails, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error creating a backend!\n"); - } - - return; - } - - println("Successfully created the backend.\n"); - }, - ); - - const removeBackend = new SSHCommand(println, "rm"); - removeBackend.description("Removes a backend"); - removeBackend.argument("", "ID of the backend"); - - removeBackend.action(async (idStr: string) => { - const id: number = parseInt(idStr); - - if (Number.isNaN(id)) { - println("ID (%s) is not a number.\n", idStr); - return; - } - - const response = await axios.post("/api/v1/backends/remove", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error deleting backend!\n"); - } - - return; - } - - println("Backend has been successfully deleted.\n"); - }); - - const lookupBackend = new SSHCommand(println, "find"); - lookupBackend.description("Looks up a backend based on your arguments"); - - lookupBackend.option("-n, --name ", "Name of the backend"); - - lookupBackend.option( - "-p, --provider ", - "Provider of the backend (ex. passyfire, ssh)", - ); - - lookupBackend.option( - "-d, --description ", - "Description for the backend", - ); - - lookupBackend.option( - "-e, --parse-connection-details", - "If specified, we automatically parse the connection details to make them human readable, if standard JSON.", - ); - - lookupBackend.action( - async (options: { - name?: string; - provider?: string; - description?: string; - parseConnectionDetails?: boolean; - }) => { - const response = await axios.post("/api/v1/backends/lookup", { - token, - - name: options.name, - description: options.description, - - backend: options.provider, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error looking up backends!\n"); - } - - return; - } - - const { data }: BackendLookupSuccess = response.data; - - for (const backend of data) { - println("ID: %s:\n", backend.id); - println(" - Name: %s\n", backend.name); - println(" - Description: %s\n", backend.description); - println(" - Using Backend: %s\n", backend.backend); - - if (backend.connectionDetails) { - if (options.parseConnectionDetails) { - // We don't know what we're recieving. We just try to parse it (hence the any type) - // {} is more accurate but TS yells at us if we do that :( - - // eslint-disable-next-line - let parsedJSONData: any | undefined; - - try { - parsedJSONData = JSON.parse(backend.connectionDetails); - } catch (e) { - println(" - Connection Details: %s\n", backend.connectionDetails); - continue; - } - - if (!parsedJSONData) { - // Not really an assertion but I don't care right now - println( - "Assertion failed: parsedJSONData should not be undefined\n", - ); - continue; - } - - println(" - Connection details:\n"); - - for (const key of Object.keys(parsedJSONData)) { - let value: string | number = parsedJSONData[key]; - - if (typeof value == "string") { - value = value.replaceAll("\n", "\n" + " ".repeat(16)); - } - - if (typeof value == "object") { - // TODO: implement? - value = JSON.stringify(value); - } - - println(" - %s: %s\n", key, value); - } - } else { - println(" - Connection Details: %s\n", backend.connectionDetails); - } - } - - println("\n"); - } - - println("%s backends found.\n", data.length); - }, - ); - - const logsCommand = new SSHCommand(println, "logs"); - logsCommand.description("View logs for a backend"); - logsCommand.argument("", "ID of the backend"); - - logsCommand.action(async (idStr: string) => { - const id: number = parseInt(idStr); - - if (Number.isNaN(id)) { - println("ID (%s) is not a number.\n", idStr); - return; - } - - const response = await axios.post("/api/v1/backends/lookup", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error getting logs!\n"); - } - - return; - } - - const { data }: BackendLookupSuccess = response.data; - const ourBackend = data.find(i => i.id == id); - - if (!ourBackend) return println("Could not find the backend!\n"); - ourBackend.logs.forEach(log => println("%s\n", log)); - }); - - program.addCommand(addBackend); - program.addCommand(removeBackend); - program.addCommand(lookupBackend); - program.addCommand(logsCommand); - - program.parse(argv); - - // It would make sense to check this, then parse argv, however this causes issues with - // the application name not displaying correctly. - - if (argv.length == 1) { - println("No arguments specified!\n\n"); - program.help(); - return; - } - - await new Promise(resolve => program.onExit(resolve)); -} diff --git a/sshfrontend/src/commands/connections.ts b/sshfrontend/src/commands/connections.ts deleted file mode 100644 index 29f4eb6..0000000 --- a/sshfrontend/src/commands/connections.ts +++ /dev/null @@ -1,504 +0,0 @@ -import type { Axios } from "axios"; - -import { SSHCommand } from "../libs/patchCommander.js"; -import type { PrintLine } from "../commands.js"; - -// https://stackoverflow.com/questions/37938504/what-is-the-best-way-to-find-all-items-are-deleted-inserted-from-original-arra -function difference(a: any[], b: any[]) { - return a.filter(x => b.indexOf(x) < 0); -} - -type InboundConnectionSuccess = { - success: true; - data: { - ip: string; - port: number; - - connectionDetails: { - sourceIP: string; - sourcePort: number; - destPort: number; - enabled: boolean; - }; - }[]; -}; - -type LookupCommandSuccess = { - success: true; - data: { - id: number; - name: string; - description: string; - sourceIP: string; - sourcePort: number; - destPort: number; - providerID: number; - autoStart: boolean; - }[]; -}; - -export async function run( - argv: string[], - println: PrintLine, - axios: Axios, - token: string, -) { - if (argv.length == 1) - return println( - "error: no arguments specified! run %s --help to see commands.\n", - argv[0], - ); - - const program = new SSHCommand(println); - program.description("Manages connections for NextNet"); - program.version("v1.0.0"); - - const addCommand = new SSHCommand(println, "add"); - addCommand.description("Creates a new connection"); - - addCommand.argument( - "", - "The backend ID to use. Can be fetched by the command 'backend search'", - ); - - addCommand.argument("", "The name for the tunnel"); - addCommand.argument("", "The protocol to use. Either TCP or UDP"); - - addCommand.argument( - "", - "Source IP and port combo (ex. '192.168.0.63:25565'", - ); - - addCommand.argument("", "Destination port to use"); - addCommand.option("-d, --description", "Description for the tunnel"); - - addCommand.action( - async ( - providerIDStr: string, - name: string, - protocolRaw: string, - source: string, - destPortRaw: string, - options: { - description?: string; - }, - ) => { - const providerID = parseInt(providerIDStr); - - if (Number.isNaN(providerID)) { - println("ID (%s) is not a number\n", providerIDStr); - return; - } - - const protocol = protocolRaw.toLowerCase().trim(); - - if (protocol != "tcp" && protocol != "udp") { - return println("Protocol is not a valid option (not tcp or udp)\n"); - } - - const sourceSplit: string[] = source.split(":"); - - if (sourceSplit.length != 2) { - return println( - "Source could not be splitted down (are you missing the ':' in the source to specify port?)\n", - ); - } - - const sourceIP: string = sourceSplit[0]; - const sourcePort: number = parseInt(sourceSplit[1]); - - if (Number.isNaN(sourcePort)) { - return println("Port splitted is not a number\n"); - } - - const destinationPort: number = parseInt(destPortRaw); - - if (Number.isNaN(destinationPort)) { - return println("Destination port could not be parsed into a number\n"); - } - - const response = await axios.post("/api/v1/forward/create", { - token, - - name, - description: options.description, - - protocol, - - sourceIP, - sourcePort, - - destinationPort, - - providerID, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error creating a connection!\n"); - } - - return; - } - - println("Successfully created connection.\n"); - }, - ); - - const lookupCommand = new SSHCommand(println, "find"); - - lookupCommand.description( - "Looks up all connections based on the arguments you specify", - ); - - lookupCommand.option( - "-b, --backend-id ", - "The backend ID to use. Can be fetched by 'back find'", - ); - - lookupCommand.option("-n, --name ", "The name for the tunnel"); - - lookupCommand.option( - "-p, --protocol ", - "The protocol to use. Either TCP or UDP", - ); - - lookupCommand.option( - "-s , --source", - "Source IP and port combo (ex. '192.168.0.63:25565'", - ); - - lookupCommand.option("-d, --dest-port ", "Destination port to use"); - - lookupCommand.option( - "-o, --description ", - "Description for the tunnel", - ); - - lookupCommand.action( - async (options: { - backendId?: string; - destPort?: string; - name?: string; - protocol?: string; - source?: string; - description?: string; - }) => { - let numberBackendID: number | undefined; - - let sourceIP: string | undefined; - let sourcePort: number | undefined; - - let destPort: number | undefined; - - if (options.backendId) { - numberBackendID = parseInt(options.backendId); - - if (Number.isNaN(numberBackendID)) { - println("ID (%s) is not a number\n", options.backendId); - return; - } - } - - if (options.source) { - const sourceSplit: string[] = options.source.split(":"); - - if (sourceSplit.length != 2) { - return println( - "Source could not be splitted down (are you missing the ':' in the source to specify port?)\n", - ); - } - - sourceIP = sourceSplit[0]; - sourcePort = parseInt(sourceSplit[1]); - - if (Number.isNaN(sourcePort)) { - return println("Port splitted is not a number\n"); - } - } - - if (options.destPort) { - destPort = parseInt(options.destPort); - - if (Number.isNaN(destPort)) { - println("ID (%s) is not a number\n", options.destPort); - return; - } - } - - const response = await axios.post("/api/v1/forward/lookup", { - token, - - name: options.name, - description: options.description, - - protocol: options.protocol, - - sourceIP, - sourcePort, - - destinationPort: destPort, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error requesting connections!\n"); - } - - return; - } - - const { data }: LookupCommandSuccess = response.data; - - for (const connection of data) { - println( - "ID: %s%s:\n", - connection.id, - connection.autoStart ? " (automatically starts)" : "", - ); - println(" - Backend ID: %s\n", connection.providerID); - println(" - Name: %s\n", connection.name); - if (connection.description) - println(" - Description: %s\n", connection.description); - println( - " - Source: %s:%s\n", - connection.sourceIP, - connection.sourcePort, - ); - println(" - Destination port: %s\n", connection.destPort); - - println("\n"); - } - - println("%s connections found.\n", data.length); - }, - ); - - const startTunnel = new SSHCommand(println, "start"); - startTunnel.description("Starts a tunnel"); - startTunnel.argument("", "Tunnel ID to start"); - - startTunnel.action(async (idStr: string) => { - const id = parseInt(idStr); - - if (Number.isNaN(id)) { - println("ID (%s) is not a number\n", idStr); - return; - } - - const response = await axios.post("/api/v1/forward/start", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error starting the connection!\n"); - } - - return; - } - - println("Successfully started tunnel.\n"); - return; - }); - - const stopTunnel = new SSHCommand(println, "stop"); - stopTunnel.description("Stops a tunnel"); - stopTunnel.argument("", "Tunnel ID to stop"); - - stopTunnel.action(async (idStr: string) => { - const id = parseInt(idStr); - - if (Number.isNaN(id)) { - println("ID (%s) is not a number\n", idStr); - return; - } - - const response = await axios.post("/api/v1/forward/stop", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error stopping a connection!\n"); - } - - return; - } - - println("Successfully stopped tunnel.\n"); - }); - - const getInbound = new SSHCommand(println, "get-inbound"); - getInbound.description("Shows all current connections"); - getInbound.argument("", "Tunnel ID to view inbound connections of"); - getInbound.option("-t, --tail", "Live-view of connection list"); - getInbound.option( - "-s, --tail-pull-rate ", - "Controls the speed to pull at (in ms)", - ); - - getInbound.action( - async ( - idStr: string, - options: { - tail?: boolean; - tailPullRate?: string; - }, - ): Promise => { - const pullRate: number = options.tailPullRate - ? parseInt(options.tailPullRate) - : 2000; - const id = parseInt(idStr); - - if (Number.isNaN(id)) { - println("ID (%s) is not a number\n", idStr); - return; - } - - if (Number.isNaN(pullRate)) { - println("Pull rate is not a number\n"); - return; - } - - if (options.tail) { - let previousEntries: string[] = []; - - // FIXME? - // eslint-disable-next-line no-constant-condition - while (true) { - const response = await axios.post("/api/v1/forward/connections", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error requesting inbound connections!\n"); - } - - return; - } - - const { data }: InboundConnectionSuccess = response.data; - const simplifiedArray: string[] = data.map(i => `${i.ip}:${i.port}`); - - const insertedItems: string[] = difference( - simplifiedArray, - previousEntries, - ); - - const removedItems: string[] = difference( - previousEntries, - simplifiedArray, - ); - - insertedItems.forEach(i => println("CONNECTED: %s\n", i)); - removedItems.forEach(i => println("DISCONNECTED: %s\n", i)); - - previousEntries = simplifiedArray; - - await new Promise(i => setTimeout(i, pullRate)); - } - } else { - const response = await axios.post("/api/v1/forward/connections", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error requesting connections!\n"); - } - - return; - } - - const { data }: InboundConnectionSuccess = response.data; - - if (data.length == 0) { - println("There are currently no connected clients.\n"); - return; - } - - println( - "Connected clients (for source: %s:%s):\n", - data[0].connectionDetails.sourceIP, - data[0].connectionDetails.sourcePort, - ); - - for (const entry of data) { - println(" - %s:%s\n", entry.ip, entry.port); - } - } - }, - ); - - const removeTunnel = new SSHCommand(println, "rm"); - removeTunnel.description("Removes a tunnel"); - removeTunnel.argument("", "Tunnel ID to remove"); - - removeTunnel.action(async (idStr: string) => { - const id = parseInt(idStr); - - if (Number.isNaN(id)) { - println("ID (%s) is not a number\n", idStr); - return; - } - - const response = await axios.post("/api/v1/forward/remove", { - token, - id, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error deleting connection!\n"); - } - - return; - } - - println("Successfully deleted connection.\n"); - }); - - program.addCommand(addCommand); - program.addCommand(lookupCommand); - program.addCommand(startTunnel); - program.addCommand(stopTunnel); - program.addCommand(getInbound); - program.addCommand(removeTunnel); - - program.parse(argv); - await new Promise(resolve => program.onExit(resolve)); -} diff --git a/sshfrontend/src/commands/users.ts b/sshfrontend/src/commands/users.ts deleted file mode 100644 index 2c9452b..0000000 --- a/sshfrontend/src/commands/users.ts +++ /dev/null @@ -1,215 +0,0 @@ -import type { Axios } from "axios"; - -import { SSHCommand } from "../libs/patchCommander.js"; -import type { PrintLine, KeyboardRead } from "../commands.js"; - -type UserLookupSuccess = { - success: true; - data: { - id: number; - isServiceAccount: boolean; - username: string; - name: string; - email: string; - }[]; -}; - -export async function run( - argv: string[], - println: PrintLine, - axios: Axios, - apiKey: string, - readKeyboard: KeyboardRead, -) { - if (argv.length == 1) - return println( - "error: no arguments specified! run %s --help to see commands.\n", - argv[0], - ); - - const program = new SSHCommand(println); - program.description("Manages users for NextNet"); - program.version("v1.0.0"); - - const addCommand = new SSHCommand(println, "add"); - addCommand.description("Create a new user"); - addCommand.argument("", "Username of new user"); - addCommand.argument("", "Email of new user"); - addCommand.argument("[name]", "Name of new user (defaults to username)"); - - addCommand.option("-p, --password", "Password of User"); - addCommand.option( - "-a, --ask-password, --ask-pass, --askpass", - "Asks for a password. Hides output", - ); - - addCommand.option( - "-s, --service-account, --service", - "Turns the user into a service account", - ); - - addCommand.action( - async ( - username: string, - email: string, - name: string, - options: { - password?: string; - askPassword?: boolean; - isServiceAccount?: boolean; - }, - ) => { - if (!options.password && !options.askPassword) { - println("No password supplied, and askpass has not been supplied.\n"); - return; - } - - let password: string = ""; - - if (options.askPassword) { - let passwordConfirmOne = "a"; - let passwordConfirmTwo = "b"; - - while (passwordConfirmOne != passwordConfirmTwo) { - println("Password: "); - passwordConfirmOne = await readKeyboard(true); - - println("\nConfirm password: "); - passwordConfirmTwo = await readKeyboard(true); - - println("\n"); - - if (passwordConfirmOne != passwordConfirmTwo) { - println("Passwords do not match! Try again.\n\n"); - } - } - - password = passwordConfirmOne; - } else { - // @ts-expect-error: From the first check we do, we know this is safe (you MUST specify a password) - password = options.password; - } - - const response = await axios.post("/api/v1/users/create", { - name, - username, - email, - password, - - allowUnsafeGlobalTokens: options.isServiceAccount, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error creating users!\n"); - } - - return; - } - - println("User created successfully.\nToken: %s\n", response.data.token); - }, - ); - - const removeCommand = new SSHCommand(println, "rm"); - removeCommand.description("Remove a user"); - removeCommand.argument("", "ID of user to remove"); - - removeCommand.action(async (uidStr: string) => { - const uid = parseInt(uidStr); - - if (Number.isNaN(uid)) { - println("UID (%s) is not a number.\n", uid); - return; - } - - const response = await axios.post("/api/v1/users/remove", { - token: apiKey, - uid, - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error deleting user!\n"); - } - - return; - } - - println("User has been successfully deleted.\n"); - }); - - const lookupCommand = new SSHCommand(println, "find"); - lookupCommand.description("Find a user"); - lookupCommand.option("-i, --id ", "UID of User"); - lookupCommand.option("-n, --name ", "Name of User"); - lookupCommand.option("-u, --username ", "Username of User"); - lookupCommand.option("-e, --email ", "Email of User"); - lookupCommand.option("-s, --service", "The user is a service account"); - - lookupCommand.action(async options => { - // FIXME: redundant parseInt calls - - if (options.id) { - const uid = parseInt(options.id); - - if (Number.isNaN(uid)) { - println("UID (%s) is not a number.\n", uid); - return; - } - } - - const response = await axios.post("/api/v1/users/lookup", { - token: apiKey, - id: options.id ? parseInt(options.id) : undefined, - name: options.name, - username: options.username, - email: options.email, - service: Boolean(options.service), - }); - - if (response.status != 200) { - if (process.env.NODE_ENV != "production") console.log(response); - - if (response.data.error) { - println(`Error: ${response.data.error}\n`); - } else { - println("Error finding users!\n"); - } - - return; - } - - const { data }: UserLookupSuccess = response.data; - - for (const user of data) { - println( - "UID: %s%s:\n", - user.id, - user.isServiceAccount ? " (service)" : "", - ); - println("- Username: %s\n", user.username); - println("- Name: %s\n", user.name); - println("- Email: %s\n", user.email); - - println("\n"); - } - - println("%s users found.\n", data.length); - }); - - program.addCommand(addCommand); - program.addCommand(removeCommand); - program.addCommand(lookupCommand); - - program.parse(argv); - await new Promise(resolve => program.onExit(resolve)); -} diff --git a/sshfrontend/src/copyID.ts b/sshfrontend/src/copyID.ts deleted file mode 100644 index c306db4..0000000 --- a/sshfrontend/src/copyID.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { writeFile } from "node:fs/promises"; -import ssh2 from "ssh2"; - -import { readFromKeyboard } from "./libs/readFromKeyboard.js"; -import type { ClientKeys } from "./index.js"; - -export async function runCopyID( - username: string, - password: string, - keys: ClientKeys, - stream: ssh2.ServerChannel, -) { - stream.write( - "Hey there! I think you're using ssh-copy-id. If this is an error, you may close this terminal.\n", - ); - - stream.write("Please wait...\n"); - - const keyData = await readFromKeyboard(stream, true); - stream.write("Parsing key...\n"); - - const parsedKey = ssh2.utils.parseKey(keyData); - - if (parsedKey instanceof Error) { - stream.write(parsedKey.message + "\n"); - return stream.close(); - } - - stream.write("Passed checks. Writing changes...\n"); - - keys.push({ - username, - password, - publicKey: keyData, - }); - - try { - await writeFile("../keys/clients.json", JSON.stringify(keys, null, 2)); - } catch (e) { - console.log(e); - return stream.write( - "ERROR: Failed to save changes! If you're the administrator, view the console for details.\n", - ); - } - - stream.write("Success!\n"); - return stream.close(); -} diff --git a/sshfrontend/src/index.ts b/sshfrontend/src/index.ts deleted file mode 100644 index c2d93ef..0000000 --- a/sshfrontend/src/index.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { readFile, writeFile, mkdir } from "node:fs/promises"; -import { timingSafeEqual } from "node:crypto"; -import { format } from "node:util"; - -import parseArgsStringToArgv from "string-argv"; -import baseAxios from "axios"; -import ssh2 from "ssh2"; - -import { readFromKeyboard } from "./libs/readFromKeyboard.js"; -import { commands } from "./commands.js"; -import { runCopyID } from "./copyID.js"; - -export type ClientKeys = { - publicKey: string; - username: string; - password: string; -}[]; - -function checkValue(input: Buffer, allowed: Buffer): boolean { - const autoReject = input.length !== allowed.length; - if (autoReject) allowed = input; - - const isMatch = timingSafeEqual(input, allowed); - return !autoReject && isMatch; -} - -let serverKeyFile: Buffer | string | undefined; -let clientKeys: ClientKeys = []; - -const serverBaseURL: string = - process.env.SERVER_BASE_URL ?? "http://127.0.0.1:3000/"; - -const axios = baseAxios.create({ - baseURL: serverBaseURL, - validateStatus: () => true, -}); - -try { - clientKeys = JSON.parse(await readFile("../keys/clients.json", "utf8")); -} catch (e) { - console.log("INFO: We don't have the client key file."); -} - -try { - serverKeyFile = await readFile("../keys/host.key"); -} catch (e) { - console.log( - "ERROR: Failed to read the host key file! Creating new keypair...", - ); - await mkdir("../keys").catch(() => null); - - const keyPair: { private: string; public: string } = await new Promise( - resolve => - ssh2.utils.generateKeyPair("ed25519", (err, keyPair) => resolve(keyPair)), - ); - - await writeFile("../keys/host.key", keyPair.private); - await writeFile("../keys/host.pub", keyPair.public); - - serverKeyFile = keyPair.private; -} - -if (!serverKeyFile) throw new Error("Somehow failed to fetch the key file!"); - -const server: ssh2.Server = new ssh2.Server({ - hostKeys: [serverKeyFile], - banner: "NextNet-LOM (c) NextNet project et al.", -}); - -server.on("connection", client => { - let token: string = ""; - - let username: string = ""; - let password: string = ""; - - client.on("authentication", async auth => { - if (auth.method == "password") { - const response = await axios.post("/api/v1/users/login", { - username: auth.username, - password: auth.password, - }); - - if (response.status == 403) { - return auth.reject(["password", "publickey"]); - } - - token = response.data.token; - - username = auth.username; - password = auth.password; - - auth.accept(); - } else if (auth.method == "publickey") { - const userData = { - username: "", - password: "", - }; - - for (const rawKey of clientKeys) { - const key = ssh2.utils.parseKey(rawKey.publicKey); - - if (key instanceof Error) { - console.log(key); - continue; - } - - if ( - (rawKey.username == auth.username && - auth.key.algo == key.type && - checkValue(auth.key.data, key.getPublicSSH())) || - (auth.signature && - key.verify(auth.blob as Buffer, auth.signature, auth.key.algo)) - ) { - userData.username = rawKey.username; - userData.password = rawKey.password; - } - } - - if (!userData.username || !userData.password) - return auth.reject(["password", "publickey"]); - - const response = await axios.post("/api/v1/users/login", userData); - - if (response.status == 403) { - return auth.reject(["password", "publickey"]); - } - - token = response.data.token; - - username = userData.username; - password = userData.password; - - auth.accept(); - } else { - return auth.reject(["password", "publickey"]); - } - }); - - client.on("ready", () => { - client.on("session", accept => { - const conn = accept(); - - conn.on("exec", async (accept, reject, info) => { - const stream = accept(); - - if ( - info.command.includes(".ssh/authorized_keys") && - info.command.startsWith("exec sh -c") - ) { - return await runCopyID(username, password, clientKeys, stream); - } - - // Matches on ; and && - const commandsRecv = info.command.split(/;|&&/).map(i => i.trim()); - - function println(...data: unknown[]) { - stream.write(format(...data).replaceAll("\n", "\r\n")); - } - - for (const command of commandsRecv) { - const argv = parseArgsStringToArgv(command); - - if (argv[0] == "exit") { - stream.close(); - } else { - const command = commands.find(i => i.name == argv[0]); - - if (!command) { - stream.write(`Unknown command ${argv[0]}.\r\n`); - - continue; - } - - await command.run(argv, println, axios, token, disableEcho => - readFromKeyboard(stream, disableEcho), - ); - } - } - - return stream.close(); - }); - - // We're dumb. We don't really care. - conn.on("pty", accept => accept()); - conn.on("window-change", accept => { - if (typeof accept != "function") return; - accept(); - }); - - conn.on("shell", async accept => { - const stream = accept(); - stream.write( - "Welcome to NextNet LOM. Run 'help' to see commands.\r\n\r\n~$ ", - ); - - function println(...data: unknown[]) { - stream.write(format(...data).replaceAll("\n", "\r\n")); - } - - // FIXME (greysoh): wtf? this isn't setting correctly. - // @eslint-disable-next-line - while (true) { - const line = await readFromKeyboard(stream); - stream.write("\r\n"); - - if (line == "") { - stream.write(`~$ `); - continue; - } - - const argv = parseArgsStringToArgv(line); - - if (argv[0] == "exit") { - stream.close(); - } else { - const command = commands.find(i => i.name == argv[0]); - - if (!command) { - stream.write( - `Unknown command ${argv[0]}. Run 'help' to see commands.\r\n~$ `, - ); - - continue; - } - - await command.run(argv, println, axios, token, disableEcho => - readFromKeyboard(stream, disableEcho), - ); - stream.write("~$ "); - } - } - }); - }); - }); -}); - -server.listen( - 2222, - process.env.NODE_ENV == "production" ? "0.0.0.0" : "127.0.0.1", -); - -console.log("Started server at ::2222"); diff --git a/sshfrontend/src/libs/patchCommander.ts b/sshfrontend/src/libs/patchCommander.ts deleted file mode 100644 index f30e492..0000000 --- a/sshfrontend/src/libs/patchCommander.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Command, type ParseOptions } from "commander"; -import { PrintLine } from "../commands"; - -export class SSHCommand extends Command { - hasRecievedExitSignal: boolean; - println: PrintLine; - - exitEventHandlers: ((...any: unknown[]) => void)[]; - parent: SSHCommand | null; - - /** - * Modified version of the Commander command with slight automated patches, to work with our SSH environment. - * @param println PrintLine function to use - * @param name Optional field for the name of the command - */ - constructor( - println: PrintLine, - name?: string, - disableSSHHelpPatching: boolean = false, - ) { - super(name); - - this.exitEventHandlers = []; - - this.configureOutput({ - writeOut: str => println(str), - writeErr: str => { - if (this.hasRecievedExitSignal) return; - println(str); - }, - }); - - if (!disableSSHHelpPatching) { - const sshCommand = new SSHCommand(println, "help", true); - - sshCommand.description("display help for command"); - sshCommand.argument("[command]", "command to show help for"); - sshCommand.action(() => { - this.hasRecievedExitSignal = true; - - if (process.env.NODE_ENV != "production") { - println( - "Caught irrecoverable crash (command help call) in patchCommander\n", - ); - } else { - println("Aborted\n"); - } - }); - - this.addCommand(sshCommand); - } - } - - recvExitDispatch() { - this.hasRecievedExitSignal = true; - this.exitEventHandlers.forEach(eventHandler => eventHandler()); - - let parentElement = this.parent; - - while (parentElement instanceof SSHCommand) { - parentElement.hasRecievedExitSignal = true; - parentElement.exitEventHandlers.forEach(eventHandler => eventHandler()); - - parentElement = parentElement.parent; - } - } - - onExit(callback: (...any: any[]) => void) { - this.exitEventHandlers.push(callback); - if (this.hasRecievedExitSignal) callback(); - } - - _exit() { - this.recvExitDispatch(); - } - - _exitCallback() { - this.recvExitDispatch(); - } - - action(fn: (...args: any[]) => void | Promise): this { - super.action(fn); - - // @ts-expect-error: This parameter is private, but we need control over it. - // prettier-ignore - const oldActionHandler: (...args: any[]) => void | Promise = this._actionHandler; - - // @ts-expect-error: Overriding private parameters (but this works) - this._actionHandler = async (...args: any[]): Promise => { - if (this.hasRecievedExitSignal) return; - await oldActionHandler(...args); - - this.recvExitDispatch(); - }; - - return this; - } - - parse(argv?: readonly string[], options?: ParseOptions): this { - super.parse(["nextruntime", ...(argv ?? [])], options); - return this; - } - - createCommand(name: string) { - const command = new SSHCommand(this.println, name); - return command; - } -} diff --git a/sshfrontend/src/libs/readFromKeyboard.ts b/sshfrontend/src/libs/readFromKeyboard.ts deleted file mode 100644 index 9ac19ee..0000000 --- a/sshfrontend/src/libs/readFromKeyboard.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { ServerChannel } from "ssh2"; - -const pullRate = process.env.KEYBOARD_PULLING_RATE - ? parseInt(process.env.KEYBOARD_PULLING_RATE) - : 5; - -const leftEscape = "\x1B[D"; -const rightEscape = "\x1B[C"; - -const ourBackspace = "\u0008"; -const clientBackspace = "\x7F"; - -export async function readFromKeyboard( - stream: ServerChannel, - disableEcho: boolean = false, -): Promise { - let promise: (value: string | PromiseLike) => void; - - let line = ""; - let lineIndex = 0; - - async function eventLoop(): Promise { - const readStreamDataBuf = stream.read(); - if (readStreamDataBuf == null) return setTimeout(eventLoop, pullRate); - - const readStreamData = readStreamDataBuf.toString(); - - // Fixes several bugs (incl. potential social eng. exploits, ssh-copy-id being broken, etc) - for (const character of readStreamData.split("")) { - if (character == "\x03") { - stream.write("^C"); - return promise(""); - } else if (character == "\r" || character == "\n") { - return promise(line.replace("\r", "")); - } else if (character == clientBackspace) { - if (line.length == 0) return setTimeout(eventLoop, pullRate); // Here because if we do it in the parent if statement, shit breaks - line = line.substring(0, lineIndex - 1) + line.substring(lineIndex); - - if (!disableEcho) { - const deltaCursor = line.length - lineIndex; - - if (deltaCursor == line.length) - return setTimeout(eventLoop, pullRate); - - if (deltaCursor < 0) { - // Use old technique if the delta is < 0, as the new one is tailored to the start + 1 to end - 1 - stream.write(ourBackspace + " " + ourBackspace); - } else { - // Jump forward to the front, and remove the last character - stream.write(rightEscape.repeat(deltaCursor) + " " + ourBackspace); - - // Go backwards & rerender text & go backwards again (wtf?) - stream.write( - leftEscape.repeat(deltaCursor + 1) + - line.substring(lineIndex - 1) + - leftEscape.repeat(deltaCursor + 1), - ); - } - - lineIndex -= 1; - } - } else if (character == "\x1B") { - if (character == rightEscape) { - if (lineIndex + 1 > line.length) - return setTimeout(eventLoop, pullRate); - lineIndex += 1; - } else if (character == leftEscape) { - if (lineIndex - 1 < 0) return setTimeout(eventLoop, pullRate); - lineIndex -= 1; - } else { - return setTimeout(eventLoop, pullRate); - } - - if (!disableEcho) stream.write(character); - } else { - lineIndex += 1; - - // There isn't a splice method for String prototypes. So, ugh: - line = - line.substring(0, lineIndex - 1) + - character + - line.substring(lineIndex - 1); - - if (!disableEcho) { - let deltaCursor = line.length - lineIndex; - - // wtf? - if (deltaCursor < 0) { - console.log( - "FIXME: somehow, our deltaCursor value is negative! please investigate me", - ); - deltaCursor = 0; - } - - stream.write( - line.substring(lineIndex - 1) + leftEscape.repeat(deltaCursor), - ); - } - } - } - - setTimeout(eventLoop, pullRate); - } - - return new Promise(resolve => { - setTimeout(eventLoop, pullRate); - promise = resolve; - }); -} diff --git a/sshfrontend/tsconfig.json b/sshfrontend/tsconfig.json deleted file mode 100644 index 99ab26e..0000000 --- a/sshfrontend/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "target": "es2021", - "module": "es2022", - "moduleResolution": "node", - - "lib": [ - "es2021" - ], - - "outDir": "./out", - "rootDir": "./src", - - "strict": true, - "esModuleInterop": true, - "sourceMap": true, - - "declaration": true, - "declarationMap": true, - - "strictPropertyInitialization": false, - }, - - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] -} \ No newline at end of file From 4ca2c809c908a82ec0e3ad8c69d235551eb1bf5d Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 16:00:37 -0500 Subject: [PATCH 06/45] chore: Remove old TypeScript git hooks. --- .gitconfig | 2 -- .githooks/pre-commit | 18 ------------------ 2 files changed, 20 deletions(-) delete mode 100644 .gitconfig delete mode 100755 .githooks/pre-commit diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index 39e61fd..0000000 --- a/.gitconfig +++ /dev/null @@ -1,2 +0,0 @@ -[core] - hooksPath = .githooks/ \ No newline at end of file diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index ec1c700..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -shopt -s globstar -set -e - -ROOT="$(git rev-parse --show-toplevel)" - -pushd $ROOT/api -npx eslint src -popd - -pushd $ROOT/lom -npx eslint src -popd - -# Formatting step -$ROOT/api/node_modules/.bin/prettier --ignore-unknown --write $ROOT/{api,lom}/{eslint.config.js,src/**/*.ts} -git update-index --again -exit 0 \ No newline at end of file From 96833b238bdc6124807dec7b641a78126c239a41 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 28 Dec 2024 17:23:25 -0500 Subject: [PATCH 07/45] chore: Tidies Go module files. --- go.mod | 23 ++++++++++++++--------- go.sum | 27 ++++++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 360d142..4f83220 100644 --- a/go.mod +++ b/go.mod @@ -2,24 +2,32 @@ module git.terah.dev/imterah/hermes go 1.23.3 +require ( + github.com/charmbracelet/log v0.4.0 + github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/validator/v10 v10.23.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/urfave/cli/v2 v2.27.5 + golang.org/x/crypto v0.31.0 + gorm.io/driver/postgres v1.5.11 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.12 +) + require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bytedance/sonic v1.12.6 // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/charmbracelet/lipgloss v0.10.0 // indirect - github.com/charmbracelet/log v0.4.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/goccy/go-json v0.10.4 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -28,6 +36,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -39,13 +48,12 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/urfave/cli/v2 v2.27.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect @@ -53,7 +61,4 @@ require ( golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/postgres v1.5.11 // indirect - gorm.io/driver/sqlite v1.5.7 // indirect - gorm.io/gorm v1.25.12 // indirect ) diff --git a/go.sum b/go.sum index 8b2b29b..65feb3f 100644 --- a/go.sum +++ b/go.sum @@ -15,7 +15,9 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= @@ -25,6 +27,8 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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-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.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.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -35,6 +39,8 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -54,12 +60,14 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -78,11 +86,14 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -93,6 +104,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= @@ -104,8 +116,6 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= @@ -115,17 +125,17 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= @@ -136,4 +146,3 @@ gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDa gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 843cd34785d07adf042ad958adcd3023e044dab5 Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 5 Jan 2025 20:51:06 -0500 Subject: [PATCH 08/45] chore: Adds login and user creation support to the API. --- .gitignore | 4 +- apiclient/apiclient.go | 21 +++++ apiclient/backendstructs/struct.go | 102 ++++++++++++++++++++ apiclient/users/auth.go | 99 ++++++++++++++++++++ apiclient/users/create.go | 63 +++++++++++++ frontend/commands/users/create.go | 115 +++++++++++++++++++++++ frontend/commands/users/login.go | 98 ++++++++++++++++++++ frontend/config/config.go | 30 ++++++ frontend/dev.env | 1 + frontend/main.go | 143 +++++++++++++++++++++++++++++ go.mod | 5 +- go.sum | 8 +- init.sh | 6 +- 13 files changed, 683 insertions(+), 12 deletions(-) create mode 100644 apiclient/apiclient.go create mode 100644 apiclient/backendstructs/struct.go create mode 100644 apiclient/users/auth.go create mode 100644 apiclient/users/create.go create mode 100644 frontend/commands/users/create.go create mode 100644 frontend/commands/users/login.go create mode 100644 frontend/config/config.go create mode 100644 frontend/dev.env create mode 100644 frontend/main.go diff --git a/.gitignore b/.gitignore index f82cd86..cf34e00 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,11 @@ backend/sshbackend/sshbackend backend/dummybackend/dummybackend backend/externalbackendlauncher/externalbackendlauncher backend/api/api +frontend/frontend # Backup artifacts *.json.gz -# LOM -sshfrontend/keys - # Output out diff --git a/apiclient/apiclient.go b/apiclient/apiclient.go new file mode 100644 index 0000000..8f23a80 --- /dev/null +++ b/apiclient/apiclient.go @@ -0,0 +1,21 @@ +package apiclient + +import "git.terah.dev/imterah/hermes/apiclient/users" + +type HermesAPIClient struct { + URL string +} + +/// Users + +func (api *HermesAPIClient) UserGetRefreshToken(username *string, email *string, password string) (string, error) { + return users.GetRefreshToken(api.URL, username, email, password) +} + +func (api *HermesAPIClient) UserGetJWTFromToken(refreshToken string) (string, error) { + return users.GetJWTFromToken(api.URL, refreshToken) +} + +func (api *HermesAPIClient) UserCreate(fullName, username, email, password string, isBot bool) (string, error) { + return users.CreateUser(api.URL, fullName, username, email, password, isBot) +} diff --git a/apiclient/backendstructs/struct.go b/apiclient/backendstructs/struct.go new file mode 100644 index 0000000..be4b757 --- /dev/null +++ b/apiclient/backendstructs/struct.go @@ -0,0 +1,102 @@ +package backendstructs + +type BackendCreationRequest struct { + Token string `validate:"required"` + Name string `validate:"required"` + Description *string `json:"description"` + Backend string `validate:"required"` + BackendParameters interface{} `json:"connectionDetails" validate:"required"` +} + +type BackendLookupRequest struct { + Token string `validate:"required"` + BackendID *uint `json:"id"` + Name *string `json:"name"` + Description *string `json:"description"` + Backend *string `json:"backend"` +} + +type BackendRemovalRequest struct { + Token string `validate:"required"` + BackendID uint `json:"id" validate:"required"` +} + +type ConnectionsRequest struct { + Token string `validate:"required" json:"token"` + Id uint `validate:"required" json:"id"` +} + +type ProxyCreationRequest struct { + Token string `validate:"required" json:"token"` + Name string `validate:"required" json:"name"` + Description *string `json:"description"` + Protocol string `validate:"required" json:"protocol"` + SourceIP string `validate:"required" json:"sourceIP"` + SourcePort uint16 `validate:"required" json:"sourcePort"` + DestinationPort uint16 `validate:"required" json:"destinationPort"` + ProviderID uint `validate:"required" json:"providerID"` + AutoStart *bool `json:"autoStart"` +} + +type ProxyLookupRequest struct { + Token string `validate:"required" json:"token"` + Id *uint `json:"id"` + Name *string `json:"name"` + Description *string `json:"description"` + Protocol *string `json:"protocol"` + SourceIP *string `json:"sourceIP"` + SourcePort *uint16 `json:"sourcePort"` + DestinationPort *uint16 `json:"destPort"` + ProviderID *uint `json:"providerID"` + AutoStart *bool `json:"autoStart"` +} + +type ProxyRemovalRequest struct { + Token string `validate:"required" json:"token"` + ID uint `validate:"required" json:"id"` +} + +type ProxyStartRequest struct { + Token string `validate:"required" json:"token"` + ID uint `validate:"required" json:"id"` +} + +type ProxyStopRequest struct { + Token string `validate:"required" json:"token"` + ID uint `validate:"required" json:"id"` +} + +type UserCreationRequest struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Username string `json:"username" validate:"required"` + + ExistingUserToken string `json:"token"` + IsBot bool `json:"isBot"` +} + +type UserLoginRequest struct { + Username *string `json:"username"` + Email *string `json:"email"` + + Password string `json:"password" validate:"required"` +} + +type UserLookupRequest struct { + Token string `validate:"required"` + UID *uint `json:"id"` + Name *string `json:"name"` + Email *string `json:"email"` + Username *string `json:"username"` + IsBot *bool `json:"isServiceAccount"` +} + +type UserRefreshRequest struct { + Token string `json:"token" validate:"required"` +} + +type UserRemovalRequest struct { + Token string `json:"token" validate:"required"` + UID *uint `json:"uid"` +} diff --git a/apiclient/users/auth.go b/apiclient/users/auth.go new file mode 100644 index 0000000..91e7f67 --- /dev/null +++ b/apiclient/users/auth.go @@ -0,0 +1,99 @@ +package users + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "git.terah.dev/imterah/hermes/apiclient/backendstructs" +) + +type refreshTokenResponse struct { + Success bool `json:"success"` + RefreshToken string `json:"refreshToken"` +} + +type jwtTokenResponse struct { + Success bool `json:"success"` + JWT string `json:"token"` +} + +func GetRefreshToken(url string, username, email *string, password string) (string, error) { + body, err := json.Marshal(&backendstructs.UserLoginRequest{ + Username: username, + Email: email, + Password: password, + }) + + if err != nil { + return "", err + } + + res, err := http.Post(fmt.Sprintf("%s/api/v1/users/login", url), "application/json", bytes.NewBuffer(body)) + + if err != nil { + return "", err + } + + bodyContents, err := io.ReadAll(res.Body) + + if err != nil { + return "", fmt.Errorf("failed to read response body: %s", err.Error()) + } + + response := &refreshTokenResponse{} + + if err := json.Unmarshal(bodyContents, response); err != nil { + return "", err + } + + if !response.Success { + return "", fmt.Errorf("failed to get refresh token") + } + + if response.RefreshToken == "" { + return "", fmt.Errorf("refresh token is empty") + } + + return response.RefreshToken, nil +} + +func GetJWTFromToken(url, refreshToken string) (string, error) { + body, err := json.Marshal(&backendstructs.UserRefreshRequest{ + Token: refreshToken, + }) + + if err != nil { + return "", err + } + + res, err := http.Post(fmt.Sprintf("%s/api/v1/users/refresh", url), "application/json", bytes.NewBuffer(body)) + + if err != nil { + return "", err + } + + bodyContents, err := io.ReadAll(res.Body) + + if err != nil { + return "", fmt.Errorf("failed to read response body: %s", err.Error()) + } + + response := &jwtTokenResponse{} + + if err := json.Unmarshal(bodyContents, response); err != nil { + return "", err + } + + if !response.Success { + return "", fmt.Errorf("failed to get JWT token") + } + + if response.JWT == "" { + return "", fmt.Errorf("JWT token is empty") + } + + return response.JWT, nil +} diff --git a/apiclient/users/create.go b/apiclient/users/create.go new file mode 100644 index 0000000..6e03c58 --- /dev/null +++ b/apiclient/users/create.go @@ -0,0 +1,63 @@ +package users + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "git.terah.dev/imterah/hermes/apiclient/backendstructs" +) + +type createUserResponse struct { + Error string `json:"error"` + Success bool `json:"success"` + RefreshToken string `json:"refreshToken"` +} + +func CreateUser(url, fullName, username, email, password string, isBot bool) (string, error) { + body, err := json.Marshal(&backendstructs.UserCreationRequest{ + Username: username, + Name: fullName, + Email: email, + Password: password, + IsBot: isBot, + }) + + if err != nil { + return "", err + } + + res, err := http.Post(fmt.Sprintf("%s/api/v1/users/create", url), "application/json", bytes.NewBuffer(body)) + + if err != nil { + return "", err + } + + bodyContents, err := io.ReadAll(res.Body) + + if err != nil { + return "", fmt.Errorf("failed to read response body: %s", err.Error()) + } + + response := &createUserResponse{} + + if err := json.Unmarshal(bodyContents, response); err != nil { + return "", err + } + + if response.Error != "" { + return "", fmt.Errorf("error from server: %s", response.Error) + } + + if !response.Success { + return "", fmt.Errorf("failed to get refresh token") + } + + if response.RefreshToken == "" { + return "", fmt.Errorf("refresh token is empty") + } + + return response.RefreshToken, nil +} diff --git a/frontend/commands/users/create.go b/frontend/commands/users/create.go new file mode 100644 index 0000000..3d6c94b --- /dev/null +++ b/frontend/commands/users/create.go @@ -0,0 +1,115 @@ +package users + +import ( + "errors" + "fmt" + "os" + "syscall" + + "git.terah.dev/imterah/hermes/apiclient" + "git.terah.dev/imterah/hermes/frontend/config" + "github.com/charmbracelet/log" + "github.com/urfave/cli/v2" + "golang.org/x/term" + "gopkg.in/yaml.v3" +) + +func CreateUserCommand(cCtx *cli.Context) error { + configPath := cCtx.String("config-path") + + var configContents *config.Config + + _, err := os.Stat(configPath) + + if err != nil { + if errors.Is(err, os.ErrNotExist) { + configContents = &config.Config{} + } else { + return fmt.Errorf("failed to get configuration file information: %s", err.Error()) + } + } else { + configContents, err = config.ReadAndParseConfig(configPath) + + if err != nil { + return fmt.Errorf("failed to read and parse configuration file: %s", err.Error()) + } + } + + username := cCtx.String("username") + + if username == "" { + if configContents.Username == "" { + return fmt.Errorf("username not specified and username is not in the configuration file") + } + + username = configContents.Username + } + + var password string + + if cCtx.Bool("ask-password") { + fmt.Print("Password: ") + passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Print("\n") + + if err != nil { + return fmt.Errorf("failed to read password from console: %s", err.Error()) + } + + password = string(passwordBytes) + } else { + password = cCtx.String("password") + + if password == "" { + return fmt.Errorf("password is not specified and password asking is not enabled") + } + } + + var serverURL string + + if cCtx.String("server-url") == "" { + if configContents.APIPath == "" { + return fmt.Errorf("server URL not specified and server URL is not in the configuration file") + } + + serverURL = configContents.APIPath + } else { + serverURL = cCtx.String("server-url") + } + + fullName := cCtx.String("full-name") + email := cCtx.String("email") + isBot := cCtx.Bool("user-is-bot") + + log.Info("Creating user...") + + api := &apiclient.HermesAPIClient{ + URL: serverURL, + } + + refreshToken, err := api.UserCreate(fullName, username, email, password, isBot) + + if err != nil { + return fmt.Errorf("failed to create user: %s", err.Error()) + } + + log.Info("Successfully created user.") + + if cCtx.Bool("do-not-save-configuration") { + return nil + } + + configContents.Username = username + configContents.RefreshToken = refreshToken + configContents.APIPath = serverURL + + data, err := yaml.Marshal(configContents) + + if err != nil { + return fmt.Errorf("failed to marshal configuration data: %s", err.Error()) + } + + os.WriteFile(configPath, data, 0644) + + return nil +} diff --git a/frontend/commands/users/login.go b/frontend/commands/users/login.go new file mode 100644 index 0000000..8248b1f --- /dev/null +++ b/frontend/commands/users/login.go @@ -0,0 +1,98 @@ +package users + +import ( + "errors" + "fmt" + "os" + "syscall" + + "git.terah.dev/imterah/hermes/apiclient" + "git.terah.dev/imterah/hermes/frontend/config" + "github.com/charmbracelet/log" + "github.com/urfave/cli/v2" + "golang.org/x/term" + "gopkg.in/yaml.v3" +) + +func GetRefreshTokenCommand(cCtx *cli.Context) error { + configPath := cCtx.String("config-path") + + var configContents *config.Config + + _, err := os.Stat(configPath) + + if err != nil { + if errors.Is(err, os.ErrNotExist) { + configContents = &config.Config{} + } else { + return fmt.Errorf("failed to get configuration file information: %s", err.Error()) + } + } else { + configContents, err = config.ReadAndParseConfig(configPath) + + if err != nil { + return fmt.Errorf("failed to read and parse configuration file: %s", err.Error()) + } + } + + var username string + var password string + + if cCtx.String("username") == "" { + if configContents.Username == "" { + return fmt.Errorf("username not specified and username is not in the configuration file") + } + + username = configContents.Username + } else { + username = cCtx.String("username") + } + + if cCtx.Bool("ask-password") { + fmt.Print("Password: ") + passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Print("\n") + + if err != nil { + return fmt.Errorf("failed to read password from console: %s", err.Error()) + } + + password = string(passwordBytes) + } else { + password = cCtx.String("password") + + if password == "" { + return fmt.Errorf("password is not specified and password asking is not enabled") + } + } + + serverURL := cCtx.String("server-url") + log.Info("Authenticating with API...") + + api := &apiclient.HermesAPIClient{ + URL: serverURL, + } + + refreshToken, err := api.UserGetRefreshToken(&username, nil, password) + + if err != nil { + return fmt.Errorf("failed to authenticate with the API: %s", err.Error()) + } + + configContents.Username = username + configContents.RefreshToken = refreshToken + configContents.APIPath = serverURL + + log.Info("Writing configuration file...") + data, err := yaml.Marshal(configContents) + + if err != nil { + return fmt.Errorf("failed to marshal configuration data: %s", err.Error()) + } + + log.Infof("config path: %s", configPath) + + os.WriteFile(configPath, data, 0644) + + return nil +} diff --git a/frontend/config/config.go b/frontend/config/config.go new file mode 100644 index 0000000..ff9cb69 --- /dev/null +++ b/frontend/config/config.go @@ -0,0 +1,30 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Username string `json:"username"` + RefreshToken string `json:"token"` + APIPath string `json:"api_path"` +} + +func ReadAndParseConfig(configFile string) (*Config, error) { + configFileContents, err := os.ReadFile(configFile) + + if err != nil { + return nil, err + } + + config := &Config{} + err = yaml.Unmarshal(configFileContents, config) + + if err != nil { + return nil, err + } + + return config, nil +} diff --git a/frontend/dev.env b/frontend/dev.env new file mode 100644 index 0000000..ccd7c30 --- /dev/null +++ b/frontend/dev.env @@ -0,0 +1 @@ +HERMES_LOG_LEVEL=debug diff --git a/frontend/main.go b/frontend/main.go new file mode 100644 index 0000000..95b366f --- /dev/null +++ b/frontend/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "os" + "path" + + "git.terah.dev/imterah/hermes/frontend/commands/users" + "github.com/charmbracelet/log" + "github.com/urfave/cli/v2" +) + +func main() { + logLevel := os.Getenv("HERMES_LOG_LEVEL") + + if logLevel != "" { + switch logLevel { + case "debug": + log.SetLevel(log.DebugLevel) + + case "info": + log.SetLevel(log.InfoLevel) + + case "warn": + log.SetLevel(log.WarnLevel) + + case "error": + log.SetLevel(log.ErrorLevel) + + case "fatal": + log.SetLevel(log.FatalLevel) + } + } + + configDir, err := os.UserConfigDir() + + if err != nil { + log.Fatalf("Failed to get configuration directory: %s", err.Error()) + } + + app := &cli.App{ + Name: "hermcli", + Usage: "client for Hermes -- port forwarding across boundaries", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config-path", + Aliases: []string{"config", "cp", "c"}, + Value: path.Join(configDir, "hermcli.yml"), + }, + }, + Commands: []*cli.Command{ + { + Name: "login", + Usage: "log in to the API", + Action: users.GetRefreshTokenCommand, + Aliases: []string{"l"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"user", "u"}, + Usage: "username to authenticate as", + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"pass", "p"}, + Usage: "password to authenticate with", + }, + &cli.StringFlag{ + Name: "server-url", + Aliases: []string{"server", "s"}, + Usage: "URL of the server to authenticate with", + }, + &cli.BoolFlag{ + Name: "ask-password", + Aliases: []string{"ask-pass", "ap"}, + Usage: "asks you the password to authenticate with", + }, + }, + }, + { + Name: "users", + Usage: "user management commands", + Aliases: []string{"u"}, + Subcommands: []*cli.Command{ + { + Name: "create", + Aliases: []string{"c"}, + Usage: "create a user", + Action: users.CreateUserCommand, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "full-name", + Aliases: []string{"name", "n"}, + Usage: "full name for the user", + Required: true, + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"user", "us"}, + Usage: "username to give the user", + Required: true, + }, + &cli.StringFlag{ + Name: "email", + Aliases: []string{"e"}, + Usage: "email to give the user", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"pass", "p"}, + Usage: "password to give the user", + }, + &cli.StringFlag{ + Name: "server-url", + Aliases: []string{"server", "s"}, + Usage: "URL of the server to connect with", + }, + &cli.BoolFlag{ + Name: "ask-password", + Aliases: []string{"ask-pass", "ap"}, + Usage: "asks you the password to give the user", + }, + &cli.BoolFlag{ + Name: "user-is-bot", + Aliases: []string{"user-bot", "ub", "u"}, + Usage: "if set, makes the user flagged as a bot", + }, + &cli.BoolFlag{ + Name: "do-not-save-configuration", + Aliases: []string{"no-save", "ns"}, + Usage: "doesn't save the authenticated user credentials", + }, + }, + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index 4f83220..0942d36 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/urfave/cli/v2 v2.27.5 golang.org/x/crypto v0.31.0 + golang.org/x/term v0.28.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 @@ -57,8 +59,7 @@ require ( golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 65feb3f..da8d74e 100644 --- a/go.sum +++ b/go.sum @@ -125,10 +125,10 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= diff --git a/init.sh b/init.sh index 1fe457d..65aaee0 100644 --- a/init.sh +++ b/init.sh @@ -7,11 +7,11 @@ if [ ! -d "backend/.tmp" ]; then mkdir backend/.tmp fi -if [ ! -f "backend-legacy/.env" ]; then - cp backend-legacy/dev.env backend-legacy/.env +if [ ! -f "frontend/.env" ]; then + cp frontend/dev.env frontend/.env fi set -a -source backend-legacy/.env source backend/.env +source frontend/.env set +a From f505ff660535668ce1e0c8f4844da47d68275341 Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 5 Jan 2025 20:51:30 -0500 Subject: [PATCH 09/45] fix: Fixes SQLite not working with Docker. --- backend/Dockerfile | 2 +- backend/build.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index cd1c390..09859b5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,7 @@ FROM golang:latest AS build WORKDIR /build COPY . /build RUN bash build.sh -FROM busybox:stable AS run +FROM busybox:stable-glibc AS run WORKDIR /app COPY --from=build /build/backends.prod.json /app/backends.json COPY --from=build /build/api/api /app/hermes diff --git a/backend/build.sh b/backend/build.sh index 8753daf..ecb7537 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash pushd sshbackend -CGO_ENABLED=0 GOOS=linux go build . +GOOS=linux go build . strip sshbackend popd pushd dummybackend -CGO_ENABLED=0 GOOS=linux go build . +GOOS=linux go build . strip dummybackend popd @@ -15,6 +15,6 @@ strip externalbackendlauncher popd pushd api -CGO_ENABLED=0 GOOS=linux go build . +GOOS=linux go build . strip api popd From 605ad31dd626794659e7bfbe5e5ceb34a98c0950 Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 5 Jan 2025 23:45:44 -0500 Subject: [PATCH 10/45] feature: Adds semi-broken stability improvements into the runtime environment. --- backend/api/backendruntime/runtime.go | 224 ++++++++++++++++++++++---- backend/api/backendruntime/struct.go | 11 +- backend/api/backup.go | 5 + 3 files changed, 205 insertions(+), 35 deletions(-) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index df9ceb6..683b7f8 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -6,6 +6,7 @@ import ( "net" "os" "os/exec" + "strings" "time" "git.terah.dev/imterah/hermes/backend/backendlauncher" @@ -23,21 +24,21 @@ func init() { RunningBackends = make(map[uint]*Runtime) } -func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) { +func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) error { bytes, err := commonbackend.Marshal(commandType, command) if err != nil { log.Warnf("Failed to marshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to marshal message: %s", err.Error()) - return + return fmt.Errorf("failed to marshal message: %s", err.Error()) } if _, err := sock.Write(bytes); err != nil { log.Warnf("Failed to write message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to write message: %s", err.Error()) - return + return fmt.Errorf("failed to write message: %s", err.Error()) } _, data, err := commonbackend.Unmarshal(sock) @@ -46,10 +47,12 @@ func handleCommand(commandType string, command interface{}, sock net.Conn, rtcCh log.Warnf("Failed to unmarshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to unmarshal message: %s", err.Error()) - return + return fmt.Errorf("failed to unmarshal message: %s", err.Error()) } rtcChan <- data + + return nil } func (runtime *Runtime) goRoutineHandler() error { @@ -69,7 +72,7 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debugf("Acquired unix socket at: %s", sockPath) go func() { - log.Debug("Creating new goroutine for socket connection handling") + log.Debug("Created new Goroutine for socket connection handling") for { log.Debug("Waiting for Unix socket connections...") @@ -80,56 +83,206 @@ func (runtime *Runtime) goRoutineHandler() error { return } - log.Debug("Recieved connection. Initializing...") + log.Debug("Recieved connection. Attempting to figure out backend state...") - defer sock.Close() + timeoutChannel := time.After(500 * time.Millisecond) + select { + case <-timeoutChannel: + log.Debug("Timeout reached. Assuming backend is running.") + case hasRestarted, ok := <-runtime.processRestartNotification: + if !ok { + log.Warnf("Failed to get the process restart notification state!") + } + + if hasRestarted { + if runtime.OnCrashCallback == nil { + log.Warn("The backend has restarted for some reason, but we could not run the on crash callback as the callback is not set!") + } else { + log.Debug("We have restarted. Running the restart callback...") + runtime.OnCrashCallback(sock) + log.Debug("Finished running the restart callback.") + } + } else { + log.Debug("We have not restarted.") + } + } + + go func() { + log.Debugf("Setting up Hermes keepalive Goroutine") + hasFailedBackendRunningCheckAlready := false + + time.Sleep(time.Second * 1) + + for { + if !runtime.isRuntimeRunning { + return + } + + // Asking for the backend status seems to be a "good-enough" keepalive system. Plus, it provides useful telemetry. + // There isn't a ping command in the backend API, so we have to make do with what we have. + // + // To be safe here, we have to use the proper (yet annoying) facilities to prevent cross-talk, since we're in + // a goroutine, and can't talk directly. This actually has benefits, as the OuterLoop should exit on its own, if we + // encounter a critical error. + runtime.RuntimeCommands <- &commonbackend.BackendStatusRequest{ + Type: "backendStatusRequest", + } + + statusResponse := <-runtime.RuntimeCommands + + switch responseMessage := statusResponse.(type) { + case error: + log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", responseMessage.Error()) + log.Debugf("Attempting to close socket...") + err := sock.Close() + + if err != nil { + log.Debugf("Failed to close socket: %s", err.Error()) + } + case *commonbackend.BackendStatusResponse: + if !responseMessage.IsRunning { + if hasFailedBackendRunningCheckAlready { + if responseMessage.Message != "" { + log.Warnf("Backend (in backend keepalive) is up but not active: %s", responseMessage.Message) + } else { + log.Warnf("Backend (in backend keepalive) is up but not active") + } + } + + hasFailedBackendRunningCheckAlready = true + } + default: + log.Errorf("Got illegal response type for backend (in backend keepalive): %T", responseMessage) + log.Debugf("Attempting to close socket...") + err := sock.Close() + + if err != nil { + log.Debugf("Failed to close socket: %s", err.Error()) + } + } + + time.Sleep(5) + } + }() + + OuterLoop: for { commandRaw := <-runtime.RuntimeCommands - log.Debug("Got message from server") - switch command := commandRaw.(type) { case *commonbackend.AddProxy: - handleCommand("addProxy", command, sock, runtime.RuntimeCommands) + err := handleCommand("addProxy", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.BackendStatusRequest: - handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) - case *commonbackend.BackendStatusResponse: - handleCommand("backendStatusResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.CheckClientParameters: - handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) - case *commonbackend.CheckParametersResponse: - handleCommand("checkParametersResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.CheckServerParameters: - handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyClientConnection: - handleCommand("proxyClientConnection", command, sock, runtime.RuntimeCommands) + err := handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.ProxyConnectionsRequest: - handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyConnectionsResponse: - handleCommand("proxyConnectionsResponse", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyInstanceResponse: - handleCommand("proxyInstanceResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.ProxyInstanceRequest: - handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) + err := handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.ProxyStatusRequest: - handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyStatusResponse: - handleCommand("proxyStatusResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.RemoveProxy: - handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) + err := handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.Start: - handleCommand("start", command, sock, runtime.RuntimeCommands) + err := handleCommand("start", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.Stop: - handleCommand("stop", command, sock, runtime.RuntimeCommands) + err := handleCommand("stop", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } default: log.Warnf("Recieved unknown command type from channel: %q", command) runtime.RuntimeCommands <- fmt.Errorf("unknown command recieved") } } + + sock.Close() } }() + runtime.processRestartNotification <- false + for { log.Debug("Starting process...") @@ -161,6 +314,14 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debug("Sleeping 5 seconds, and then restarting process") time.Sleep(5 * time.Second) + + // NOTE(imterah): This could cause hangs if we're not careful. If the process dies so much that we can't keep up, it should deserve to be hung, really. + // There's probably a better way to do this, but this works. + // + // If this does turn out to be a problem, just increase the Goroutine buffer size. + runtime.processRestartNotification <- true + + log.Debug("Sent off notification.") } } @@ -170,6 +331,7 @@ func (runtime *Runtime) Start() error { } runtime.RuntimeCommands = make(chan interface{}) + runtime.processRestartNotification = make(chan bool, 1) runtime.logger = &writeLogger{ Runtime: runtime, diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index 68f1316..28a74b9 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -12,14 +12,17 @@ type Backend struct { } type Runtime struct { - isRuntimeRunning bool - logger *writeLogger - currentProcess *exec.Cmd - currentListener net.Listener + isRuntimeRunning bool + logger *writeLogger + currentProcess *exec.Cmd + currentListener net.Listener + processRestartNotification chan bool ProcessPath string Logs []string RuntimeCommands chan interface{} + + OnCrashCallback func(sock net.Conn) } type writeLogger struct { diff --git a/backend/api/backup.go b/backend/api/backup.go index 98b83ec..d5a9b86 100644 --- a/backend/api/backup.go +++ b/backend/api/backup.go @@ -67,16 +67,21 @@ type BackupData struct { } // From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text +// Strips all alphanumeric characters from a string func stripAllAlphanumeric(s string) string { var result strings.Builder + for i := 0; i < len(s); i++ { b := s[i] if ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') || ('0' <= b && b <= '9') { result.WriteByte(b) + } else { + result.WriteByte('_') } } + return result.String() } From 157e1c8712213bf11f61f934abaa47e173213abf Mon Sep 17 00:00:00 2001 From: imterah Date: Mon, 6 Jan 2025 00:04:35 -0500 Subject: [PATCH 11/45] Revert "feature: Adds semi-broken stability improvements into the runtime environment." This wasn't meant to be pushed to dev. This reverts commit 605ad31dd626794659e7bfbe5e5ceb34a98c0950. --- backend/api/backendruntime/runtime.go | 224 ++++---------------------- backend/api/backendruntime/struct.go | 11 +- backend/api/backup.go | 5 - 3 files changed, 35 insertions(+), 205 deletions(-) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index 683b7f8..df9ceb6 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -6,7 +6,6 @@ import ( "net" "os" "os/exec" - "strings" "time" "git.terah.dev/imterah/hermes/backend/backendlauncher" @@ -24,21 +23,21 @@ func init() { RunningBackends = make(map[uint]*Runtime) } -func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) error { +func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) { bytes, err := commonbackend.Marshal(commandType, command) if err != nil { log.Warnf("Failed to marshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to marshal message: %s", err.Error()) - return fmt.Errorf("failed to marshal message: %s", err.Error()) + return } if _, err := sock.Write(bytes); err != nil { log.Warnf("Failed to write message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to write message: %s", err.Error()) - return fmt.Errorf("failed to write message: %s", err.Error()) + return } _, data, err := commonbackend.Unmarshal(sock) @@ -47,12 +46,10 @@ func handleCommand(commandType string, command interface{}, sock net.Conn, rtcCh log.Warnf("Failed to unmarshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to unmarshal message: %s", err.Error()) - return fmt.Errorf("failed to unmarshal message: %s", err.Error()) + return } rtcChan <- data - - return nil } func (runtime *Runtime) goRoutineHandler() error { @@ -72,7 +69,7 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debugf("Acquired unix socket at: %s", sockPath) go func() { - log.Debug("Created new Goroutine for socket connection handling") + log.Debug("Creating new goroutine for socket connection handling") for { log.Debug("Waiting for Unix socket connections...") @@ -83,206 +80,56 @@ func (runtime *Runtime) goRoutineHandler() error { return } - log.Debug("Recieved connection. Attempting to figure out backend state...") + log.Debug("Recieved connection. Initializing...") - timeoutChannel := time.After(500 * time.Millisecond) + defer sock.Close() - select { - case <-timeoutChannel: - log.Debug("Timeout reached. Assuming backend is running.") - case hasRestarted, ok := <-runtime.processRestartNotification: - if !ok { - log.Warnf("Failed to get the process restart notification state!") - } - - if hasRestarted { - if runtime.OnCrashCallback == nil { - log.Warn("The backend has restarted for some reason, but we could not run the on crash callback as the callback is not set!") - } else { - log.Debug("We have restarted. Running the restart callback...") - runtime.OnCrashCallback(sock) - log.Debug("Finished running the restart callback.") - } - } else { - log.Debug("We have not restarted.") - } - } - - go func() { - log.Debugf("Setting up Hermes keepalive Goroutine") - hasFailedBackendRunningCheckAlready := false - - time.Sleep(time.Second * 1) - - for { - if !runtime.isRuntimeRunning { - return - } - - // Asking for the backend status seems to be a "good-enough" keepalive system. Plus, it provides useful telemetry. - // There isn't a ping command in the backend API, so we have to make do with what we have. - // - // To be safe here, we have to use the proper (yet annoying) facilities to prevent cross-talk, since we're in - // a goroutine, and can't talk directly. This actually has benefits, as the OuterLoop should exit on its own, if we - // encounter a critical error. - runtime.RuntimeCommands <- &commonbackend.BackendStatusRequest{ - Type: "backendStatusRequest", - } - - statusResponse := <-runtime.RuntimeCommands - - switch responseMessage := statusResponse.(type) { - case error: - log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", responseMessage.Error()) - log.Debugf("Attempting to close socket...") - err := sock.Close() - - if err != nil { - log.Debugf("Failed to close socket: %s", err.Error()) - } - case *commonbackend.BackendStatusResponse: - if !responseMessage.IsRunning { - if hasFailedBackendRunningCheckAlready { - if responseMessage.Message != "" { - log.Warnf("Backend (in backend keepalive) is up but not active: %s", responseMessage.Message) - } else { - log.Warnf("Backend (in backend keepalive) is up but not active") - } - } - - hasFailedBackendRunningCheckAlready = true - } - default: - log.Errorf("Got illegal response type for backend (in backend keepalive): %T", responseMessage) - log.Debugf("Attempting to close socket...") - err := sock.Close() - - if err != nil { - log.Debugf("Failed to close socket: %s", err.Error()) - } - } - - time.Sleep(5) - } - }() - - OuterLoop: for { commandRaw := <-runtime.RuntimeCommands + log.Debug("Got message from server") + switch command := commandRaw.(type) { case *commonbackend.AddProxy: - err := handleCommand("addProxy", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("addProxy", command, sock, runtime.RuntimeCommands) case *commonbackend.BackendStatusRequest: - err := handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.BackendStatusResponse: + handleCommand("backendStatusResponse", command, sock, runtime.RuntimeCommands) case *commonbackend.CheckClientParameters: - err := handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) + case *commonbackend.CheckParametersResponse: + handleCommand("checkParametersResponse", command, sock, runtime.RuntimeCommands) case *commonbackend.CheckServerParameters: - err := handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyClientConnection: + handleCommand("proxyClientConnection", command, sock, runtime.RuntimeCommands) case *commonbackend.ProxyConnectionsRequest: - err := handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyConnectionsResponse: + handleCommand("proxyConnectionsResponse", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyInstanceResponse: + handleCommand("proxyInstanceResponse", command, sock, runtime.RuntimeCommands) case *commonbackend.ProxyInstanceRequest: - err := handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) case *commonbackend.ProxyStatusRequest: - err := handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyStatusResponse: + handleCommand("proxyStatusResponse", command, sock, runtime.RuntimeCommands) case *commonbackend.RemoveProxy: - err := handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) case *commonbackend.Start: - err := handleCommand("start", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("start", command, sock, runtime.RuntimeCommands) case *commonbackend.Stop: - err := handleCommand("stop", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } + handleCommand("stop", command, sock, runtime.RuntimeCommands) default: log.Warnf("Recieved unknown command type from channel: %q", command) runtime.RuntimeCommands <- fmt.Errorf("unknown command recieved") } } - - sock.Close() } }() - runtime.processRestartNotification <- false - for { log.Debug("Starting process...") @@ -314,14 +161,6 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debug("Sleeping 5 seconds, and then restarting process") time.Sleep(5 * time.Second) - - // NOTE(imterah): This could cause hangs if we're not careful. If the process dies so much that we can't keep up, it should deserve to be hung, really. - // There's probably a better way to do this, but this works. - // - // If this does turn out to be a problem, just increase the Goroutine buffer size. - runtime.processRestartNotification <- true - - log.Debug("Sent off notification.") } } @@ -331,7 +170,6 @@ func (runtime *Runtime) Start() error { } runtime.RuntimeCommands = make(chan interface{}) - runtime.processRestartNotification = make(chan bool, 1) runtime.logger = &writeLogger{ Runtime: runtime, diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index 28a74b9..68f1316 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -12,17 +12,14 @@ type Backend struct { } type Runtime struct { - isRuntimeRunning bool - logger *writeLogger - currentProcess *exec.Cmd - currentListener net.Listener - processRestartNotification chan bool + isRuntimeRunning bool + logger *writeLogger + currentProcess *exec.Cmd + currentListener net.Listener ProcessPath string Logs []string RuntimeCommands chan interface{} - - OnCrashCallback func(sock net.Conn) } type writeLogger struct { diff --git a/backend/api/backup.go b/backend/api/backup.go index d5a9b86..98b83ec 100644 --- a/backend/api/backup.go +++ b/backend/api/backup.go @@ -67,21 +67,16 @@ type BackupData struct { } // From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text -// Strips all alphanumeric characters from a string func stripAllAlphanumeric(s string) string { var result strings.Builder - for i := 0; i < len(s); i++ { b := s[i] if ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') || ('0' <= b && b <= '9') { result.WriteByte(b) - } else { - result.WriteByte('_') } } - return result.String() } From 93f2f9cbee9c0253763309e6a3ce69417c688ad2 Mon Sep 17 00:00:00 2001 From: imterah Date: Mon, 6 Jan 2025 00:09:14 -0500 Subject: [PATCH 12/45] fix: Adds missing backend status command implementations. --- backend/backendutil/application.go | 36 ++++++++++++++++++++++++++++++ backend/backendutil/structure.go | 1 + backend/dummybackend/main.go | 4 ++++ backend/sshbackend/main.go | 4 ++++ 4 files changed, 45 insertions(+) diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index 5b99b2d..ccb21f2 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -72,6 +72,42 @@ func (helper *BackendApplicationHelper) Start() error { continue } + helper.socket.Write(responseMarshalled) + case "backendStatusRequest": + _, ok := commandRaw.(*commonbackend.BackendStatusRequest) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err := helper.Backend.GetBackendStatus() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + Type: "backendStatusResponse", + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response.Type, response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + helper.socket.Write(responseMarshalled) case "stop": _, ok := commandRaw.(*commonbackend.Stop) diff --git a/backend/backendutil/structure.go b/backend/backendutil/structure.go index 3b97466..0eb7116 100644 --- a/backend/backendutil/structure.go +++ b/backend/backendutil/structure.go @@ -5,6 +5,7 @@ import "git.terah.dev/imterah/hermes/backend/commonbackend" type BackendInterface interface { StartBackend(arguments []byte) (bool, error) StopBackend() (bool, error) + GetBackendStatus() (bool, error) StartProxy(command *commonbackend.AddProxy) (bool, error) StopProxy(command *commonbackend.RemoveProxy) (bool, error) GetAllClientConnections() []*commonbackend.ProxyClientConnection diff --git a/backend/dummybackend/main.go b/backend/dummybackend/main.go index 92d88f5..f28615c 100644 --- a/backend/dummybackend/main.go +++ b/backend/dummybackend/main.go @@ -19,6 +19,10 @@ func (backend *DummyBackend) StopBackend() (bool, error) { return true, nil } +func (backend *DummyBackend) GetBackendStatus() (bool, error) { + return true, nil +} + func (backend *DummyBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { return true, nil } diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index e96ed5e..1bf35e7 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -96,6 +96,10 @@ func (backend *SSHBackend) StopBackend() (bool, error) { return true, nil } +func (backend *SSHBackend) GetBackendStatus() (bool, error) { + return backend.conn != nil, nil +} + func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { listenerObject := &SSHListener{ SourceIP: command.SourceIP, From 1e1a330a4bf5e18fc4cfe93c96f384dbdff25bac Mon Sep 17 00:00:00 2001 From: imterah Date: Mon, 6 Jan 2025 01:24:11 -0500 Subject: [PATCH 13/45] feature: Refactors backend runtime's communication mechanism to be more stable. --- backend/api/backendruntime/core.go | 15 + backend/api/backendruntime/runtime.go | 261 +++++++++++------- backend/api/backendruntime/struct.go | 27 +- backend/api/controllers/v1/backends/create.go | 28 +- .../api/controllers/v1/proxies/connections.go | 14 +- backend/api/controllers/v1/proxies/create.go | 14 +- backend/api/controllers/v1/proxies/remove.go | 14 +- backend/api/controllers/v1/proxies/start.go | 6 +- backend/api/controllers/v1/proxies/stop.go | 14 +- backend/api/main.go | 18 +- 10 files changed, 254 insertions(+), 157 deletions(-) create mode 100644 backend/api/backendruntime/core.go diff --git a/backend/api/backendruntime/core.go b/backend/api/backendruntime/core.go new file mode 100644 index 0000000..e06a2a6 --- /dev/null +++ b/backend/api/backendruntime/core.go @@ -0,0 +1,15 @@ +package backendruntime + +import "os" + +var ( + AvailableBackends []*Backend + RunningBackends map[uint]*Runtime + TempDir string + isDevelopmentMode bool +) + +func init() { + RunningBackends = make(map[uint]*Runtime) + isDevelopmentMode = os.Getenv("HERMES_DEVELOPMENT_MODE") != "" +} diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index 683b7f8..e2359c2 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "strings" + "sync" "time" "git.terah.dev/imterah/hermes/backend/backendlauncher" @@ -14,16 +15,6 @@ import ( "github.com/charmbracelet/log" ) -var ( - AvailableBackends []*Backend - RunningBackends map[uint]*Runtime - TempDir string -) - -func init() { - RunningBackends = make(map[uint]*Runtime) -} - func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) error { bytes, err := commonbackend.Marshal(commandType, command) @@ -101,7 +92,9 @@ func (runtime *Runtime) goRoutineHandler() error { } else { log.Debug("We have restarted. Running the restart callback...") runtime.OnCrashCallback(sock) - log.Debug("Finished running the restart callback.") + log.Debug("Finished running the restart callback. Clearing caches...") + runtime.cleanUpPendingCommandProcessingJobs() + runtime.messageBufferLock = sync.Mutex{} } } else { log.Debug("We have not restarted.") @@ -112,8 +105,6 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debugf("Setting up Hermes keepalive Goroutine") hasFailedBackendRunningCheckAlready := false - time.Sleep(time.Second * 1) - for { if !runtime.isRuntimeRunning { return @@ -125,21 +116,23 @@ func (runtime *Runtime) goRoutineHandler() error { // To be safe here, we have to use the proper (yet annoying) facilities to prevent cross-talk, since we're in // a goroutine, and can't talk directly. This actually has benefits, as the OuterLoop should exit on its own, if we // encounter a critical error. - runtime.RuntimeCommands <- &commonbackend.BackendStatusRequest{ + statusResponse, err := runtime.ProcessCommand(&commonbackend.BackendStatusRequest{ Type: "backendStatusRequest", - } + }) - statusResponse := <-runtime.RuntimeCommands - - switch responseMessage := statusResponse.(type) { - case error: - log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", err.Error()) log.Debugf("Attempting to close socket...") err := sock.Close() if err != nil { log.Debugf("Failed to close socket: %s", err.Error()) } + + continue + } + + switch responseMessage := statusResponse.(type) { case *commonbackend.BackendStatusResponse: if !responseMessage.IsRunning { if hasFailedBackendRunningCheckAlready { @@ -168,112 +161,119 @@ func (runtime *Runtime) goRoutineHandler() error { OuterLoop: for { - commandRaw := <-runtime.RuntimeCommands - - switch command := commandRaw.(type) { - case *commonbackend.AddProxy: - err := handleCommand("addProxy", command, sock, runtime.RuntimeCommands) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } + for chanIndex, messageData := range runtime.messageBuffer { + if messageData == nil { + continue } - case *commonbackend.BackendStatusRequest: - err := handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + // We don't use the Mutex here as the Mutex is only to prevent cross-talk between multiple ProcessCommand() calls. + switch command := messageData.Message.(type) { + case *commonbackend.AddProxy: + err := handleCommand("addProxy", command, sock, messageData.Channel) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.CheckClientParameters: - err := handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) + case *commonbackend.BackendStatusRequest: + err := handleCommand("backendStatusRequest", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.CheckServerParameters: - err := handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) + case *commonbackend.CheckClientParameters: + err := handleCommand("checkClientParameters", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.ProxyConnectionsRequest: - err := handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.CheckServerParameters: + err := handleCommand("checkServerParameters", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.ProxyInstanceRequest: - err := handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyConnectionsRequest: + err := handleCommand("proxyConnectionsRequest", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.ProxyStatusRequest: - err := handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyInstanceRequest: + err := handleCommand("proxyInstanceRequest", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.RemoveProxy: - err := handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) + case *commonbackend.ProxyStatusRequest: + err := handleCommand("proxyStatusRequest", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.Start: - err := handleCommand("start", command, sock, runtime.RuntimeCommands) + case *commonbackend.RemoveProxy: + err := handleCommand("removeProxy", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } - } - case *commonbackend.Stop: - err := handleCommand("stop", command, sock, runtime.RuntimeCommands) + case *commonbackend.Start: + err := handleCommand("start", command, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } } + case *commonbackend.Stop: + err := handleCommand("stop", command, sock, messageData.Channel) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } + default: + log.Warnf("Recieved unknown command type from channel: %q", command) + messageData.Channel <- fmt.Errorf("unknown command recieved") } - default: - log.Warnf("Recieved unknown command type from channel: %q", command) - runtime.RuntimeCommands <- fmt.Errorf("unknown command recieved") + + runtime.messageBuffer[chanIndex] = nil } } @@ -330,7 +330,9 @@ func (runtime *Runtime) Start() error { return fmt.Errorf("runtime already running") } - runtime.RuntimeCommands = make(chan interface{}) + runtime.messageBuffer = make([]*messageForBuf, 10) + runtime.messageBufferLock = sync.Mutex{} + runtime.processRestartNotification = make(chan bool, 1) runtime.logger = &writeLogger{ @@ -379,6 +381,69 @@ func (runtime *Runtime) Stop() error { return nil } +func (runtime *Runtime) ProcessCommand(command interface{}) (interface{}, error) { + schedulingAttempts := 0 + var commandChannel chan interface{} + +SchedulingLoop: + for { + if !runtime.isRuntimeRunning { + time.Sleep(10 * time.Millisecond) + } + + if schedulingAttempts > 50 { + return nil, fmt.Errorf("failed to schedule synchronous message transmission after 50 tries with 100ms intervals (REPORT THIS ISSUE)") + } + + runtime.messageBufferLock.Lock() + + // Attempt to find spot in buffer to schedule message transmission + for i, message := range runtime.messageBuffer { + if message != nil { + continue + } + + commandChannel = make(chan interface{}) + + runtime.messageBuffer[i] = &messageForBuf{ + Channel: commandChannel, + Message: command, + } + + runtime.messageBufferLock.Unlock() + break SchedulingLoop + } + + runtime.messageBufferLock.Unlock() + time.Sleep(100 * time.Millisecond) + + schedulingAttempts++ + } + + // Fetch response and close Channel + response := <-commandChannel + close(commandChannel) + + err, ok := response.(error) + + if ok { + return nil, err + } + + return response, nil +} + +func (runtime *Runtime) cleanUpPendingCommandProcessingJobs() { + for messageIndex, message := range runtime.messageBuffer { + if message == nil { + continue + } + + close(message.Channel) + runtime.messageBuffer[messageIndex] = nil + } +} + func NewBackend(path string) *Runtime { return &Runtime{ ProcessPath: path, diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index 28a74b9..9d057ad 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -4,6 +4,9 @@ import ( "net" "os/exec" "strings" + "sync" + + "github.com/charmbracelet/log" ) type Backend struct { @@ -11,6 +14,11 @@ type Backend struct { Path string `validate:"required"` } +type messageForBuf struct { + Channel chan interface{} + Message interface{} +} + type Runtime struct { isRuntimeRunning bool logger *writeLogger @@ -18,9 +26,11 @@ type Runtime struct { currentListener net.Listener processRestartNotification chan bool - ProcessPath string - Logs []string - RuntimeCommands chan interface{} + messageBufferLock sync.Mutex + messageBuffer []*messageForBuf + + ProcessPath string + Logs []string OnCrashCallback func(sock net.Conn) } @@ -31,6 +41,17 @@ type writeLogger struct { func (writer writeLogger) Write(p []byte) (n int, err error) { logSplit := strings.Split(string(p), "\n") + + if isDevelopmentMode { + for _, logLine := range logSplit { + if logLine == "" { + continue + } + + log.Debug("spawned backend logs: " + logLine) + } + } + writer.Runtime.Logs = append(writer.Runtime.Logs, logSplit...) return len(p), err diff --git a/backend/api/controllers/v1/backends/create.go b/backend/api/controllers/v1/backends/create.go index 03c3509..0d8c614 100644 --- a/backend/api/controllers/v1/backends/create.go +++ b/backend/api/controllers/v1/backends/create.go @@ -125,16 +125,13 @@ func CreateBackend(c *gin.Context) { return } - backend.RuntimeCommands <- &commonbackend.CheckServerParameters{ + backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{ Type: "checkServerParameters", Arguments: backendParameters, - } + }) - backendParamCheckResponse := <-backend.RuntimeCommands - - switch responseMessage := backendParamCheckResponse.(type) { - case error: - log.Warnf("Failed to get response for backend: %s", responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend: %s", err.Error()) err = backend.Stop() @@ -147,6 +144,9 @@ func CreateBackend(c *gin.Context) { }) return + } + + switch responseMessage := backendParamCheckResponse.(type) { case *commonbackend.CheckParametersResponse: if responseMessage.InResponseTo != "checkServerParameters" { log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo) @@ -215,16 +215,13 @@ func CreateBackend(c *gin.Context) { return } - backend.RuntimeCommands <- &commonbackend.Start{ + backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{ Type: "start", Arguments: backendParameters, - } + }) - backendStartResponse := <-backend.RuntimeCommands - - switch responseMessage := backendStartResponse.(type) { - case error: - log.Warnf("Failed to get response for backend: %s", responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend: %s", err.Error()) err = backend.Stop() @@ -237,6 +234,9 @@ func CreateBackend(c *gin.Context) { }) return + } + + switch responseMessage := backendStartResponse.(type) { case *commonbackend.BackendStatusResponse: if !responseMessage.IsRunning { err = backend.Stop() diff --git a/backend/api/controllers/v1/proxies/connections.go b/backend/api/controllers/v1/proxies/connections.go index 9f1affa..baceb4e 100644 --- a/backend/api/controllers/v1/proxies/connections.go +++ b/backend/api/controllers/v1/proxies/connections.go @@ -118,19 +118,19 @@ func GetConnections(c *gin.Context) { return } - backendRuntime.RuntimeCommands <- &commonbackend.ProxyConnectionsRequest{ + backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{ Type: "proxyConnectionsRequest", - } + }) - backendResponse := <-backendRuntime.RuntimeCommands - - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend: %s", responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get status response from backend", }) + } + + switch responseMessage := backendResponse.(type) { case *commonbackend.ProxyConnectionsResponse: sanitizedConnections := []*SanitizedConnection{} diff --git a/backend/api/controllers/v1/proxies/create.go b/backend/api/controllers/v1/proxies/create.go index 1ac0bbc..b8f2256 100644 --- a/backend/api/controllers/v1/proxies/create.go +++ b/backend/api/controllers/v1/proxies/create.go @@ -139,25 +139,25 @@ func CreateProxy(c *gin.Context) { return } - backend.RuntimeCommands <- &commonbackend.AddProxy{ + backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, Protocol: proxy.Protocol, - } + }) - backendResponse := <-backend.RuntimeCommands - - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ "error": "failed to get response from backend", }) return + } + + switch responseMessage := backendResponse.(type) { case *commonbackend.ProxyStatusResponse: if !responseMessage.IsActive { log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID) diff --git a/backend/api/controllers/v1/proxies/remove.go b/backend/api/controllers/v1/proxies/remove.go index 4e476dd..8087e29 100644 --- a/backend/api/controllers/v1/proxies/remove.go +++ b/backend/api/controllers/v1/proxies/remove.go @@ -110,25 +110,25 @@ func RemoveProxy(c *gin.Context) { return } - backend.RuntimeCommands <- &commonbackend.RemoveProxy{ + backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ Type: "removeProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, Protocol: proxy.Protocol, - } + }) - backendResponse := <-backend.RuntimeCommands - - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get response from backend. Proxy was still successfully deleted", }) return + } + + switch responseMessage := backendResponse.(type) { case *commonbackend.ProxyStatusResponse: if responseMessage.IsActive { c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/backend/api/controllers/v1/proxies/start.go b/backend/api/controllers/v1/proxies/start.go index 2647277..d0cd5e0 100644 --- a/backend/api/controllers/v1/proxies/start.go +++ b/backend/api/controllers/v1/proxies/start.go @@ -100,15 +100,13 @@ func StartProxy(c *gin.Context) { return } - backend.RuntimeCommands <- &commonbackend.AddProxy{ + backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, Protocol: proxy.Protocol, - } - - backendResponse := <-backend.RuntimeCommands + }) switch responseMessage := backendResponse.(type) { case error: diff --git a/backend/api/controllers/v1/proxies/stop.go b/backend/api/controllers/v1/proxies/stop.go index 474228a..1f5f525 100644 --- a/backend/api/controllers/v1/proxies/stop.go +++ b/backend/api/controllers/v1/proxies/stop.go @@ -100,25 +100,25 @@ func StopProxy(c *gin.Context) { return } - backend.RuntimeCommands <- &commonbackend.RemoveProxy{ + backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ Type: "removeProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, Protocol: proxy.Protocol, - } + }) - backendResponse := <-backend.RuntimeCommands - - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ "error": "failed to get response from backend", }) return + } + + switch responseMessage := backendResponse.(type) { case *commonbackend.ProxyStatusResponse: if responseMessage.IsActive { c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/backend/api/main.go b/backend/api/main.go index c233d06..81fc2ba 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -112,12 +112,10 @@ func apiEntrypoint(cCtx *cli.Context) error { continue } - backendInstance.RuntimeCommands <- &commonbackend.Start{ + backendStartResponse, err := backendInstance.ProcessCommand(&commonbackend.Start{ Type: "start", Arguments: backendParameters, - } - - backendStartResponse := <-backendInstance.RuntimeCommands + }) switch responseMessage := backendStartResponse.(type) { case error: @@ -165,20 +163,20 @@ func apiEntrypoint(cCtx *cli.Context) error { for _, proxy := range autoStartProxies { log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) - backendInstance.RuntimeCommands <- &commonbackend.AddProxy{ + backendResponse, err := backendInstance.ProcessCommand(&commonbackend.AddProxy{ Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, Protocol: proxy.Protocol, + }) + + if err != nil { + log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) + continue } - backendResponse := <-backendInstance.RuntimeCommands - switch responseMessage := backendResponse.(type) { - case error: - log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, responseMessage.Error()) - continue case *commonbackend.ProxyStatusResponse: if !responseMessage.IsActive { log.Warnf("Failed to start proxy for backend #%d and route #%d", proxy.BackendID, proxy.ID) From f8d32fb1c6fd319446886a61bc9983ce8216c53a Mon Sep 17 00:00:00 2001 From: imterah Date: Wed, 8 Jan 2025 09:12:48 -0500 Subject: [PATCH 14/45] fix: Fixes more instability issues. :) --- .devcontainer/devcontainer.json | 18 ++-- backend/api/backendruntime/runtime.go | 30 ++++-- .../api/controllers/v1/proxies/connections.go | 2 + backend/api/controllers/v1/proxies/create.go | 2 + backend/api/controllers/v1/proxies/lookup.go | 2 + backend/api/main.go | 99 ++++++++++++++++++- backend/commonbackend/marshal.go | 2 +- routes/Hermes API/Backend/Lookup.bru | 4 +- 8 files changed, 132 insertions(+), 27 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9abd7c6..5e6cec4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,16 +13,14 @@ }, // run arguments passed to docker - "runArgs": [ - "--security-opt", "label=disable" - ], + "runArgs": ["--security-opt", "label=disable"], "containerEnv": { - // extensions to preload before other extensions + // extensions to preload before other extensions "PRELOAD_EXTENSIONS": "arrterian.nix-env-selector" }, - // disable command overriding and updating remote user ID + // disable command overriding and updating remote user ID "overrideCommand": false, "userEnvProbe": "loginShell", "updateRemoteUserUID": false, @@ -31,18 +29,14 @@ "onCreateCommand": "nix-shell --command 'echo done building nix dev environment'", // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [ - 3000 - ], + "forwardPorts": [8000], "customizations": { "vscode": { - "extensions": [ - "arrterian.nix-env-selector" - ] + "extensions": ["arrterian.nix-env-selector"] } } // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "go version", -} \ No newline at end of file +} diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index e2359c2..1e79406 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -92,17 +92,18 @@ func (runtime *Runtime) goRoutineHandler() error { } else { log.Debug("We have restarted. Running the restart callback...") runtime.OnCrashCallback(sock) - log.Debug("Finished running the restart callback. Clearing caches...") - runtime.cleanUpPendingCommandProcessingJobs() - runtime.messageBufferLock = sync.Mutex{} } + + log.Debug("Clearing caches...") + runtime.cleanUpPendingCommandProcessingJobs() + runtime.messageBufferLock = sync.Mutex{} } else { log.Debug("We have not restarted.") } } go func() { - log.Debugf("Setting up Hermes keepalive Goroutine") + log.Debug("Setting up Hermes keepalive Goroutine") hasFailedBackendRunningCheckAlready := false for { @@ -155,7 +156,7 @@ func (runtime *Runtime) goRoutineHandler() error { } } - time.Sleep(5) + time.Sleep(5 * time.Second) } }() @@ -166,7 +167,6 @@ func (runtime *Runtime) goRoutineHandler() error { continue } - // We don't use the Mutex here as the Mutex is only to prevent cross-talk between multiple ProcessCommand() calls. switch command := messageData.Message.(type) { case *commonbackend.AddProxy: err := handleCommand("addProxy", command, sock, messageData.Channel) @@ -269,7 +269,7 @@ func (runtime *Runtime) goRoutineHandler() error { } } default: - log.Warnf("Recieved unknown command type from channel: %q", command) + log.Warnf("Recieved unknown command type from channel: %T", command) messageData.Channel <- fmt.Errorf("unknown command recieved") } @@ -392,7 +392,7 @@ SchedulingLoop: } if schedulingAttempts > 50 { - return nil, fmt.Errorf("failed to schedule synchronous message transmission after 50 tries with 100ms intervals (REPORT THIS ISSUE)") + return nil, fmt.Errorf("failed to schedule message transmission after 50 tries (REPORT THIS ISSUE)") } runtime.messageBufferLock.Lock() @@ -439,7 +439,19 @@ func (runtime *Runtime) cleanUpPendingCommandProcessingJobs() { continue } - close(message.Channel) + timeoutChannel := time.After(100 * time.Millisecond) + + select { + case <-timeoutChannel: + log.Fatal("Message channel is likely running (timed out reading from it without an error)") + close(message.Channel) + case _, ok := <-message.Channel: + if ok { + log.Fatal("Message channel is running, but should be stopped (since message is NOT nil!)") + close(message.Channel) + } + } + runtime.messageBuffer[messageIndex] = nil } } diff --git a/backend/api/controllers/v1/proxies/connections.go b/backend/api/controllers/v1/proxies/connections.go index baceb4e..f46c284 100644 --- a/backend/api/controllers/v1/proxies/connections.go +++ b/backend/api/controllers/v1/proxies/connections.go @@ -128,6 +128,8 @@ func GetConnections(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get status response from backend", }) + + return } switch responseMessage := backendResponse.(type) { diff --git a/backend/api/controllers/v1/proxies/create.go b/backend/api/controllers/v1/proxies/create.go index b8f2256..20ad144 100644 --- a/backend/api/controllers/v1/proxies/create.go +++ b/backend/api/controllers/v1/proxies/create.go @@ -123,6 +123,8 @@ func CreateProxy(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to add forward rule to database", }) + + return } if autoStart { diff --git a/backend/api/controllers/v1/proxies/lookup.go b/backend/api/controllers/v1/proxies/lookup.go index 5f40698..3b0d4c3 100644 --- a/backend/api/controllers/v1/proxies/lookup.go +++ b/backend/api/controllers/v1/proxies/lookup.go @@ -95,6 +95,8 @@ func LookupProxy(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{ "error": "Protocol specified in body must either be 'tcp' or 'udp'", }) + + return } } diff --git a/backend/api/main.go b/backend/api/main.go index 81fc2ba..2969e9b 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net" "os" "path" "path/filepath" @@ -98,6 +99,96 @@ func apiEntrypoint(cCtx *cli.Context) error { } backendInstance := backendruntime.NewBackend(backendRuntimeFilePath) + + backendInstance.OnCrashCallback = func(conn net.Conn) { + backendParameters, err := base64.StdEncoding.DecodeString(backend.BackendParameters) + + if err != nil { + log.Errorf("Failed to decode backend parameters for backend #%d: %s", backend.ID, err.Error()) + return + } + + marshalledStartCommand, err := commonbackend.Marshal("start", &commonbackend.Start{ + Type: "start", + Arguments: backendParameters, + }) + + if err != nil { + log.Errorf("Failed to marshal start command for backend #%d: %s", backend.ID, err.Error()) + return + } + + if _, err := conn.Write(marshalledStartCommand); err != nil { + log.Errorf("Failed to send start command for backend #%d: %s", backend.ID, err.Error()) + return + } + + _, backendResponse, err := commonbackend.Unmarshal(conn) + + if err != nil { + log.Errorf("Failed to get start command response for backend #%d: %s", backend.ID, err.Error()) + return + } + + switch responseMessage := backendResponse.(type) { + case *commonbackend.BackendStatusResponse: + if !responseMessage.IsRunning { + log.Errorf("Failed to start backend #%d: %s", backend.ID, responseMessage.Message) + return + } + + log.Infof("Backend #%d has been reinitialized successfully", backend.ID) + } + + log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID) + + autoStartProxies := []dbcore.Proxy{} + + if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { + log.Errorf("Failed to query proxies to autostart: %s", err.Error()) + return + } + + for _, proxy := range autoStartProxies { + log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) + + marhalledCommand, err := commonbackend.Marshal("addProxy", &commonbackend.AddProxy{ + Type: "addProxy", + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, + }) + + if err != nil { + log.Errorf("Failed to marshal proxy adding request for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) + continue + } + + if _, err := conn.Write(marhalledCommand); err != nil { + log.Errorf("Failed to send proxy adding request for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) + continue + } + + _, backendResponse, err := commonbackend.Unmarshal(conn) + + if err != nil { + log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) + continue + } + + switch responseMessage := backendResponse.(type) { + case *commonbackend.ProxyStatusResponse: + if !responseMessage.IsActive { + log.Warnf("Failed to start proxy for backend #%d and route #%d", proxy.BackendID, proxy.ID) + } + default: + log.Errorf("Got illegal response type for backend #%d and proxy #%d: %T", proxy.BackendID, proxy.ID, responseMessage) + continue + } + } + } + err = backendInstance.Start() if err != nil { @@ -117,9 +208,8 @@ func apiEntrypoint(cCtx *cli.Context) error { Arguments: backendParameters, }) - switch responseMessage := backendStartResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", backend.ID, responseMessage.Error()) + if err != nil { + log.Warnf("Failed to get response for backend #%d: %s", backend.ID, err.Error()) err = backendInstance.Stop() @@ -128,6 +218,9 @@ func apiEntrypoint(cCtx *cli.Context) error { } continue + } + + switch responseMessage := backendStartResponse.(type) { case *commonbackend.BackendStatusResponse: if !responseMessage.IsRunning { err = backendInstance.Stop() diff --git a/backend/commonbackend/marshal.go b/backend/commonbackend/marshal.go index 31895de..6baf02e 100644 --- a/backend/commonbackend/marshal.go +++ b/backend/commonbackend/marshal.go @@ -346,7 +346,7 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { return nil, fmt.Errorf("failed to typecast") } - statusRequestBytes := make([]byte, 2) + statusRequestBytes := make([]byte, 1) statusRequestBytes[0] = BackendStatusRequestID return statusRequestBytes, nil diff --git a/routes/Hermes API/Backend/Lookup.bru b/routes/Hermes API/Backend/Lookup.bru index 0da7fa7..8c50b52 100644 --- a/routes/Hermes API/Backend/Lookup.bru +++ b/routes/Hermes API/Backend/Lookup.bru @@ -12,8 +12,8 @@ post { body:json { { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzUwNzY0MTEsIm5iZiI6MTczNDk5MDAxMSwiaWF0IjoxNzM0OTkwMDExfQ.N9TLraX4peHt7FKv8tPcHuEzL0K7T2IBEw3piQS_4OY", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiMSJdLCJleHAiOjE3MzYyMzI2NjEsIm5iZiI6MTczNjE0NjI2MSwiaWF0IjoxNzM2MTQ2MjYxfQ.juoZ74xs-FBnbbT9Zlei1LmcNx7kTEfzymHlVbeMmtQ", "name": "SSH", - "id": 2 + "id": 1 } } From f24daabe4527ee89dfc362c4723d8a0dd7879d0e Mon Sep 17 00:00:00 2001 From: imterah Date: Wed, 8 Jan 2025 11:23:05 -0500 Subject: [PATCH 15/45] Reapply "feature: Adds semi-broken stability improvements into the runtime environment." This reverts commit 157e1c8712213bf11f61f934abaa47e173213abf. --- backend/api/backendruntime/runtime.go | 224 ++++++++++++++++++++++---- backend/api/backendruntime/struct.go | 11 +- backend/api/backup.go | 5 + 3 files changed, 205 insertions(+), 35 deletions(-) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index df9ceb6..683b7f8 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -6,6 +6,7 @@ import ( "net" "os" "os/exec" + "strings" "time" "git.terah.dev/imterah/hermes/backend/backendlauncher" @@ -23,21 +24,21 @@ func init() { RunningBackends = make(map[uint]*Runtime) } -func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) { +func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) error { bytes, err := commonbackend.Marshal(commandType, command) if err != nil { log.Warnf("Failed to marshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to marshal message: %s", err.Error()) - return + return fmt.Errorf("failed to marshal message: %s", err.Error()) } if _, err := sock.Write(bytes); err != nil { log.Warnf("Failed to write message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to write message: %s", err.Error()) - return + return fmt.Errorf("failed to write message: %s", err.Error()) } _, data, err := commonbackend.Unmarshal(sock) @@ -46,10 +47,12 @@ func handleCommand(commandType string, command interface{}, sock net.Conn, rtcCh log.Warnf("Failed to unmarshal message: %s", err.Error()) rtcChan <- fmt.Errorf("failed to unmarshal message: %s", err.Error()) - return + return fmt.Errorf("failed to unmarshal message: %s", err.Error()) } rtcChan <- data + + return nil } func (runtime *Runtime) goRoutineHandler() error { @@ -69,7 +72,7 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debugf("Acquired unix socket at: %s", sockPath) go func() { - log.Debug("Creating new goroutine for socket connection handling") + log.Debug("Created new Goroutine for socket connection handling") for { log.Debug("Waiting for Unix socket connections...") @@ -80,56 +83,206 @@ func (runtime *Runtime) goRoutineHandler() error { return } - log.Debug("Recieved connection. Initializing...") + log.Debug("Recieved connection. Attempting to figure out backend state...") - defer sock.Close() + timeoutChannel := time.After(500 * time.Millisecond) + select { + case <-timeoutChannel: + log.Debug("Timeout reached. Assuming backend is running.") + case hasRestarted, ok := <-runtime.processRestartNotification: + if !ok { + log.Warnf("Failed to get the process restart notification state!") + } + + if hasRestarted { + if runtime.OnCrashCallback == nil { + log.Warn("The backend has restarted for some reason, but we could not run the on crash callback as the callback is not set!") + } else { + log.Debug("We have restarted. Running the restart callback...") + runtime.OnCrashCallback(sock) + log.Debug("Finished running the restart callback.") + } + } else { + log.Debug("We have not restarted.") + } + } + + go func() { + log.Debugf("Setting up Hermes keepalive Goroutine") + hasFailedBackendRunningCheckAlready := false + + time.Sleep(time.Second * 1) + + for { + if !runtime.isRuntimeRunning { + return + } + + // Asking for the backend status seems to be a "good-enough" keepalive system. Plus, it provides useful telemetry. + // There isn't a ping command in the backend API, so we have to make do with what we have. + // + // To be safe here, we have to use the proper (yet annoying) facilities to prevent cross-talk, since we're in + // a goroutine, and can't talk directly. This actually has benefits, as the OuterLoop should exit on its own, if we + // encounter a critical error. + runtime.RuntimeCommands <- &commonbackend.BackendStatusRequest{ + Type: "backendStatusRequest", + } + + statusResponse := <-runtime.RuntimeCommands + + switch responseMessage := statusResponse.(type) { + case error: + log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", responseMessage.Error()) + log.Debugf("Attempting to close socket...") + err := sock.Close() + + if err != nil { + log.Debugf("Failed to close socket: %s", err.Error()) + } + case *commonbackend.BackendStatusResponse: + if !responseMessage.IsRunning { + if hasFailedBackendRunningCheckAlready { + if responseMessage.Message != "" { + log.Warnf("Backend (in backend keepalive) is up but not active: %s", responseMessage.Message) + } else { + log.Warnf("Backend (in backend keepalive) is up but not active") + } + } + + hasFailedBackendRunningCheckAlready = true + } + default: + log.Errorf("Got illegal response type for backend (in backend keepalive): %T", responseMessage) + log.Debugf("Attempting to close socket...") + err := sock.Close() + + if err != nil { + log.Debugf("Failed to close socket: %s", err.Error()) + } + } + + time.Sleep(5) + } + }() + + OuterLoop: for { commandRaw := <-runtime.RuntimeCommands - log.Debug("Got message from server") - switch command := commandRaw.(type) { case *commonbackend.AddProxy: - handleCommand("addProxy", command, sock, runtime.RuntimeCommands) + err := handleCommand("addProxy", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.BackendStatusRequest: - handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) - case *commonbackend.BackendStatusResponse: - handleCommand("backendStatusResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("backendStatusRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.CheckClientParameters: - handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) - case *commonbackend.CheckParametersResponse: - handleCommand("checkParametersResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("checkClientParameters", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.CheckServerParameters: - handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyClientConnection: - handleCommand("proxyClientConnection", command, sock, runtime.RuntimeCommands) + err := handleCommand("checkServerParameters", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.ProxyConnectionsRequest: - handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyConnectionsResponse: - handleCommand("proxyConnectionsResponse", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyInstanceResponse: - handleCommand("proxyInstanceResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("proxyConnectionsRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.ProxyInstanceRequest: - handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) + err := handleCommand("proxyInstanceRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.ProxyStatusRequest: - handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) - case *commonbackend.ProxyStatusResponse: - handleCommand("proxyStatusResponse", command, sock, runtime.RuntimeCommands) + err := handleCommand("proxyStatusRequest", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.RemoveProxy: - handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) + err := handleCommand("removeProxy", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.Start: - handleCommand("start", command, sock, runtime.RuntimeCommands) + err := handleCommand("start", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } case *commonbackend.Stop: - handleCommand("stop", command, sock, runtime.RuntimeCommands) + err := handleCommand("stop", command, sock, runtime.RuntimeCommands) + + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop + } + } default: log.Warnf("Recieved unknown command type from channel: %q", command) runtime.RuntimeCommands <- fmt.Errorf("unknown command recieved") } } + + sock.Close() } }() + runtime.processRestartNotification <- false + for { log.Debug("Starting process...") @@ -161,6 +314,14 @@ func (runtime *Runtime) goRoutineHandler() error { log.Debug("Sleeping 5 seconds, and then restarting process") time.Sleep(5 * time.Second) + + // NOTE(imterah): This could cause hangs if we're not careful. If the process dies so much that we can't keep up, it should deserve to be hung, really. + // There's probably a better way to do this, but this works. + // + // If this does turn out to be a problem, just increase the Goroutine buffer size. + runtime.processRestartNotification <- true + + log.Debug("Sent off notification.") } } @@ -170,6 +331,7 @@ func (runtime *Runtime) Start() error { } runtime.RuntimeCommands = make(chan interface{}) + runtime.processRestartNotification = make(chan bool, 1) runtime.logger = &writeLogger{ Runtime: runtime, diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index 68f1316..28a74b9 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -12,14 +12,17 @@ type Backend struct { } type Runtime struct { - isRuntimeRunning bool - logger *writeLogger - currentProcess *exec.Cmd - currentListener net.Listener + isRuntimeRunning bool + logger *writeLogger + currentProcess *exec.Cmd + currentListener net.Listener + processRestartNotification chan bool ProcessPath string Logs []string RuntimeCommands chan interface{} + + OnCrashCallback func(sock net.Conn) } type writeLogger struct { diff --git a/backend/api/backup.go b/backend/api/backup.go index 98b83ec..d5a9b86 100644 --- a/backend/api/backup.go +++ b/backend/api/backup.go @@ -67,16 +67,21 @@ type BackupData struct { } // From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text +// Strips all alphanumeric characters from a string func stripAllAlphanumeric(s string) string { var result strings.Builder + for i := 0; i < len(s); i++ { b := s[i] if ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z') || ('0' <= b && b <= '9') { result.WriteByte(b) + } else { + result.WriteByte('_') } } + return result.String() } From 7837334361e95fcc4a3dcb7d40f00f07ee7d0e4c Mon Sep 17 00:00:00 2001 From: imterah Date: Wed, 8 Jan 2025 11:40:42 -0500 Subject: [PATCH 16/45] fix: Fixes various crashes. --- backend/api/backendruntime/runtime.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index 1e79406..a65c077 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -421,7 +421,12 @@ SchedulingLoop: } // Fetch response and close Channel - response := <-commandChannel + response, ok := <-commandChannel + + if !ok { + return nil, fmt.Errorf("failed to read from command channel: recieved signal that is not OK") + } + close(commandChannel) err, ok := response.(error) @@ -443,11 +448,11 @@ func (runtime *Runtime) cleanUpPendingCommandProcessingJobs() { select { case <-timeoutChannel: - log.Fatal("Message channel is likely running (timed out reading from it without an error)") + log.Warn("Message channel is likely running (timed out reading from it without an error)") close(message.Channel) case _, ok := <-message.Channel: if ok { - log.Fatal("Message channel is running, but should be stopped (since message is NOT nil!)") + log.Warn("Message channel is running, but should be stopped (since message is NOT nil!)") close(message.Channel) } } From 4cb648cd66bb5d9beb4d0d74ee10dcce3a809203 Mon Sep 17 00:00:00 2001 From: imterah Date: Wed, 8 Jan 2025 12:38:10 -0500 Subject: [PATCH 17/45] fix: Fixes errors showing for no reason. --- backend/sshbackend/main.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index 1bf35e7..fd0a680 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "net" "os" @@ -211,13 +212,13 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er for { len, err := forwardedConn.Read(forwardedBuffer) - if err != nil { + + if err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { log.Errorf("failed to read from forwarded connection: %s", err.Error()) return } - _, err = sourceConn.Write(forwardedBuffer[:len]) - if err != nil { + if _, err = sourceConn.Write(forwardedBuffer[:len]); err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { log.Errorf("failed to write to source connection: %s", err.Error()) return } @@ -229,13 +230,13 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er for { len, err := sourceConn.Read(sourceBuffer) - if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + + if err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { log.Errorf("failed to read from source connection: %s", err.Error()) return } - _, err = forwardedConn.Write(sourceBuffer[:len]) - if err != nil && err.Error() != "EOF" && strings.HasSuffix(err.Error(), "use of closed network connection") { + if _, err = forwardedConn.Write(sourceBuffer[:len]); err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { log.Errorf("failed to write to forwarded connection: %s", err.Error()) return } From a3519220beab8dc4dda35f7086d63c6edb491ea3 Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 9 Jan 2025 07:22:12 -0500 Subject: [PATCH 18/45] fix: Adds automatic restarting upon failure for SSH. --- backend/sshbackend/main.go | 77 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index fd0a680..b86dd41 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" "sync" + "time" "git.terah.dev/imterah/hermes/backend/backendutil" "git.terah.dev/imterah/hermes/backend/commonbackend" @@ -84,6 +85,8 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { backend.conn = conn log.Info("SSHBackend has initialized successfully.") + go backend.backendDisconnectHandler() + return true, nil } @@ -139,6 +142,11 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er if err != nil { log.Warnf("failed to accept listener connection: %s", err.Error()) + + if err.Error() == "EOF" { + return + } + continue } @@ -324,6 +332,75 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba } } +func (backend *SSHBackend) backendDisconnectHandler() { + for { + if backend.conn != nil { + err := backend.conn.Wait() + + if err == nil || err.Error() != "EOF" { + continue + } + } + + log.Info("Disconnected from SSHBackend. Attempting to reconnect in 5 seconds...") + + time.Sleep(5 * time.Second) + + // Make the connection nil to accurately report our status incase GetBackendStatus is called + backend.conn = nil + + // Use the last half of the code from the main initialization + signer, err := ssh.ParsePrivateKey([]byte(backend.config.PrivateKey)) + + if err != nil { + log.Errorf("Failed to parse private key: %s", err.Error()) + return + } + + auth := ssh.PublicKeys(signer) + + config := &ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + User: backend.config.Username, + Auth: []ssh.AuthMethod{ + auth, + }, + } + + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port), config) + + if err != nil { + log.Errorf("Failed to connect to the server: %s", err.Error()) + return + } + + backend.conn = conn + + log.Info("SSHBackend has reconnected successfully. Attempting to set up proxies again...") + + for _, proxy := range backend.proxies { + ok, err := backend.StartProxy(&commonbackend.AddProxy{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestPort, + Protocol: proxy.Protocol, + }) + + if err != nil { + log.Errorf("Failed to set up proxy: %s", err.Error()) + continue + } + + if !ok { + log.Errorf("Failed to set up proxy: OK status is false") + continue + } + } + + log.Info("SSHBackend has reinitialized and restored state successfully.") + } +} + func main() { logLevel := os.Getenv("HERMES_LOG_LEVEL") From 3429f2cd376dd0d7a09f1d591901304fe15a9565 Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 9 Jan 2025 07:39:35 -0500 Subject: [PATCH 19/45] chore: Reword restart notification message. --- backend/sshbackend/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index b86dd41..12db671 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -342,7 +342,7 @@ func (backend *SSHBackend) backendDisconnectHandler() { } } - log.Info("Disconnected from SSHBackend. Attempting to reconnect in 5 seconds...") + log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...") time.Sleep(5 * time.Second) From ea0a953b0ee39733fd5793b2eb86dd78923f6eca Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 9 Jan 2025 07:50:32 -0500 Subject: [PATCH 20/45] chore: Remove unneeded debug messages. --- backend/sshbackend/main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index 12db671..f63b8c3 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -266,10 +266,7 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, backend.arrayPropMutex.Lock() for proxyIndex, proxy := range backend.proxies { - // Check if memory addresses are equal for the pointer if command.SourceIP == proxy.SourceIP && command.SourcePort == proxy.SourcePort && command.DestPort == proxy.DestPort && command.Protocol == proxy.Protocol { - log.Debug("found proxy in StopProxy. shutting down listeners") - for _, listener := range proxy.Listeners { err := listener.Close() From 356cfb8dca264bb7b829ffd01a7d8d2f983c2856 Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 9 Jan 2025 08:00:37 -0500 Subject: [PATCH 21/45] fix: Fixes action workflows. --- .forgejo/workflows/release.yml | 2 +- Dockerfile | 10 ++++++++++ backend/Dockerfile | 10 ---------- 3 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 Dockerfile delete mode 100644 backend/Dockerfile diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index e60aacf..a8c4cd9 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -34,7 +34,7 @@ jobs: - name: Build Docker image run: | - docker build ./backend --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME + docker build . --tag ghcr.io/imterah/hermes:$GITHUB_REF_NAME - name: Upload Docker image run: | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81710a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:latest AS build +WORKDIR /build +COPY . /build +RUN cd backend; bash build.sh +FROM busybox:stable-glibc AS run +WORKDIR /app +COPY --from=build /build/backend/backends.prod.json /app/backends.json +COPY --from=build /build/backend/api/api /app/hermes +COPY --from=build /build/backend/sshbackend/sshbackend /app/sshbackend +ENTRYPOINT ["/app/hermes", "--backends-path", "/app/backends.json"] diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 09859b5..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM golang:latest AS build -WORKDIR /build -COPY . /build -RUN bash build.sh -FROM busybox:stable-glibc AS run -WORKDIR /app -COPY --from=build /build/backends.prod.json /app/backends.json -COPY --from=build /build/api/api /app/hermes -COPY --from=build /build/sshbackend/sshbackend /app/sshbackend -ENTRYPOINT ["/app/hermes", "--backends-path", "/app/backends.json"] From 0efda4b2833cecdd39bb1cae7db75930216ad677 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 10 Jan 2025 16:23:26 -0500 Subject: [PATCH 22/45] feature: Add profiling documentation for backends based on BackendUtil. --- backend/backendutil/application.go | 7 +- backend/backendutil/profiling_disabled.go | 9 +++ backend/backendutil/profiling_enabled.go | 91 +++++++++++++++++++++++ docs/profiling.md | 6 ++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 backend/backendutil/profiling_disabled.go create mode 100644 backend/backendutil/profiling_enabled.go create mode 100644 docs/profiling.md diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index ccb21f2..802e8c7 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -18,9 +18,14 @@ type BackendApplicationHelper struct { func (helper *BackendApplicationHelper) Start() error { log.Debug("BackendApplicationHelper is starting") + err := configureAndLaunchBackgroundProfilingTasks() + + if err != nil { + return err + } + log.Debug("Currently waiting for Unix socket connection...") - var err error helper.socket, err = net.Dial("unix", helper.SocketPath) if err != nil { diff --git a/backend/backendutil/profiling_disabled.go b/backend/backendutil/profiling_disabled.go new file mode 100644 index 0000000..d93cbdd --- /dev/null +++ b/backend/backendutil/profiling_disabled.go @@ -0,0 +1,9 @@ +//go:build !debug + +package backendutil + +var endProfileFunc func() + +func configureAndLaunchBackgroundProfilingTasks() error { + return nil +} diff --git a/backend/backendutil/profiling_enabled.go b/backend/backendutil/profiling_enabled.go new file mode 100644 index 0000000..a3be527 --- /dev/null +++ b/backend/backendutil/profiling_enabled.go @@ -0,0 +1,91 @@ +//go:build debug + +package backendutil + +import ( + "errors" + "fmt" + "os" + "os/signal" + "runtime/pprof" + "syscall" + "time" + + "github.com/charmbracelet/log" + "golang.org/x/exp/rand" +) + +func configureAndLaunchBackgroundProfilingTasks() error { + profilingMode, err := os.ReadFile("/tmp/hermes.backendlauncher.profilebackends") + + if err != nil && errors.Is(err, os.ErrNotExist) { + return nil + } + + switch string(profilingMode) { + case "cpu": + log.Debug("Starting CPU profiling as a background task") + go doCPUProfiling() + case "mem": + log.Debug("Starting memory profiling as a background task") + go doMemoryProfiling() + default: + log.Warnf("Unknown profiling mode: %s", string(profilingMode)) + return nil + } + + return nil +} + +func doCPUProfiling() { + // (imterah) WTF? why isn't this being seeded on its own? according to Go docs, this should be seeded automatically... + rand.Seed(uint64(time.Now().UnixNano())) + + profileFileName := fmt.Sprintf("/tmp/hermes.backendlauncher.cpu.prof.%d", rand.Int()) + profileFile, err := os.Create(profileFileName) + + if err != nil { + log.Fatalf("Failed to create CPU profiling file: %s", err.Error()) + } + + log.Debugf("Writing CPU usage profile to '%s'. Will capture when Ctrl+C/SIGTERM is recieved.", profileFileName) + pprof.StartCPUProfile(profileFile) + + exitNotification := make(chan os.Signal, 1) + signal.Notify(exitNotification, os.Interrupt, syscall.SIGTERM) + <-exitNotification + + log.Debug("Recieved SIGTERM. Cleaning up and exiting...") + + pprof.StopCPUProfile() + profileFile.Close() + + log.Debug("Exiting...") + os.Exit(0) +} + +func doMemoryProfiling() { + // (imterah) WTF? why isn't this being seeded on its own? according to Go docs, this should be seeded automatically... + rand.Seed(uint64(time.Now().UnixNano())) + + profileFileName := fmt.Sprintf("/tmp/hermes.backendlauncher.mem.prof.%d", rand.Int()) + profileFile, err := os.Create(profileFileName) + + if err != nil { + log.Fatalf("Failed to create memory profiling file: %s", err.Error()) + } + + log.Debugf("Writing memory profile to '%s'. Will capture when Ctrl+C/SIGTERM is recieved.", profileFileName) + + exitNotification := make(chan os.Signal, 1) + signal.Notify(exitNotification, os.Interrupt, syscall.SIGTERM) + <-exitNotification + + log.Debug("Recieved SIGTERM. Cleaning up and exiting...") + + pprof.WriteHeapProfile(profileFile) + profileFile.Close() + + log.Debug("Exiting...") + os.Exit(0) +} diff --git a/docs/profiling.md b/docs/profiling.md new file mode 100644 index 0000000..06ceb7d --- /dev/null +++ b/docs/profiling.md @@ -0,0 +1,6 @@ +# Profiling +To profile any backend code based on `backendutil`, follow these steps: +1. Rebuild the backend with the `debug` flag: `cd $BACKEND_HERE; GOOS=linux go build -tags debug .; cd ..` +2. Copy the binary to the target machine (if applicable), and stop the API server. +3. If you want to profile the CPU utilization, write `cpu` to the file `/tmp/hermes.backendlauncher.profilebackends`: `echo -n "cpu" > /tmp/hermes.backendlauncher.profilebackends`. Else, replace `cpu` with `mem`. +4. Start the API server, with development mode and debug logging enabled. From 48adfc88db93a0eb6e3827d4fb86d90b179c047f Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 10 Jan 2025 16:37:38 -0500 Subject: [PATCH 23/45] fix: Fixes performance regression introduced in 4cb648cd66 / v2.1.0. (closes #7) --- backend/sshbackend/main.go | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index f63b8c3..baf1994 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -221,13 +221,19 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er for { len, err := forwardedConn.Read(forwardedBuffer) - if err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to read from forwarded connection: %s", err.Error()) + if err != nil { + if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { + log.Errorf("failed to read from forwarded connection: %s", err.Error()) + } + return } - if _, err = sourceConn.Write(forwardedBuffer[:len]); err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to write to source connection: %s", err.Error()) + if _, err = sourceConn.Write(forwardedBuffer[:len]); err != nil { + if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { + log.Errorf("failed to write to source connection: %s", err.Error()) + } + return } } @@ -239,13 +245,19 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er for { len, err := sourceConn.Read(sourceBuffer) - if err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to read from source connection: %s", err.Error()) + if err != nil { + if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { + log.Errorf("failed to read from source connection: %s", err.Error()) + } + return } - if _, err = forwardedConn.Write(sourceBuffer[:len]); err != nil && err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { - log.Errorf("failed to write to forwarded connection: %s", err.Error()) + if _, err = forwardedConn.Write(sourceBuffer[:len]); err != nil { + if err.Error() != "EOF" && !errors.Is(err, net.ErrClosed) { + log.Errorf("failed to write to forwarded connection: %s", err.Error()) + } + return } } From 737ba2887f1a9f58eaee98f67d7c75e9f22fc7c8 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 10 Jan 2025 20:34:50 -0500 Subject: [PATCH 24/45] chore: Delete old commonbackend code. --- backend/commonbackend/constants.go | 147 ---- backend/commonbackend/marshal.go | 507 ------------- backend/commonbackend/marshalling_test.go | 824 ---------------------- backend/commonbackend/unmarshal.go | 665 ----------------- 4 files changed, 2143 deletions(-) delete mode 100644 backend/commonbackend/constants.go delete mode 100644 backend/commonbackend/marshal.go delete mode 100644 backend/commonbackend/marshalling_test.go delete mode 100644 backend/commonbackend/unmarshal.go diff --git a/backend/commonbackend/constants.go b/backend/commonbackend/constants.go deleted file mode 100644 index cdb68f2..0000000 --- a/backend/commonbackend/constants.go +++ /dev/null @@ -1,147 +0,0 @@ -package commonbackend - -type Start struct { - Type string // Will be 'start' always - Arguments []byte -} - -type Stop struct { - Type string // Will be 'stop' always -} - -type AddProxy struct { - Type string // Will be 'addProxy' always - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' -} - -type RemoveProxy struct { - Type string // Will be 'removeProxy' always - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' -} - -type ProxyStatusRequest struct { - Type string // Will be 'proxyStatusRequest' always - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' -} - -type ProxyStatusResponse struct { - Type string // Will be 'proxyStatusResponse' always - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' - IsActive bool -} - -type ProxyInstance struct { - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' -} - -type ProxyInstanceResponse struct { - Type string // Will be 'proxyConnectionResponse' always - Proxies []*ProxyInstance // List of connections -} - -type ProxyInstanceRequest struct { - Type string // Will be 'proxyConnectionRequest' always -} - -type BackendStatusResponse struct { - Type string // Will be 'backendStatusResponse' always - IsRunning bool // True if running, false if not running - StatusCode int // Either the 'Success' or 'Failure' constant - Message string // String message from the client (ex. failed to dial TCP) -} - -type BackendStatusRequest struct { - Type string // Will be 'backendStatusRequest' always -} - -type ProxyConnectionsRequest struct { - Type string // Will be 'proxyConnectionsRequest' always -} - -// Client's connection to a specific proxy -type ProxyClientConnection struct { - SourceIP string - SourcePort uint16 - DestPort uint16 - ClientIP string - ClientPort uint16 -} - -type ProxyConnectionsResponse struct { - Type string // Will be 'proxyConnectionsResponse' always - Connections []*ProxyClientConnection // List of connections -} - -type CheckClientParameters struct { - Type string // Will be 'checkClientParameters' always - SourceIP string - SourcePort uint16 - DestPort uint16 - Protocol string // Will be either 'tcp' or 'udp' -} - -type CheckServerParameters struct { - Type string // Will be 'checkServerParameters' always - Arguments []byte -} - -// Sent as a response to either CheckClientParameters or CheckBackendParameters -type CheckParametersResponse struct { - Type string // Will be 'checkParametersResponse' always - InResponseTo string // Will be either 'checkClientParameters' or 'checkServerParameters' - IsValid bool // If true, valid, and if false, invalid - Message string // String message from the client (ex. failed to unmarshal JSON: x is not defined) -} - -const ( - StartID = iota - StopID - AddProxyID - RemoveProxyID - ProxyConnectionsResponseID - CheckClientParametersID - CheckServerParametersID - CheckParametersResponseID - ProxyConnectionsRequestID - BackendStatusResponseID - BackendStatusRequestID - ProxyStatusRequestID - ProxyStatusResponseID - ProxyInstanceResponseID - ProxyInstanceRequestID -) - -const ( - TCP = iota - UDP -) - -const ( - StatusSuccess = iota - StatusFailure -) - -const ( - // IP versions - IPv4 = 4 - IPv6 = 6 - - // TODO: net has these constants defined already. We should switch to these - IPv4Size = 4 - IPv6Size = 16 -) diff --git a/backend/commonbackend/marshal.go b/backend/commonbackend/marshal.go deleted file mode 100644 index 6baf02e..0000000 --- a/backend/commonbackend/marshal.go +++ /dev/null @@ -1,507 +0,0 @@ -package commonbackend - -import ( - "encoding/binary" - "fmt" - "net" -) - -func marshalIndividualConnectionStruct(conn *ProxyClientConnection) []byte { - sourceIPOriginal := net.ParseIP(conn.SourceIP) - clientIPOriginal := net.ParseIP(conn.ClientIP) - - var serverIPVer uint8 - var sourceIP []byte - - if sourceIPOriginal.To4() == nil { - serverIPVer = IPv6 - sourceIP = sourceIPOriginal.To16() - } else { - serverIPVer = IPv4 - sourceIP = sourceIPOriginal.To4() - } - - var clientIPVer uint8 - var clientIP []byte - - if clientIPOriginal.To4() == nil { - clientIPVer = IPv6 - clientIP = clientIPOriginal.To16() - } else { - clientIPVer = IPv4 - clientIP = clientIPOriginal.To4() - } - - connectionBlock := make([]byte, 8+len(sourceIP)+len(clientIP)) - - connectionBlock[0] = serverIPVer - copy(connectionBlock[1:len(sourceIP)+1], sourceIP) - - binary.BigEndian.PutUint16(connectionBlock[1+len(sourceIP):3+len(sourceIP)], conn.SourcePort) - binary.BigEndian.PutUint16(connectionBlock[3+len(sourceIP):5+len(sourceIP)], conn.DestPort) - - connectionBlock[5+len(sourceIP)] = clientIPVer - copy(connectionBlock[6+len(sourceIP):6+len(sourceIP)+len(clientIP)], clientIP) - binary.BigEndian.PutUint16(connectionBlock[6+len(sourceIP)+len(clientIP):8+len(sourceIP)+len(clientIP)], conn.ClientPort) - - return connectionBlock -} - -func marshalIndividualProxyStruct(conn *ProxyInstance) ([]byte, error) { - sourceIPOriginal := net.ParseIP(conn.SourceIP) - - var sourceIPVer uint8 - var sourceIP []byte - - if sourceIPOriginal.To4() == nil { - sourceIPVer = IPv6 - sourceIP = sourceIPOriginal.To16() - } else { - sourceIPVer = IPv4 - sourceIP = sourceIPOriginal.To4() - } - - proxyBlock := make([]byte, 6+len(sourceIP)) - - proxyBlock[0] = sourceIPVer - copy(proxyBlock[1:len(sourceIP)+1], sourceIP) - - binary.BigEndian.PutUint16(proxyBlock[1+len(sourceIP):3+len(sourceIP)], conn.SourcePort) - binary.BigEndian.PutUint16(proxyBlock[3+len(sourceIP):5+len(sourceIP)], conn.DestPort) - - var protocolVersion uint8 - - if conn.Protocol == "tcp" { - protocolVersion = TCP - } else if conn.Protocol == "udp" { - protocolVersion = UDP - } else { - return proxyBlock, fmt.Errorf("invalid protocol recieved") - } - - proxyBlock[5+len(sourceIP)] = protocolVersion - - return proxyBlock, nil -} - -func Marshal(commandType string, command interface{}) ([]byte, error) { - switch commandType { - case "start": - startCommand, ok := command.(*Start) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - startCommandBytes := make([]byte, 1+2+len(startCommand.Arguments)) - startCommandBytes[0] = StartID - binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(startCommand.Arguments))) - copy(startCommandBytes[3:], startCommand.Arguments) - - return startCommandBytes, nil - case "stop": - _, ok := command.(*Stop) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - return []byte{StopID}, nil - case "addProxy": - addConnectionCommand, ok := command.(*AddProxy) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(addConnectionCommand.SourceIP) - - var ipVer uint8 - var ipBytes []byte - - if sourceIP.To4() == nil { - ipBytes = sourceIP.To16() - ipVer = IPv6 - } else { - ipBytes = sourceIP.To4() - ipVer = IPv4 - } - - addConnectionBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) - - addConnectionBytes[0] = AddProxyID - addConnectionBytes[1] = ipVer - - copy(addConnectionBytes[2:2+len(ipBytes)], ipBytes) - - binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], addConnectionCommand.SourcePort) - binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], addConnectionCommand.DestPort) - - var protocol uint8 - - if addConnectionCommand.Protocol == "tcp" { - protocol = TCP - } else if addConnectionCommand.Protocol == "udp" { - protocol = UDP - } else { - return nil, fmt.Errorf("invalid protocol") - } - - addConnectionBytes[6+len(ipBytes)] = protocol - - return addConnectionBytes, nil - case "removeProxy": - removeConnectionCommand, ok := command.(*RemoveProxy) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(removeConnectionCommand.SourceIP) - - var ipVer uint8 - var ipBytes []byte - - if sourceIP.To4() == nil { - ipBytes = sourceIP.To16() - ipVer = IPv6 - } else { - ipBytes = sourceIP.To4() - ipVer = IPv4 - } - - removeConnectionBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) - - removeConnectionBytes[0] = RemoveProxyID - removeConnectionBytes[1] = ipVer - copy(removeConnectionBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], removeConnectionCommand.SourcePort) - binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], removeConnectionCommand.DestPort) - - var protocol uint8 - - if removeConnectionCommand.Protocol == "tcp" { - protocol = TCP - } else if removeConnectionCommand.Protocol == "udp" { - protocol = UDP - } else { - return nil, fmt.Errorf("invalid protocol") - } - - removeConnectionBytes[6+len(ipBytes)] = protocol - - return removeConnectionBytes, nil - case "proxyConnectionsResponse": - allConnectionsCommand, ok := command.(*ProxyConnectionsResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - connectionsArray := make([][]byte, len(allConnectionsCommand.Connections)) - totalSize := 0 - - for connIndex, conn := range allConnectionsCommand.Connections { - connectionsArray[connIndex] = marshalIndividualConnectionStruct(conn) - totalSize += len(connectionsArray[connIndex]) + 1 - } - - if totalSize == 0 { - totalSize = 1 - } - - connectionCommandArray := make([]byte, totalSize+1) - connectionCommandArray[0] = ProxyConnectionsResponseID - - currentPosition := 1 - - for _, connection := range connectionsArray { - copy(connectionCommandArray[currentPosition:currentPosition+len(connection)], connection) - connectionCommandArray[currentPosition+len(connection)] = '\r' - currentPosition += len(connection) + 1 - } - - connectionCommandArray[totalSize] = '\n' - return connectionCommandArray, nil - case "checkClientParameters": - checkClientCommand, ok := command.(*CheckClientParameters) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(checkClientCommand.SourceIP) - - var ipVer uint8 - var ipBytes []byte - - if sourceIP.To4() == nil { - ipBytes = sourceIP.To16() - ipVer = IPv6 - } else { - ipBytes = sourceIP.To4() - ipVer = IPv4 - } - - checkClientBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) - - checkClientBytes[0] = CheckClientParametersID - checkClientBytes[1] = ipVer - copy(checkClientBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], checkClientCommand.SourcePort) - binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], checkClientCommand.DestPort) - - var protocol uint8 - - if checkClientCommand.Protocol == "tcp" { - protocol = TCP - } else if checkClientCommand.Protocol == "udp" { - protocol = UDP - } else { - return nil, fmt.Errorf("invalid protocol") - } - - checkClientBytes[6+len(ipBytes)] = protocol - - return checkClientBytes, nil - case "checkServerParameters": - checkServerCommand, ok := command.(*CheckServerParameters) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - serverCommandBytes := make([]byte, 1+2+len(checkServerCommand.Arguments)) - serverCommandBytes[0] = CheckServerParametersID - binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(checkServerCommand.Arguments))) - copy(serverCommandBytes[3:], checkServerCommand.Arguments) - - return serverCommandBytes, nil - case "checkParametersResponse": - checkParametersCommand, ok := command.(*CheckParametersResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - var checkMethod uint8 - - if checkParametersCommand.InResponseTo == "checkClientParameters" { - checkMethod = CheckClientParametersID - } else if checkParametersCommand.InResponseTo == "checkServerParameters" { - checkMethod = CheckServerParametersID - } else { - return nil, fmt.Errorf("invalid mode recieved (must be either checkClientParameters or checkServerParameters)") - } - - var isValid uint8 - - if checkParametersCommand.IsValid { - isValid = 1 - } - - checkResponseBytes := make([]byte, 3+2+len(checkParametersCommand.Message)) - checkResponseBytes[0] = CheckParametersResponseID - checkResponseBytes[1] = checkMethod - checkResponseBytes[2] = isValid - - binary.BigEndian.PutUint16(checkResponseBytes[3:5], uint16(len(checkParametersCommand.Message))) - - if len(checkParametersCommand.Message) != 0 { - copy(checkResponseBytes[5:], []byte(checkParametersCommand.Message)) - } - - return checkResponseBytes, nil - case "backendStatusResponse": - backendStatusResponse, ok := command.(*BackendStatusResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - var isRunning uint8 - - if backendStatusResponse.IsRunning { - isRunning = 1 - } else { - isRunning = 0 - } - - statusResponseBytes := make([]byte, 3+2+len(backendStatusResponse.Message)) - statusResponseBytes[0] = BackendStatusResponseID - statusResponseBytes[1] = isRunning - statusResponseBytes[2] = byte(backendStatusResponse.StatusCode) - - binary.BigEndian.PutUint16(statusResponseBytes[3:5], uint16(len(backendStatusResponse.Message))) - - if len(backendStatusResponse.Message) != 0 { - copy(statusResponseBytes[5:], []byte(backendStatusResponse.Message)) - } - - return statusResponseBytes, nil - case "backendStatusRequest": - _, ok := command.(*BackendStatusRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - statusRequestBytes := make([]byte, 1) - statusRequestBytes[0] = BackendStatusRequestID - - return statusRequestBytes, nil - case "proxyStatusRequest": - proxyStatusRequest, ok := command.(*ProxyStatusRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(proxyStatusRequest.SourceIP) - - var ipVer uint8 - var ipBytes []byte - - if sourceIP.To4() == nil { - ipBytes = sourceIP.To16() - ipVer = IPv6 - } else { - ipBytes = sourceIP.To4() - ipVer = IPv4 - } - - proxyStatusRequestBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) - - proxyStatusRequestBytes[0] = ProxyStatusRequestID - proxyStatusRequestBytes[1] = ipVer - - copy(proxyStatusRequestBytes[2:2+len(ipBytes)], ipBytes) - - binary.BigEndian.PutUint16(proxyStatusRequestBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusRequest.SourcePort) - binary.BigEndian.PutUint16(proxyStatusRequestBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusRequest.DestPort) - - var protocol uint8 - - if proxyStatusRequest.Protocol == "tcp" { - protocol = TCP - } else if proxyStatusRequest.Protocol == "udp" { - protocol = UDP - } else { - return nil, fmt.Errorf("invalid protocol") - } - - proxyStatusRequestBytes[6+len(ipBytes)] = protocol - - return proxyStatusRequestBytes, nil - case "proxyStatusResponse": - proxyStatusResponse, ok := command.(*ProxyStatusResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(proxyStatusResponse.SourceIP) - - var ipVer uint8 - var ipBytes []byte - - if sourceIP.To4() == nil { - ipBytes = sourceIP.To16() - ipVer = IPv6 - } else { - ipBytes = sourceIP.To4() - ipVer = IPv4 - } - - proxyStatusResponseBytes := make([]byte, 1+1+len(ipBytes)+2+2+1+1) - - proxyStatusResponseBytes[0] = ProxyStatusResponseID - proxyStatusResponseBytes[1] = ipVer - - copy(proxyStatusResponseBytes[2:2+len(ipBytes)], ipBytes) - - binary.BigEndian.PutUint16(proxyStatusResponseBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusResponse.SourcePort) - binary.BigEndian.PutUint16(proxyStatusResponseBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusResponse.DestPort) - - var protocol uint8 - - if proxyStatusResponse.Protocol == "tcp" { - protocol = TCP - } else if proxyStatusResponse.Protocol == "udp" { - protocol = UDP - } else { - return nil, fmt.Errorf("invalid protocol") - } - - proxyStatusResponseBytes[6+len(ipBytes)] = protocol - - var isActive uint8 - - if proxyStatusResponse.IsActive { - isActive = 1 - } else { - isActive = 0 - } - - proxyStatusResponseBytes[7+len(ipBytes)] = isActive - - return proxyStatusResponseBytes, nil - case "proxyInstanceResponse": - proxyConectionResponse, ok := command.(*ProxyInstanceResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - proxyArray := make([][]byte, len(proxyConectionResponse.Proxies)) - totalSize := 0 - - for proxyIndex, proxy := range proxyConectionResponse.Proxies { - var err error - proxyArray[proxyIndex], err = marshalIndividualProxyStruct(proxy) - - if err != nil { - return nil, err - } - - totalSize += len(proxyArray[proxyIndex]) + 1 - } - - if totalSize == 0 { - totalSize = 1 - } - - connectionCommandArray := make([]byte, totalSize+1) - connectionCommandArray[0] = ProxyInstanceResponseID - - currentPosition := 1 - - for _, connection := range proxyArray { - copy(connectionCommandArray[currentPosition:currentPosition+len(connection)], connection) - connectionCommandArray[currentPosition+len(connection)] = '\r' - currentPosition += len(connection) + 1 - } - - connectionCommandArray[totalSize] = '\n' - - return connectionCommandArray, nil - case "proxyInstanceRequest": - _, ok := command.(*ProxyInstanceRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - return []byte{ProxyInstanceRequestID}, nil - case "proxyConnectionsRequest": - _, ok := command.(*ProxyConnectionsRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - return []byte{ProxyConnectionsRequestID}, nil - } - - return nil, fmt.Errorf("couldn't match command name") -} diff --git a/backend/commonbackend/marshalling_test.go b/backend/commonbackend/marshalling_test.go deleted file mode 100644 index 1c93f94..0000000 --- a/backend/commonbackend/marshalling_test.go +++ /dev/null @@ -1,824 +0,0 @@ -package commonbackend - -import ( - "bytes" - "log" - "os" - "testing" -) - -var logLevel = os.Getenv("HERMES_LOG_LEVEL") - -func TestStartCommandMarshalSupport(t *testing.T) { - commandInput := &Start{ - Type: "start", - Arguments: []byte("Hello from automated testing"), - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*Start) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { - log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) - } -} - -func TestStopCommandMarshalSupport(t *testing.T) { - commandInput := &Stop{ - Type: "stop", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*Stop) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } -} - -func TestAddConnectionCommandMarshalSupport(t *testing.T) { - commandInput := &AddProxy{ - Type: "addProxy", - SourceIP: "192.168.0.139", - SourcePort: 19132, - DestPort: 19132, - Protocol: "tcp", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*AddProxy) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.SourceIP != commandUnmarshalled.SourceIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) - } - - if commandInput.SourcePort != commandUnmarshalled.SourcePort { - t.Fail() - log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) - } - - if commandInput.DestPort != commandUnmarshalled.DestPort { - t.Fail() - log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) - } - - if commandInput.Protocol != commandUnmarshalled.Protocol { - t.Fail() - log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) - } -} - -func TestRemoveConnectionCommandMarshalSupport(t *testing.T) { - commandInput := &RemoveProxy{ - Type: "removeProxy", - SourceIP: "192.168.0.139", - SourcePort: 19132, - DestPort: 19132, - Protocol: "tcp", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.SourceIP != commandUnmarshalled.SourceIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) - } - - if commandInput.SourcePort != commandUnmarshalled.SourcePort { - t.Fail() - log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) - } - - if commandInput.DestPort != commandUnmarshalled.DestPort { - t.Fail() - log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) - } - - if commandInput.Protocol != commandUnmarshalled.Protocol { - t.Fail() - log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) - } -} - -func TestGetAllConnectionsCommandMarshalSupport(t *testing.T) { - commandInput := &ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", - Connections: []*ProxyClientConnection{ - { - SourceIP: "127.0.0.1", - SourcePort: 19132, - DestPort: 19132, - ClientIP: "127.0.0.1", - ClientPort: 12321, - }, - { - SourceIP: "127.0.0.1", - SourcePort: 19132, - DestPort: 19132, - ClientIP: "192.168.0.168", - ClientPort: 23457, - }, - { - SourceIP: "127.0.0.1", - SourcePort: 19132, - DestPort: 19132, - ClientIP: "68.42.203.47", - ClientPort: 38721, - }, - }, - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - for commandIndex, originalConnection := range commandInput.Connections { - remoteConnection := commandUnmarshalled.Connections[commandIndex] - - if originalConnection.SourceIP != remoteConnection.SourceIP { - t.Fail() - log.Printf("(in #%d) SourceIP's are not equal (orig: %s, unmsh: %s)", commandIndex, originalConnection.SourceIP, remoteConnection.SourceIP) - } - - if originalConnection.SourcePort != remoteConnection.SourcePort { - t.Fail() - log.Printf("(in #%d) SourcePort's are not equal (orig: %d, unmsh: %d)", commandIndex, originalConnection.SourcePort, remoteConnection.SourcePort) - } - - if originalConnection.DestPort != remoteConnection.DestPort { - t.Fail() - log.Printf("(in #%d) DestPort's are not equal (orig: %d, unmsh: %d)", commandIndex, originalConnection.DestPort, remoteConnection.DestPort) - } - - if originalConnection.ClientIP != remoteConnection.ClientIP { - t.Fail() - log.Printf("(in #%d) ClientIP's are not equal (orig: %s, unmsh: %s)", commandIndex, originalConnection.ClientIP, remoteConnection.ClientIP) - } - - if originalConnection.ClientPort != remoteConnection.ClientPort { - t.Fail() - log.Printf("(in #%d) ClientPort's are not equal (orig: %d, unmsh: %d)", commandIndex, originalConnection.ClientPort, remoteConnection.ClientPort) - } - } -} - -func TestCheckClientParametersMarshalSupport(t *testing.T) { - commandInput := &CheckClientParameters{ - Type: "checkClientParameters", - SourceIP: "192.168.0.139", - SourcePort: 19132, - DestPort: 19132, - Protocol: "tcp", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckClientParameters) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.SourceIP != commandUnmarshalled.SourceIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) - } - - if commandInput.SourcePort != commandUnmarshalled.SourcePort { - t.Fail() - log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) - } - - if commandInput.DestPort != commandUnmarshalled.DestPort { - t.Fail() - log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) - } - - if commandInput.Protocol != commandUnmarshalled.Protocol { - t.Fail() - log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) - } -} - -func TestCheckServerParametersMarshalSupport(t *testing.T) { - commandInput := &CheckServerParameters{ - Type: "checkServerParameters", - Arguments: []byte("Hello from automated testing"), - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckServerParameters) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { - log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) - } -} - -func TestCheckParametersResponseMarshalSupport(t *testing.T) { - commandInput := &CheckParametersResponse{ - Type: "checkParametersResponse", - InResponseTo: "checkClientParameters", - IsValid: true, - Message: "Hello from automated testing", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckParametersResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.InResponseTo != commandUnmarshalled.InResponseTo { - t.Fail() - log.Printf("InResponseTo's are not equal (orig: %s, unmsh: %s)", commandInput.InResponseTo, commandUnmarshalled.InResponseTo) - } - - if commandInput.IsValid != commandUnmarshalled.IsValid { - t.Fail() - log.Printf("IsValid's are not equal (orig: %t, unmsh: %t)", commandInput.IsValid, commandUnmarshalled.IsValid) - } - - if commandInput.Message != commandUnmarshalled.Message { - t.Fail() - log.Printf("Messages are not equal (orig: %s, unmsh: %s)", commandInput.Message, commandUnmarshalled.Message) - } -} - -func TestBackendStatusRequestMarshalSupport(t *testing.T) { - commandInput := &BackendStatusRequest{ - Type: "backendStatusRequest", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } -} - -func TestBackendStatusResponseMarshalSupport(t *testing.T) { - commandInput := &BackendStatusResponse{ - Type: "backendStatusResponse", - IsRunning: true, - StatusCode: StatusFailure, - Message: "Hello from automated testing", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.IsRunning != commandUnmarshalled.IsRunning { - t.Fail() - log.Printf("IsRunning's are not equal (orig: %t, unmsh: %t)", commandInput.IsRunning, commandUnmarshalled.IsRunning) - } - - if commandInput.StatusCode != commandUnmarshalled.StatusCode { - t.Fail() - log.Printf("StatusCodes are not equal (orig: %d, unmsh: %d)", commandInput.StatusCode, commandUnmarshalled.StatusCode) - } - - if commandInput.Message != commandUnmarshalled.Message { - t.Fail() - log.Printf("Messages are not equal (orig: %s, unmsh: %s)", commandInput.Message, commandUnmarshalled.Message) - } -} - -func TestProxyStatusRequestMarshalSupport(t *testing.T) { - commandInput := &ProxyStatusRequest{ - Type: "proxyStatusRequest", - SourceIP: "192.168.0.139", - SourcePort: 19132, - DestPort: 19132, - Protocol: "tcp", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.SourceIP != commandUnmarshalled.SourceIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) - } - - if commandInput.SourcePort != commandUnmarshalled.SourcePort { - t.Fail() - log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) - } - - if commandInput.DestPort != commandUnmarshalled.DestPort { - t.Fail() - log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) - } - - if commandInput.Protocol != commandUnmarshalled.Protocol { - t.Fail() - log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) - } -} - -func TestProxyStatusResponseMarshalSupport(t *testing.T) { - commandInput := &ProxyStatusResponse{ - Type: "proxyStatusResponse", - SourceIP: "192.168.0.139", - SourcePort: 19132, - DestPort: 19132, - Protocol: "tcp", - IsActive: true, - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - if commandInput.SourceIP != commandUnmarshalled.SourceIP { - t.Fail() - log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) - } - - if commandInput.SourcePort != commandUnmarshalled.SourcePort { - t.Fail() - log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) - } - - if commandInput.DestPort != commandUnmarshalled.DestPort { - t.Fail() - log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) - } - - if commandInput.Protocol != commandUnmarshalled.Protocol { - t.Fail() - log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) - } - - if commandInput.IsActive != commandUnmarshalled.IsActive { - t.Fail() - log.Printf("IsActive's are not equal (orig: %t, unmsh: %t)", commandInput.IsActive, commandUnmarshalled.IsActive) - } -} - -func TestProxyConnectionRequestMarshalSupport(t *testing.T) { - commandInput := &ProxyInstanceRequest{ - Type: "proxyInstanceRequest", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - if err != nil { - t.Fatal(err.Error()) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceRequest) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } -} - -func TestProxyConnectionResponseMarshalSupport(t *testing.T) { - commandInput := &ProxyInstanceResponse{ - Type: "proxyInstanceResponse", - Proxies: []*ProxyInstance{ - { - SourceIP: "192.168.0.168", - SourcePort: 25565, - DestPort: 25565, - Protocol: "tcp", - }, - { - SourceIP: "127.0.0.1", - SourcePort: 19132, - DestPort: 19132, - Protocol: "udp", - }, - { - SourceIP: "68.42.203.47", - SourcePort: 22, - DestPort: 2222, - Protocol: "tcp", - }, - }, - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) - - if err != nil { - t.Fatal(err.Error()) - } - - if logLevel == "debug" { - log.Printf("Generated array contents: %v", commandMarshalled) - } - - buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) - - if err != nil { - t.Fatal(err.Error()) - } - - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) - - if !ok { - t.Fatal("failed typecast") - } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - - for proxyIndex, originalProxy := range commandInput.Proxies { - remoteProxy := commandUnmarshalled.Proxies[proxyIndex] - - if originalProxy.SourceIP != remoteProxy.SourceIP { - t.Fail() - log.Printf("(in #%d) SourceIP's are not equal (orig: %s, unmsh: %s)", proxyIndex, originalProxy.SourceIP, remoteProxy.SourceIP) - } - - if originalProxy.SourcePort != remoteProxy.SourcePort { - t.Fail() - log.Printf("(in #%d) SourcePort's are not equal (orig: %d, unmsh: %d)", proxyIndex, originalProxy.SourcePort, remoteProxy.SourcePort) - } - - if originalProxy.DestPort != remoteProxy.DestPort { - t.Fail() - log.Printf("(in #%d) DestPort's are not equal (orig: %d, unmsh: %d)", proxyIndex, originalProxy.DestPort, remoteProxy.DestPort) - } - - if originalProxy.Protocol != remoteProxy.Protocol { - t.Fail() - log.Printf("(in #%d) ClientIP's are not equal (orig: %s, unmsh: %s)", proxyIndex, originalProxy.Protocol, remoteProxy.Protocol) - } - } -} diff --git a/backend/commonbackend/unmarshal.go b/backend/commonbackend/unmarshal.go deleted file mode 100644 index b8500dd..0000000 --- a/backend/commonbackend/unmarshal.go +++ /dev/null @@ -1,665 +0,0 @@ -package commonbackend - -import ( - "encoding/binary" - "fmt" - "io" - "net" -) - -func unmarshalIndividualConnectionStruct(conn io.Reader) (*ProxyClientConnection, error) { - serverIPVersion := make([]byte, 1) - - if _, err := conn.Read(serverIPVersion); err != nil { - return nil, fmt.Errorf("couldn't read server IP version") - } - - var serverIPSize uint8 - - if serverIPVersion[0] == 4 { - serverIPSize = IPv4Size - } else if serverIPVersion[0] == 6 { - serverIPSize = IPv6Size - } else if serverIPVersion[0] == '\n' { - return nil, fmt.Errorf("no data found") - } else { - return nil, fmt.Errorf("invalid server IP version recieved") - } - - serverIP := make(net.IP, serverIPSize) - - if _, err := conn.Read(serverIP); err != nil { - return nil, fmt.Errorf("couldn't read server IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") - } - - destinationPort := make([]byte, 2) - - if _, err := conn.Read(destinationPort); err != nil { - return nil, fmt.Errorf("couldn't read source port") - } - - clientIPVersion := make([]byte, 1) - - if _, err := conn.Read(clientIPVersion); err != nil { - return nil, fmt.Errorf("couldn't read server IP version") - } - - var clientIPSize uint8 - - if clientIPVersion[0] == 4 { - clientIPSize = IPv4Size - } else if clientIPVersion[0] == 6 { - clientIPSize = IPv6Size - } else { - return nil, fmt.Errorf("invalid server IP version recieved") - } - - clientIP := make(net.IP, clientIPSize) - - if _, err := conn.Read(clientIP); err != nil { - return nil, fmt.Errorf("couldn't read server IP") - } - - clientPort := make([]byte, 2) - - if _, err := conn.Read(clientPort); err != nil { - return nil, fmt.Errorf("couldn't read source port") - } - - return &ProxyClientConnection{ - SourceIP: serverIP.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destinationPort), - ClientIP: clientIP.String(), - ClientPort: binary.BigEndian.Uint16(clientPort), - }, nil -} - -func unmarshalIndividualProxyStruct(conn io.Reader) (*ProxyInstance, error) { - ipVersion := make([]byte, 1) - - if _, err := conn.Read(ipVersion); err != nil { - return nil, fmt.Errorf("couldn't read ip version") - } - - var ipSize uint8 - - if ipVersion[0] == 4 { - ipSize = IPv4Size - } else if ipVersion[0] == 6 { - ipSize = IPv6Size - } else if ipVersion[0] == '\n' { - return nil, fmt.Errorf("no data found") - } else { - return nil, fmt.Errorf("invalid IP version recieved") - } - - ip := make(net.IP, ipSize) - - if _, err := conn.Read(ip); err != nil { - return nil, fmt.Errorf("couldn't read source IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return nil, fmt.Errorf("couldn't read source port") - } - - destPort := make([]byte, 2) - - if _, err := conn.Read(destPort); err != nil { - return nil, fmt.Errorf("couldn't read destination port") - } - - protocolBytes := make([]byte, 1) - - if _, err := conn.Read(protocolBytes); err != nil { - return nil, fmt.Errorf("couldn't read protocol") - } - - var protocol string - - if protocolBytes[0] == TCP { - protocol = "tcp" - } else if protocolBytes[0] == UDP { - protocol = "udp" - } else { - return nil, fmt.Errorf("invalid protocol") - } - - return &ProxyInstance{ - SourceIP: ip.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destPort), - Protocol: protocol, - }, nil -} - -func Unmarshal(conn io.Reader) (string, interface{}, error) { - commandType := make([]byte, 1) - - if _, err := conn.Read(commandType); err != nil { - return "", nil, fmt.Errorf("couldn't read command") - } - - switch commandType[0] { - case StartID: - argumentsLength := make([]byte, 2) - - if _, err := conn.Read(argumentsLength); err != nil { - return "", nil, fmt.Errorf("couldn't read argument length") - } - - arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) - - if _, err := conn.Read(arguments); err != nil { - return "", nil, fmt.Errorf("couldn't read arguments") - } - - return "start", &Start{ - Type: "start", - Arguments: arguments, - }, nil - case StopID: - return "stop", &Stop{ - Type: "stop", - }, nil - case AddProxyID: - ipVersion := make([]byte, 1) - - if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") - } - - var ipSize uint8 - - if ipVersion[0] == 4 { - ipSize = IPv4Size - } else if ipVersion[0] == 6 { - ipSize = IPv6Size - } else { - return "", nil, fmt.Errorf("invalid IP version recieved") - } - - ip := make(net.IP, ipSize) - - if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") - } - - destPort := make([]byte, 2) - - if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") - } - - protocolBytes := make([]byte, 1) - - if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") - } - - var protocol string - - if protocolBytes[0] == TCP { - protocol = "tcp" - } else if protocolBytes[1] == UDP { - protocol = "udp" - } else { - return "", nil, fmt.Errorf("invalid protocol") - } - - return "addProxy", &AddProxy{ - Type: "addProxy", - SourceIP: ip.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destPort), - Protocol: protocol, - }, nil - case RemoveProxyID: - ipVersion := make([]byte, 1) - - if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") - } - - var ipSize uint8 - - if ipVersion[0] == 4 { - ipSize = IPv4Size - } else if ipVersion[0] == 6 { - ipSize = IPv6Size - } else { - return "", nil, fmt.Errorf("invalid IP version recieved") - } - - ip := make(net.IP, ipSize) - - if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") - } - - destPort := make([]byte, 2) - - if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") - } - - protocolBytes := make([]byte, 1) - - if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") - } - - var protocol string - - if protocolBytes[0] == TCP { - protocol = "tcp" - } else if protocolBytes[1] == UDP { - protocol = "udp" - } else { - return "", nil, fmt.Errorf("invalid protocol") - } - - return "removeProxy", &RemoveProxy{ - Type: "removeProxy", - SourceIP: ip.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destPort), - Protocol: protocol, - }, nil - case ProxyConnectionsResponseID: - connections := []*ProxyClientConnection{} - delimiter := make([]byte, 1) - var errorReturn error - - // Infinite loop because we don't know the length - for { - connection, err := unmarshalIndividualConnectionStruct(conn) - - if err != nil { - if err.Error() == "no data found" { - break - } - - return "", nil, err - } - - connections = append(connections, connection) - - if _, err := conn.Read(delimiter); err != nil { - return "", nil, fmt.Errorf("couldn't read delimiter") - } - - if delimiter[0] == '\r' { - continue - } else if delimiter[0] == '\n' { - break - } else { - // WTF? This shouldn't happen. Break out and return, but give an error - errorReturn = fmt.Errorf("invalid delimiter recieved while processing stream") - break - } - } - - return "proxyConnectionsResponse", &ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", - Connections: connections, - }, errorReturn - case CheckClientParametersID: - ipVersion := make([]byte, 1) - - if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") - } - - var ipSize uint8 - - if ipVersion[0] == 4 { - ipSize = IPv4Size - } else if ipVersion[0] == 6 { - ipSize = IPv6Size - } else { - return "", nil, fmt.Errorf("invalid IP version recieved") - } - - ip := make(net.IP, ipSize) - - if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") - } - - destPort := make([]byte, 2) - - if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") - } - - protocolBytes := make([]byte, 1) - - if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") - } - - var protocol string - - if protocolBytes[0] == TCP { - protocol = "tcp" - } else if protocolBytes[1] == UDP { - protocol = "udp" - } else { - return "", nil, fmt.Errorf("invalid protocol") - } - - return "checkClientParameters", &CheckClientParameters{ - Type: "checkClientParameters", - SourceIP: ip.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destPort), - Protocol: protocol, - }, nil - case CheckServerParametersID: - argumentsLength := make([]byte, 2) - - if _, err := conn.Read(argumentsLength); err != nil { - return "", nil, fmt.Errorf("couldn't read argument length") - } - - arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) - - if _, err := conn.Read(arguments); err != nil { - return "", nil, fmt.Errorf("couldn't read arguments") - } - - return "checkServerParameters", &CheckServerParameters{ - Type: "checkServerParameters", - Arguments: arguments, - }, nil - case CheckParametersResponseID: - checkMethodByte := make([]byte, 1) - - if _, err := conn.Read(checkMethodByte); err != nil { - return "", nil, fmt.Errorf("couldn't read check method byte") - } - - var checkMethod string - - if checkMethodByte[0] == CheckClientParametersID { - checkMethod = "checkClientParameters" - } else if checkMethodByte[0] == CheckServerParametersID { - checkMethod = "checkServerParameters" - } else { - return "", nil, fmt.Errorf("invalid check method recieved") - } - - isValid := make([]byte, 1) - - if _, err := conn.Read(isValid); err != nil { - return "", nil, fmt.Errorf("couldn't read isValid byte") - } - - messageLengthBytes := make([]byte, 2) - - if _, err := conn.Read(messageLengthBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message length") - } - - messageLength := binary.BigEndian.Uint16(messageLengthBytes) - var message string - - if messageLength != 0 { - messageBytes := make([]byte, messageLength) - - if _, err := conn.Read(messageBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message") - } - - message = string(messageBytes) - } - - return "checkParametersResponse", &CheckParametersResponse{ - Type: "checkParametersResponse", - InResponseTo: checkMethod, - IsValid: isValid[0] == 1, - Message: message, - }, nil - case BackendStatusResponseID: - isRunning := make([]byte, 1) - - if _, err := conn.Read(isRunning); err != nil { - return "", nil, fmt.Errorf("couldn't read isRunning field") - } - - statusCode := make([]byte, 1) - - if _, err := conn.Read(statusCode); err != nil { - return "", nil, fmt.Errorf("couldn't read status code field") - } - - messageLengthBytes := make([]byte, 2) - - if _, err := conn.Read(messageLengthBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message length") - } - - messageLength := binary.BigEndian.Uint16(messageLengthBytes) - var message string - - if messageLength != 0 { - messageBytes := make([]byte, messageLength) - - if _, err := conn.Read(messageBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message") - } - - message = string(messageBytes) - } - - return "backendStatusResponse", &BackendStatusResponse{ - Type: "backendStatusResponse", - IsRunning: isRunning[0] == 1, - StatusCode: int(statusCode[0]), - Message: message, - }, nil - case BackendStatusRequestID: - return "backendStatusRequest", &BackendStatusRequest{ - Type: "backendStatusRequest", - }, nil - case ProxyStatusRequestID: - ipVersion := make([]byte, 1) - - if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") - } - - var ipSize uint8 - - if ipVersion[0] == 4 { - ipSize = IPv4Size - } else if ipVersion[0] == 6 { - ipSize = IPv6Size - } else { - return "", nil, fmt.Errorf("invalid IP version recieved") - } - - ip := make(net.IP, ipSize) - - if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") - } - - destPort := make([]byte, 2) - - if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") - } - - protocolBytes := make([]byte, 1) - - if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") - } - - var protocol string - - if protocolBytes[0] == TCP { - protocol = "tcp" - } else if protocolBytes[1] == UDP { - protocol = "udp" - } else { - return "", nil, fmt.Errorf("invalid protocol") - } - - return "proxyStatusRequest", &ProxyStatusRequest{ - Type: "proxyStatusRequest", - SourceIP: ip.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destPort), - Protocol: protocol, - }, nil - case ProxyStatusResponseID: - ipVersion := make([]byte, 1) - - if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") - } - - var ipSize uint8 - - if ipVersion[0] == 4 { - ipSize = IPv4Size - } else if ipVersion[0] == 6 { - ipSize = IPv6Size - } else { - return "", nil, fmt.Errorf("invalid IP version recieved") - } - - ip := make(net.IP, ipSize) - - if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") - } - - sourcePort := make([]byte, 2) - - if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") - } - - destPort := make([]byte, 2) - - if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") - } - - protocolBytes := make([]byte, 1) - - if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") - } - - var protocol string - - if protocolBytes[0] == TCP { - protocol = "tcp" - } else if protocolBytes[1] == UDP { - protocol = "udp" - } else { - return "", nil, fmt.Errorf("invalid protocol") - } - - isActive := make([]byte, 1) - - if _, err := conn.Read(isActive); err != nil { - return "", nil, fmt.Errorf("couldn't read isActive field") - } - - return "proxyStatusResponse", &ProxyStatusResponse{ - Type: "proxyStatusResponse", - SourceIP: ip.String(), - SourcePort: binary.BigEndian.Uint16(sourcePort), - DestPort: binary.BigEndian.Uint16(destPort), - Protocol: protocol, - IsActive: isActive[0] == 1, - }, nil - case ProxyInstanceRequestID: - return "proxyInstanceRequest", &ProxyInstanceRequest{ - Type: "proxyInstanceRequest", - }, nil - case ProxyInstanceResponseID: - proxies := []*ProxyInstance{} - delimiter := make([]byte, 1) - var errorReturn error - - // Infinite loop because we don't know the length - for { - proxy, err := unmarshalIndividualProxyStruct(conn) - - if err != nil { - if err.Error() == "no data found" { - break - } - - return "", nil, err - } - - proxies = append(proxies, proxy) - - if _, err := conn.Read(delimiter); err != nil { - return "", nil, fmt.Errorf("couldn't read delimiter") - } - - if delimiter[0] == '\r' { - continue - } else if delimiter[0] == '\n' { - break - } else { - // WTF? This shouldn't happen. Break out and return, but give an error - errorReturn = fmt.Errorf("invalid delimiter recieved while processing stream") - break - } - } - - return "proxyInstanceResponse", &ProxyInstanceResponse{ - Type: "proxyInstanceResponse", - Proxies: proxies, - }, errorReturn - case ProxyConnectionsRequestID: - return "proxyConnectionsRequest", &ProxyConnectionsRequest{ - Type: "proxyConnectionsRequest", - }, nil - } - - return "", nil, fmt.Errorf("couldn't match command ID") -} From 4101ce70071386ac69de439d784afe49629d6fa6 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 10 Jan 2025 20:44:02 -0500 Subject: [PATCH 25/45] Revert "chore: Delete old commonbackend code." Wrong branch oops This reverts commit 737ba2887f1a9f58eaee98f67d7c75e9f22fc7c8. --- backend/commonbackend/constants.go | 147 ++++ backend/commonbackend/marshal.go | 507 +++++++++++++ backend/commonbackend/marshalling_test.go | 824 ++++++++++++++++++++++ backend/commonbackend/unmarshal.go | 665 +++++++++++++++++ 4 files changed, 2143 insertions(+) create mode 100644 backend/commonbackend/constants.go create mode 100644 backend/commonbackend/marshal.go create mode 100644 backend/commonbackend/marshalling_test.go create mode 100644 backend/commonbackend/unmarshal.go diff --git a/backend/commonbackend/constants.go b/backend/commonbackend/constants.go new file mode 100644 index 0000000..cdb68f2 --- /dev/null +++ b/backend/commonbackend/constants.go @@ -0,0 +1,147 @@ +package commonbackend + +type Start struct { + Type string // Will be 'start' always + Arguments []byte +} + +type Stop struct { + Type string // Will be 'stop' always +} + +type AddProxy struct { + Type string // Will be 'addProxy' always + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' +} + +type RemoveProxy struct { + Type string // Will be 'removeProxy' always + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' +} + +type ProxyStatusRequest struct { + Type string // Will be 'proxyStatusRequest' always + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' +} + +type ProxyStatusResponse struct { + Type string // Will be 'proxyStatusResponse' always + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' + IsActive bool +} + +type ProxyInstance struct { + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' +} + +type ProxyInstanceResponse struct { + Type string // Will be 'proxyConnectionResponse' always + Proxies []*ProxyInstance // List of connections +} + +type ProxyInstanceRequest struct { + Type string // Will be 'proxyConnectionRequest' always +} + +type BackendStatusResponse struct { + Type string // Will be 'backendStatusResponse' always + IsRunning bool // True if running, false if not running + StatusCode int // Either the 'Success' or 'Failure' constant + Message string // String message from the client (ex. failed to dial TCP) +} + +type BackendStatusRequest struct { + Type string // Will be 'backendStatusRequest' always +} + +type ProxyConnectionsRequest struct { + Type string // Will be 'proxyConnectionsRequest' always +} + +// Client's connection to a specific proxy +type ProxyClientConnection struct { + SourceIP string + SourcePort uint16 + DestPort uint16 + ClientIP string + ClientPort uint16 +} + +type ProxyConnectionsResponse struct { + Type string // Will be 'proxyConnectionsResponse' always + Connections []*ProxyClientConnection // List of connections +} + +type CheckClientParameters struct { + Type string // Will be 'checkClientParameters' always + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' +} + +type CheckServerParameters struct { + Type string // Will be 'checkServerParameters' always + Arguments []byte +} + +// Sent as a response to either CheckClientParameters or CheckBackendParameters +type CheckParametersResponse struct { + Type string // Will be 'checkParametersResponse' always + InResponseTo string // Will be either 'checkClientParameters' or 'checkServerParameters' + IsValid bool // If true, valid, and if false, invalid + Message string // String message from the client (ex. failed to unmarshal JSON: x is not defined) +} + +const ( + StartID = iota + StopID + AddProxyID + RemoveProxyID + ProxyConnectionsResponseID + CheckClientParametersID + CheckServerParametersID + CheckParametersResponseID + ProxyConnectionsRequestID + BackendStatusResponseID + BackendStatusRequestID + ProxyStatusRequestID + ProxyStatusResponseID + ProxyInstanceResponseID + ProxyInstanceRequestID +) + +const ( + TCP = iota + UDP +) + +const ( + StatusSuccess = iota + StatusFailure +) + +const ( + // IP versions + IPv4 = 4 + IPv6 = 6 + + // TODO: net has these constants defined already. We should switch to these + IPv4Size = 4 + IPv6Size = 16 +) diff --git a/backend/commonbackend/marshal.go b/backend/commonbackend/marshal.go new file mode 100644 index 0000000..6baf02e --- /dev/null +++ b/backend/commonbackend/marshal.go @@ -0,0 +1,507 @@ +package commonbackend + +import ( + "encoding/binary" + "fmt" + "net" +) + +func marshalIndividualConnectionStruct(conn *ProxyClientConnection) []byte { + sourceIPOriginal := net.ParseIP(conn.SourceIP) + clientIPOriginal := net.ParseIP(conn.ClientIP) + + var serverIPVer uint8 + var sourceIP []byte + + if sourceIPOriginal.To4() == nil { + serverIPVer = IPv6 + sourceIP = sourceIPOriginal.To16() + } else { + serverIPVer = IPv4 + sourceIP = sourceIPOriginal.To4() + } + + var clientIPVer uint8 + var clientIP []byte + + if clientIPOriginal.To4() == nil { + clientIPVer = IPv6 + clientIP = clientIPOriginal.To16() + } else { + clientIPVer = IPv4 + clientIP = clientIPOriginal.To4() + } + + connectionBlock := make([]byte, 8+len(sourceIP)+len(clientIP)) + + connectionBlock[0] = serverIPVer + copy(connectionBlock[1:len(sourceIP)+1], sourceIP) + + binary.BigEndian.PutUint16(connectionBlock[1+len(sourceIP):3+len(sourceIP)], conn.SourcePort) + binary.BigEndian.PutUint16(connectionBlock[3+len(sourceIP):5+len(sourceIP)], conn.DestPort) + + connectionBlock[5+len(sourceIP)] = clientIPVer + copy(connectionBlock[6+len(sourceIP):6+len(sourceIP)+len(clientIP)], clientIP) + binary.BigEndian.PutUint16(connectionBlock[6+len(sourceIP)+len(clientIP):8+len(sourceIP)+len(clientIP)], conn.ClientPort) + + return connectionBlock +} + +func marshalIndividualProxyStruct(conn *ProxyInstance) ([]byte, error) { + sourceIPOriginal := net.ParseIP(conn.SourceIP) + + var sourceIPVer uint8 + var sourceIP []byte + + if sourceIPOriginal.To4() == nil { + sourceIPVer = IPv6 + sourceIP = sourceIPOriginal.To16() + } else { + sourceIPVer = IPv4 + sourceIP = sourceIPOriginal.To4() + } + + proxyBlock := make([]byte, 6+len(sourceIP)) + + proxyBlock[0] = sourceIPVer + copy(proxyBlock[1:len(sourceIP)+1], sourceIP) + + binary.BigEndian.PutUint16(proxyBlock[1+len(sourceIP):3+len(sourceIP)], conn.SourcePort) + binary.BigEndian.PutUint16(proxyBlock[3+len(sourceIP):5+len(sourceIP)], conn.DestPort) + + var protocolVersion uint8 + + if conn.Protocol == "tcp" { + protocolVersion = TCP + } else if conn.Protocol == "udp" { + protocolVersion = UDP + } else { + return proxyBlock, fmt.Errorf("invalid protocol recieved") + } + + proxyBlock[5+len(sourceIP)] = protocolVersion + + return proxyBlock, nil +} + +func Marshal(commandType string, command interface{}) ([]byte, error) { + switch commandType { + case "start": + startCommand, ok := command.(*Start) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + startCommandBytes := make([]byte, 1+2+len(startCommand.Arguments)) + startCommandBytes[0] = StartID + binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(startCommand.Arguments))) + copy(startCommandBytes[3:], startCommand.Arguments) + + return startCommandBytes, nil + case "stop": + _, ok := command.(*Stop) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + return []byte{StopID}, nil + case "addProxy": + addConnectionCommand, ok := command.(*AddProxy) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(addConnectionCommand.SourceIP) + + var ipVer uint8 + var ipBytes []byte + + if sourceIP.To4() == nil { + ipBytes = sourceIP.To16() + ipVer = IPv6 + } else { + ipBytes = sourceIP.To4() + ipVer = IPv4 + } + + addConnectionBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) + + addConnectionBytes[0] = AddProxyID + addConnectionBytes[1] = ipVer + + copy(addConnectionBytes[2:2+len(ipBytes)], ipBytes) + + binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], addConnectionCommand.SourcePort) + binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], addConnectionCommand.DestPort) + + var protocol uint8 + + if addConnectionCommand.Protocol == "tcp" { + protocol = TCP + } else if addConnectionCommand.Protocol == "udp" { + protocol = UDP + } else { + return nil, fmt.Errorf("invalid protocol") + } + + addConnectionBytes[6+len(ipBytes)] = protocol + + return addConnectionBytes, nil + case "removeProxy": + removeConnectionCommand, ok := command.(*RemoveProxy) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(removeConnectionCommand.SourceIP) + + var ipVer uint8 + var ipBytes []byte + + if sourceIP.To4() == nil { + ipBytes = sourceIP.To16() + ipVer = IPv6 + } else { + ipBytes = sourceIP.To4() + ipVer = IPv4 + } + + removeConnectionBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) + + removeConnectionBytes[0] = RemoveProxyID + removeConnectionBytes[1] = ipVer + copy(removeConnectionBytes[2:2+len(ipBytes)], ipBytes) + binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], removeConnectionCommand.SourcePort) + binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], removeConnectionCommand.DestPort) + + var protocol uint8 + + if removeConnectionCommand.Protocol == "tcp" { + protocol = TCP + } else if removeConnectionCommand.Protocol == "udp" { + protocol = UDP + } else { + return nil, fmt.Errorf("invalid protocol") + } + + removeConnectionBytes[6+len(ipBytes)] = protocol + + return removeConnectionBytes, nil + case "proxyConnectionsResponse": + allConnectionsCommand, ok := command.(*ProxyConnectionsResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + connectionsArray := make([][]byte, len(allConnectionsCommand.Connections)) + totalSize := 0 + + for connIndex, conn := range allConnectionsCommand.Connections { + connectionsArray[connIndex] = marshalIndividualConnectionStruct(conn) + totalSize += len(connectionsArray[connIndex]) + 1 + } + + if totalSize == 0 { + totalSize = 1 + } + + connectionCommandArray := make([]byte, totalSize+1) + connectionCommandArray[0] = ProxyConnectionsResponseID + + currentPosition := 1 + + for _, connection := range connectionsArray { + copy(connectionCommandArray[currentPosition:currentPosition+len(connection)], connection) + connectionCommandArray[currentPosition+len(connection)] = '\r' + currentPosition += len(connection) + 1 + } + + connectionCommandArray[totalSize] = '\n' + return connectionCommandArray, nil + case "checkClientParameters": + checkClientCommand, ok := command.(*CheckClientParameters) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(checkClientCommand.SourceIP) + + var ipVer uint8 + var ipBytes []byte + + if sourceIP.To4() == nil { + ipBytes = sourceIP.To16() + ipVer = IPv6 + } else { + ipBytes = sourceIP.To4() + ipVer = IPv4 + } + + checkClientBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) + + checkClientBytes[0] = CheckClientParametersID + checkClientBytes[1] = ipVer + copy(checkClientBytes[2:2+len(ipBytes)], ipBytes) + binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], checkClientCommand.SourcePort) + binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], checkClientCommand.DestPort) + + var protocol uint8 + + if checkClientCommand.Protocol == "tcp" { + protocol = TCP + } else if checkClientCommand.Protocol == "udp" { + protocol = UDP + } else { + return nil, fmt.Errorf("invalid protocol") + } + + checkClientBytes[6+len(ipBytes)] = protocol + + return checkClientBytes, nil + case "checkServerParameters": + checkServerCommand, ok := command.(*CheckServerParameters) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + serverCommandBytes := make([]byte, 1+2+len(checkServerCommand.Arguments)) + serverCommandBytes[0] = CheckServerParametersID + binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(checkServerCommand.Arguments))) + copy(serverCommandBytes[3:], checkServerCommand.Arguments) + + return serverCommandBytes, nil + case "checkParametersResponse": + checkParametersCommand, ok := command.(*CheckParametersResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + var checkMethod uint8 + + if checkParametersCommand.InResponseTo == "checkClientParameters" { + checkMethod = CheckClientParametersID + } else if checkParametersCommand.InResponseTo == "checkServerParameters" { + checkMethod = CheckServerParametersID + } else { + return nil, fmt.Errorf("invalid mode recieved (must be either checkClientParameters or checkServerParameters)") + } + + var isValid uint8 + + if checkParametersCommand.IsValid { + isValid = 1 + } + + checkResponseBytes := make([]byte, 3+2+len(checkParametersCommand.Message)) + checkResponseBytes[0] = CheckParametersResponseID + checkResponseBytes[1] = checkMethod + checkResponseBytes[2] = isValid + + binary.BigEndian.PutUint16(checkResponseBytes[3:5], uint16(len(checkParametersCommand.Message))) + + if len(checkParametersCommand.Message) != 0 { + copy(checkResponseBytes[5:], []byte(checkParametersCommand.Message)) + } + + return checkResponseBytes, nil + case "backendStatusResponse": + backendStatusResponse, ok := command.(*BackendStatusResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + var isRunning uint8 + + if backendStatusResponse.IsRunning { + isRunning = 1 + } else { + isRunning = 0 + } + + statusResponseBytes := make([]byte, 3+2+len(backendStatusResponse.Message)) + statusResponseBytes[0] = BackendStatusResponseID + statusResponseBytes[1] = isRunning + statusResponseBytes[2] = byte(backendStatusResponse.StatusCode) + + binary.BigEndian.PutUint16(statusResponseBytes[3:5], uint16(len(backendStatusResponse.Message))) + + if len(backendStatusResponse.Message) != 0 { + copy(statusResponseBytes[5:], []byte(backendStatusResponse.Message)) + } + + return statusResponseBytes, nil + case "backendStatusRequest": + _, ok := command.(*BackendStatusRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + statusRequestBytes := make([]byte, 1) + statusRequestBytes[0] = BackendStatusRequestID + + return statusRequestBytes, nil + case "proxyStatusRequest": + proxyStatusRequest, ok := command.(*ProxyStatusRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(proxyStatusRequest.SourceIP) + + var ipVer uint8 + var ipBytes []byte + + if sourceIP.To4() == nil { + ipBytes = sourceIP.To16() + ipVer = IPv6 + } else { + ipBytes = sourceIP.To4() + ipVer = IPv4 + } + + proxyStatusRequestBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) + + proxyStatusRequestBytes[0] = ProxyStatusRequestID + proxyStatusRequestBytes[1] = ipVer + + copy(proxyStatusRequestBytes[2:2+len(ipBytes)], ipBytes) + + binary.BigEndian.PutUint16(proxyStatusRequestBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusRequest.SourcePort) + binary.BigEndian.PutUint16(proxyStatusRequestBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusRequest.DestPort) + + var protocol uint8 + + if proxyStatusRequest.Protocol == "tcp" { + protocol = TCP + } else if proxyStatusRequest.Protocol == "udp" { + protocol = UDP + } else { + return nil, fmt.Errorf("invalid protocol") + } + + proxyStatusRequestBytes[6+len(ipBytes)] = protocol + + return proxyStatusRequestBytes, nil + case "proxyStatusResponse": + proxyStatusResponse, ok := command.(*ProxyStatusResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + sourceIP := net.ParseIP(proxyStatusResponse.SourceIP) + + var ipVer uint8 + var ipBytes []byte + + if sourceIP.To4() == nil { + ipBytes = sourceIP.To16() + ipVer = IPv6 + } else { + ipBytes = sourceIP.To4() + ipVer = IPv4 + } + + proxyStatusResponseBytes := make([]byte, 1+1+len(ipBytes)+2+2+1+1) + + proxyStatusResponseBytes[0] = ProxyStatusResponseID + proxyStatusResponseBytes[1] = ipVer + + copy(proxyStatusResponseBytes[2:2+len(ipBytes)], ipBytes) + + binary.BigEndian.PutUint16(proxyStatusResponseBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusResponse.SourcePort) + binary.BigEndian.PutUint16(proxyStatusResponseBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusResponse.DestPort) + + var protocol uint8 + + if proxyStatusResponse.Protocol == "tcp" { + protocol = TCP + } else if proxyStatusResponse.Protocol == "udp" { + protocol = UDP + } else { + return nil, fmt.Errorf("invalid protocol") + } + + proxyStatusResponseBytes[6+len(ipBytes)] = protocol + + var isActive uint8 + + if proxyStatusResponse.IsActive { + isActive = 1 + } else { + isActive = 0 + } + + proxyStatusResponseBytes[7+len(ipBytes)] = isActive + + return proxyStatusResponseBytes, nil + case "proxyInstanceResponse": + proxyConectionResponse, ok := command.(*ProxyInstanceResponse) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + proxyArray := make([][]byte, len(proxyConectionResponse.Proxies)) + totalSize := 0 + + for proxyIndex, proxy := range proxyConectionResponse.Proxies { + var err error + proxyArray[proxyIndex], err = marshalIndividualProxyStruct(proxy) + + if err != nil { + return nil, err + } + + totalSize += len(proxyArray[proxyIndex]) + 1 + } + + if totalSize == 0 { + totalSize = 1 + } + + connectionCommandArray := make([]byte, totalSize+1) + connectionCommandArray[0] = ProxyInstanceResponseID + + currentPosition := 1 + + for _, connection := range proxyArray { + copy(connectionCommandArray[currentPosition:currentPosition+len(connection)], connection) + connectionCommandArray[currentPosition+len(connection)] = '\r' + currentPosition += len(connection) + 1 + } + + connectionCommandArray[totalSize] = '\n' + + return connectionCommandArray, nil + case "proxyInstanceRequest": + _, ok := command.(*ProxyInstanceRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + return []byte{ProxyInstanceRequestID}, nil + case "proxyConnectionsRequest": + _, ok := command.(*ProxyConnectionsRequest) + + if !ok { + return nil, fmt.Errorf("failed to typecast") + } + + return []byte{ProxyConnectionsRequestID}, nil + } + + return nil, fmt.Errorf("couldn't match command name") +} diff --git a/backend/commonbackend/marshalling_test.go b/backend/commonbackend/marshalling_test.go new file mode 100644 index 0000000..1c93f94 --- /dev/null +++ b/backend/commonbackend/marshalling_test.go @@ -0,0 +1,824 @@ +package commonbackend + +import ( + "bytes" + "log" + "os" + "testing" +) + +var logLevel = os.Getenv("HERMES_LOG_LEVEL") + +func TestStartCommandMarshalSupport(t *testing.T) { + commandInput := &Start{ + Type: "start", + Arguments: []byte("Hello from automated testing"), + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*Start) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { + log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) + } +} + +func TestStopCommandMarshalSupport(t *testing.T) { + commandInput := &Stop{ + Type: "stop", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*Stop) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } +} + +func TestAddConnectionCommandMarshalSupport(t *testing.T) { + commandInput := &AddProxy{ + Type: "addProxy", + SourceIP: "192.168.0.139", + SourcePort: 19132, + DestPort: 19132, + Protocol: "tcp", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*AddProxy) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.SourceIP != commandUnmarshalled.SourceIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) + } + + if commandInput.SourcePort != commandUnmarshalled.SourcePort { + t.Fail() + log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) + } + + if commandInput.DestPort != commandUnmarshalled.DestPort { + t.Fail() + log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) + } + + if commandInput.Protocol != commandUnmarshalled.Protocol { + t.Fail() + log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) + } +} + +func TestRemoveConnectionCommandMarshalSupport(t *testing.T) { + commandInput := &RemoveProxy{ + Type: "removeProxy", + SourceIP: "192.168.0.139", + SourcePort: 19132, + DestPort: 19132, + Protocol: "tcp", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.SourceIP != commandUnmarshalled.SourceIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) + } + + if commandInput.SourcePort != commandUnmarshalled.SourcePort { + t.Fail() + log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) + } + + if commandInput.DestPort != commandUnmarshalled.DestPort { + t.Fail() + log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) + } + + if commandInput.Protocol != commandUnmarshalled.Protocol { + t.Fail() + log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) + } +} + +func TestGetAllConnectionsCommandMarshalSupport(t *testing.T) { + commandInput := &ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", + Connections: []*ProxyClientConnection{ + { + SourceIP: "127.0.0.1", + SourcePort: 19132, + DestPort: 19132, + ClientIP: "127.0.0.1", + ClientPort: 12321, + }, + { + SourceIP: "127.0.0.1", + SourcePort: 19132, + DestPort: 19132, + ClientIP: "192.168.0.168", + ClientPort: 23457, + }, + { + SourceIP: "127.0.0.1", + SourcePort: 19132, + DestPort: 19132, + ClientIP: "68.42.203.47", + ClientPort: 38721, + }, + }, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + for commandIndex, originalConnection := range commandInput.Connections { + remoteConnection := commandUnmarshalled.Connections[commandIndex] + + if originalConnection.SourceIP != remoteConnection.SourceIP { + t.Fail() + log.Printf("(in #%d) SourceIP's are not equal (orig: %s, unmsh: %s)", commandIndex, originalConnection.SourceIP, remoteConnection.SourceIP) + } + + if originalConnection.SourcePort != remoteConnection.SourcePort { + t.Fail() + log.Printf("(in #%d) SourcePort's are not equal (orig: %d, unmsh: %d)", commandIndex, originalConnection.SourcePort, remoteConnection.SourcePort) + } + + if originalConnection.DestPort != remoteConnection.DestPort { + t.Fail() + log.Printf("(in #%d) DestPort's are not equal (orig: %d, unmsh: %d)", commandIndex, originalConnection.DestPort, remoteConnection.DestPort) + } + + if originalConnection.ClientIP != remoteConnection.ClientIP { + t.Fail() + log.Printf("(in #%d) ClientIP's are not equal (orig: %s, unmsh: %s)", commandIndex, originalConnection.ClientIP, remoteConnection.ClientIP) + } + + if originalConnection.ClientPort != remoteConnection.ClientPort { + t.Fail() + log.Printf("(in #%d) ClientPort's are not equal (orig: %d, unmsh: %d)", commandIndex, originalConnection.ClientPort, remoteConnection.ClientPort) + } + } +} + +func TestCheckClientParametersMarshalSupport(t *testing.T) { + commandInput := &CheckClientParameters{ + Type: "checkClientParameters", + SourceIP: "192.168.0.139", + SourcePort: 19132, + DestPort: 19132, + Protocol: "tcp", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckClientParameters) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.SourceIP != commandUnmarshalled.SourceIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) + } + + if commandInput.SourcePort != commandUnmarshalled.SourcePort { + t.Fail() + log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) + } + + if commandInput.DestPort != commandUnmarshalled.DestPort { + t.Fail() + log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) + } + + if commandInput.Protocol != commandUnmarshalled.Protocol { + t.Fail() + log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) + } +} + +func TestCheckServerParametersMarshalSupport(t *testing.T) { + commandInput := &CheckServerParameters{ + Type: "checkServerParameters", + Arguments: []byte("Hello from automated testing"), + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckServerParameters) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { + log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) + } +} + +func TestCheckParametersResponseMarshalSupport(t *testing.T) { + commandInput := &CheckParametersResponse{ + Type: "checkParametersResponse", + InResponseTo: "checkClientParameters", + IsValid: true, + Message: "Hello from automated testing", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckParametersResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.InResponseTo != commandUnmarshalled.InResponseTo { + t.Fail() + log.Printf("InResponseTo's are not equal (orig: %s, unmsh: %s)", commandInput.InResponseTo, commandUnmarshalled.InResponseTo) + } + + if commandInput.IsValid != commandUnmarshalled.IsValid { + t.Fail() + log.Printf("IsValid's are not equal (orig: %t, unmsh: %t)", commandInput.IsValid, commandUnmarshalled.IsValid) + } + + if commandInput.Message != commandUnmarshalled.Message { + t.Fail() + log.Printf("Messages are not equal (orig: %s, unmsh: %s)", commandInput.Message, commandUnmarshalled.Message) + } +} + +func TestBackendStatusRequestMarshalSupport(t *testing.T) { + commandInput := &BackendStatusRequest{ + Type: "backendStatusRequest", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } +} + +func TestBackendStatusResponseMarshalSupport(t *testing.T) { + commandInput := &BackendStatusResponse{ + Type: "backendStatusResponse", + IsRunning: true, + StatusCode: StatusFailure, + Message: "Hello from automated testing", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.IsRunning != commandUnmarshalled.IsRunning { + t.Fail() + log.Printf("IsRunning's are not equal (orig: %t, unmsh: %t)", commandInput.IsRunning, commandUnmarshalled.IsRunning) + } + + if commandInput.StatusCode != commandUnmarshalled.StatusCode { + t.Fail() + log.Printf("StatusCodes are not equal (orig: %d, unmsh: %d)", commandInput.StatusCode, commandUnmarshalled.StatusCode) + } + + if commandInput.Message != commandUnmarshalled.Message { + t.Fail() + log.Printf("Messages are not equal (orig: %s, unmsh: %s)", commandInput.Message, commandUnmarshalled.Message) + } +} + +func TestProxyStatusRequestMarshalSupport(t *testing.T) { + commandInput := &ProxyStatusRequest{ + Type: "proxyStatusRequest", + SourceIP: "192.168.0.139", + SourcePort: 19132, + DestPort: 19132, + Protocol: "tcp", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.SourceIP != commandUnmarshalled.SourceIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) + } + + if commandInput.SourcePort != commandUnmarshalled.SourcePort { + t.Fail() + log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) + } + + if commandInput.DestPort != commandUnmarshalled.DestPort { + t.Fail() + log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) + } + + if commandInput.Protocol != commandUnmarshalled.Protocol { + t.Fail() + log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) + } +} + +func TestProxyStatusResponseMarshalSupport(t *testing.T) { + commandInput := &ProxyStatusResponse{ + Type: "proxyStatusResponse", + SourceIP: "192.168.0.139", + SourcePort: 19132, + DestPort: 19132, + Protocol: "tcp", + IsActive: true, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.SourceIP != commandUnmarshalled.SourceIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) + } + + if commandInput.SourcePort != commandUnmarshalled.SourcePort { + t.Fail() + log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) + } + + if commandInput.DestPort != commandUnmarshalled.DestPort { + t.Fail() + log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) + } + + if commandInput.Protocol != commandUnmarshalled.Protocol { + t.Fail() + log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) + } + + if commandInput.IsActive != commandUnmarshalled.IsActive { + t.Fail() + log.Printf("IsActive's are not equal (orig: %t, unmsh: %t)", commandInput.IsActive, commandUnmarshalled.IsActive) + } +} + +func TestProxyConnectionRequestMarshalSupport(t *testing.T) { + commandInput := &ProxyInstanceRequest{ + Type: "proxyInstanceRequest", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } +} + +func TestProxyConnectionResponseMarshalSupport(t *testing.T) { + commandInput := &ProxyInstanceResponse{ + Type: "proxyInstanceResponse", + Proxies: []*ProxyInstance{ + { + SourceIP: "192.168.0.168", + SourcePort: 25565, + DestPort: 25565, + Protocol: "tcp", + }, + { + SourceIP: "127.0.0.1", + SourcePort: 19132, + DestPort: 19132, + Protocol: "udp", + }, + { + SourceIP: "68.42.203.47", + SourcePort: 22, + DestPort: 2222, + Protocol: "tcp", + }, + }, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + for proxyIndex, originalProxy := range commandInput.Proxies { + remoteProxy := commandUnmarshalled.Proxies[proxyIndex] + + if originalProxy.SourceIP != remoteProxy.SourceIP { + t.Fail() + log.Printf("(in #%d) SourceIP's are not equal (orig: %s, unmsh: %s)", proxyIndex, originalProxy.SourceIP, remoteProxy.SourceIP) + } + + if originalProxy.SourcePort != remoteProxy.SourcePort { + t.Fail() + log.Printf("(in #%d) SourcePort's are not equal (orig: %d, unmsh: %d)", proxyIndex, originalProxy.SourcePort, remoteProxy.SourcePort) + } + + if originalProxy.DestPort != remoteProxy.DestPort { + t.Fail() + log.Printf("(in #%d) DestPort's are not equal (orig: %d, unmsh: %d)", proxyIndex, originalProxy.DestPort, remoteProxy.DestPort) + } + + if originalProxy.Protocol != remoteProxy.Protocol { + t.Fail() + log.Printf("(in #%d) ClientIP's are not equal (orig: %s, unmsh: %s)", proxyIndex, originalProxy.Protocol, remoteProxy.Protocol) + } + } +} diff --git a/backend/commonbackend/unmarshal.go b/backend/commonbackend/unmarshal.go new file mode 100644 index 0000000..b8500dd --- /dev/null +++ b/backend/commonbackend/unmarshal.go @@ -0,0 +1,665 @@ +package commonbackend + +import ( + "encoding/binary" + "fmt" + "io" + "net" +) + +func unmarshalIndividualConnectionStruct(conn io.Reader) (*ProxyClientConnection, error) { + serverIPVersion := make([]byte, 1) + + if _, err := conn.Read(serverIPVersion); err != nil { + return nil, fmt.Errorf("couldn't read server IP version") + } + + var serverIPSize uint8 + + if serverIPVersion[0] == 4 { + serverIPSize = IPv4Size + } else if serverIPVersion[0] == 6 { + serverIPSize = IPv6Size + } else if serverIPVersion[0] == '\n' { + return nil, fmt.Errorf("no data found") + } else { + return nil, fmt.Errorf("invalid server IP version recieved") + } + + serverIP := make(net.IP, serverIPSize) + + if _, err := conn.Read(serverIP); err != nil { + return nil, fmt.Errorf("couldn't read server IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return nil, fmt.Errorf("couldn't read source port") + } + + destinationPort := make([]byte, 2) + + if _, err := conn.Read(destinationPort); err != nil { + return nil, fmt.Errorf("couldn't read source port") + } + + clientIPVersion := make([]byte, 1) + + if _, err := conn.Read(clientIPVersion); err != nil { + return nil, fmt.Errorf("couldn't read server IP version") + } + + var clientIPSize uint8 + + if clientIPVersion[0] == 4 { + clientIPSize = IPv4Size + } else if clientIPVersion[0] == 6 { + clientIPSize = IPv6Size + } else { + return nil, fmt.Errorf("invalid server IP version recieved") + } + + clientIP := make(net.IP, clientIPSize) + + if _, err := conn.Read(clientIP); err != nil { + return nil, fmt.Errorf("couldn't read server IP") + } + + clientPort := make([]byte, 2) + + if _, err := conn.Read(clientPort); err != nil { + return nil, fmt.Errorf("couldn't read source port") + } + + return &ProxyClientConnection{ + SourceIP: serverIP.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destinationPort), + ClientIP: clientIP.String(), + ClientPort: binary.BigEndian.Uint16(clientPort), + }, nil +} + +func unmarshalIndividualProxyStruct(conn io.Reader) (*ProxyInstance, error) { + ipVersion := make([]byte, 1) + + if _, err := conn.Read(ipVersion); err != nil { + return nil, fmt.Errorf("couldn't read ip version") + } + + var ipSize uint8 + + if ipVersion[0] == 4 { + ipSize = IPv4Size + } else if ipVersion[0] == 6 { + ipSize = IPv6Size + } else if ipVersion[0] == '\n' { + return nil, fmt.Errorf("no data found") + } else { + return nil, fmt.Errorf("invalid IP version recieved") + } + + ip := make(net.IP, ipSize) + + if _, err := conn.Read(ip); err != nil { + return nil, fmt.Errorf("couldn't read source IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return nil, fmt.Errorf("couldn't read source port") + } + + destPort := make([]byte, 2) + + if _, err := conn.Read(destPort); err != nil { + return nil, fmt.Errorf("couldn't read destination port") + } + + protocolBytes := make([]byte, 1) + + if _, err := conn.Read(protocolBytes); err != nil { + return nil, fmt.Errorf("couldn't read protocol") + } + + var protocol string + + if protocolBytes[0] == TCP { + protocol = "tcp" + } else if protocolBytes[0] == UDP { + protocol = "udp" + } else { + return nil, fmt.Errorf("invalid protocol") + } + + return &ProxyInstance{ + SourceIP: ip.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destPort), + Protocol: protocol, + }, nil +} + +func Unmarshal(conn io.Reader) (string, interface{}, error) { + commandType := make([]byte, 1) + + if _, err := conn.Read(commandType); err != nil { + return "", nil, fmt.Errorf("couldn't read command") + } + + switch commandType[0] { + case StartID: + argumentsLength := make([]byte, 2) + + if _, err := conn.Read(argumentsLength); err != nil { + return "", nil, fmt.Errorf("couldn't read argument length") + } + + arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) + + if _, err := conn.Read(arguments); err != nil { + return "", nil, fmt.Errorf("couldn't read arguments") + } + + return "start", &Start{ + Type: "start", + Arguments: arguments, + }, nil + case StopID: + return "stop", &Stop{ + Type: "stop", + }, nil + case AddProxyID: + ipVersion := make([]byte, 1) + + if _, err := conn.Read(ipVersion); err != nil { + return "", nil, fmt.Errorf("couldn't read ip version") + } + + var ipSize uint8 + + if ipVersion[0] == 4 { + ipSize = IPv4Size + } else if ipVersion[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version recieved") + } + + ip := make(net.IP, ipSize) + + if _, err := conn.Read(ip); err != nil { + return "", nil, fmt.Errorf("couldn't read source IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return "", nil, fmt.Errorf("couldn't read source port") + } + + destPort := make([]byte, 2) + + if _, err := conn.Read(destPort); err != nil { + return "", nil, fmt.Errorf("couldn't read destination port") + } + + protocolBytes := make([]byte, 1) + + if _, err := conn.Read(protocolBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read protocol") + } + + var protocol string + + if protocolBytes[0] == TCP { + protocol = "tcp" + } else if protocolBytes[1] == UDP { + protocol = "udp" + } else { + return "", nil, fmt.Errorf("invalid protocol") + } + + return "addProxy", &AddProxy{ + Type: "addProxy", + SourceIP: ip.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destPort), + Protocol: protocol, + }, nil + case RemoveProxyID: + ipVersion := make([]byte, 1) + + if _, err := conn.Read(ipVersion); err != nil { + return "", nil, fmt.Errorf("couldn't read ip version") + } + + var ipSize uint8 + + if ipVersion[0] == 4 { + ipSize = IPv4Size + } else if ipVersion[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version recieved") + } + + ip := make(net.IP, ipSize) + + if _, err := conn.Read(ip); err != nil { + return "", nil, fmt.Errorf("couldn't read source IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return "", nil, fmt.Errorf("couldn't read source port") + } + + destPort := make([]byte, 2) + + if _, err := conn.Read(destPort); err != nil { + return "", nil, fmt.Errorf("couldn't read destination port") + } + + protocolBytes := make([]byte, 1) + + if _, err := conn.Read(protocolBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read protocol") + } + + var protocol string + + if protocolBytes[0] == TCP { + protocol = "tcp" + } else if protocolBytes[1] == UDP { + protocol = "udp" + } else { + return "", nil, fmt.Errorf("invalid protocol") + } + + return "removeProxy", &RemoveProxy{ + Type: "removeProxy", + SourceIP: ip.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destPort), + Protocol: protocol, + }, nil + case ProxyConnectionsResponseID: + connections := []*ProxyClientConnection{} + delimiter := make([]byte, 1) + var errorReturn error + + // Infinite loop because we don't know the length + for { + connection, err := unmarshalIndividualConnectionStruct(conn) + + if err != nil { + if err.Error() == "no data found" { + break + } + + return "", nil, err + } + + connections = append(connections, connection) + + if _, err := conn.Read(delimiter); err != nil { + return "", nil, fmt.Errorf("couldn't read delimiter") + } + + if delimiter[0] == '\r' { + continue + } else if delimiter[0] == '\n' { + break + } else { + // WTF? This shouldn't happen. Break out and return, but give an error + errorReturn = fmt.Errorf("invalid delimiter recieved while processing stream") + break + } + } + + return "proxyConnectionsResponse", &ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", + Connections: connections, + }, errorReturn + case CheckClientParametersID: + ipVersion := make([]byte, 1) + + if _, err := conn.Read(ipVersion); err != nil { + return "", nil, fmt.Errorf("couldn't read ip version") + } + + var ipSize uint8 + + if ipVersion[0] == 4 { + ipSize = IPv4Size + } else if ipVersion[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version recieved") + } + + ip := make(net.IP, ipSize) + + if _, err := conn.Read(ip); err != nil { + return "", nil, fmt.Errorf("couldn't read source IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return "", nil, fmt.Errorf("couldn't read source port") + } + + destPort := make([]byte, 2) + + if _, err := conn.Read(destPort); err != nil { + return "", nil, fmt.Errorf("couldn't read destination port") + } + + protocolBytes := make([]byte, 1) + + if _, err := conn.Read(protocolBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read protocol") + } + + var protocol string + + if protocolBytes[0] == TCP { + protocol = "tcp" + } else if protocolBytes[1] == UDP { + protocol = "udp" + } else { + return "", nil, fmt.Errorf("invalid protocol") + } + + return "checkClientParameters", &CheckClientParameters{ + Type: "checkClientParameters", + SourceIP: ip.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destPort), + Protocol: protocol, + }, nil + case CheckServerParametersID: + argumentsLength := make([]byte, 2) + + if _, err := conn.Read(argumentsLength); err != nil { + return "", nil, fmt.Errorf("couldn't read argument length") + } + + arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) + + if _, err := conn.Read(arguments); err != nil { + return "", nil, fmt.Errorf("couldn't read arguments") + } + + return "checkServerParameters", &CheckServerParameters{ + Type: "checkServerParameters", + Arguments: arguments, + }, nil + case CheckParametersResponseID: + checkMethodByte := make([]byte, 1) + + if _, err := conn.Read(checkMethodByte); err != nil { + return "", nil, fmt.Errorf("couldn't read check method byte") + } + + var checkMethod string + + if checkMethodByte[0] == CheckClientParametersID { + checkMethod = "checkClientParameters" + } else if checkMethodByte[0] == CheckServerParametersID { + checkMethod = "checkServerParameters" + } else { + return "", nil, fmt.Errorf("invalid check method recieved") + } + + isValid := make([]byte, 1) + + if _, err := conn.Read(isValid); err != nil { + return "", nil, fmt.Errorf("couldn't read isValid byte") + } + + messageLengthBytes := make([]byte, 2) + + if _, err := conn.Read(messageLengthBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read message length") + } + + messageLength := binary.BigEndian.Uint16(messageLengthBytes) + var message string + + if messageLength != 0 { + messageBytes := make([]byte, messageLength) + + if _, err := conn.Read(messageBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read message") + } + + message = string(messageBytes) + } + + return "checkParametersResponse", &CheckParametersResponse{ + Type: "checkParametersResponse", + InResponseTo: checkMethod, + IsValid: isValid[0] == 1, + Message: message, + }, nil + case BackendStatusResponseID: + isRunning := make([]byte, 1) + + if _, err := conn.Read(isRunning); err != nil { + return "", nil, fmt.Errorf("couldn't read isRunning field") + } + + statusCode := make([]byte, 1) + + if _, err := conn.Read(statusCode); err != nil { + return "", nil, fmt.Errorf("couldn't read status code field") + } + + messageLengthBytes := make([]byte, 2) + + if _, err := conn.Read(messageLengthBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read message length") + } + + messageLength := binary.BigEndian.Uint16(messageLengthBytes) + var message string + + if messageLength != 0 { + messageBytes := make([]byte, messageLength) + + if _, err := conn.Read(messageBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read message") + } + + message = string(messageBytes) + } + + return "backendStatusResponse", &BackendStatusResponse{ + Type: "backendStatusResponse", + IsRunning: isRunning[0] == 1, + StatusCode: int(statusCode[0]), + Message: message, + }, nil + case BackendStatusRequestID: + return "backendStatusRequest", &BackendStatusRequest{ + Type: "backendStatusRequest", + }, nil + case ProxyStatusRequestID: + ipVersion := make([]byte, 1) + + if _, err := conn.Read(ipVersion); err != nil { + return "", nil, fmt.Errorf("couldn't read ip version") + } + + var ipSize uint8 + + if ipVersion[0] == 4 { + ipSize = IPv4Size + } else if ipVersion[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version recieved") + } + + ip := make(net.IP, ipSize) + + if _, err := conn.Read(ip); err != nil { + return "", nil, fmt.Errorf("couldn't read source IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return "", nil, fmt.Errorf("couldn't read source port") + } + + destPort := make([]byte, 2) + + if _, err := conn.Read(destPort); err != nil { + return "", nil, fmt.Errorf("couldn't read destination port") + } + + protocolBytes := make([]byte, 1) + + if _, err := conn.Read(protocolBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read protocol") + } + + var protocol string + + if protocolBytes[0] == TCP { + protocol = "tcp" + } else if protocolBytes[1] == UDP { + protocol = "udp" + } else { + return "", nil, fmt.Errorf("invalid protocol") + } + + return "proxyStatusRequest", &ProxyStatusRequest{ + Type: "proxyStatusRequest", + SourceIP: ip.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destPort), + Protocol: protocol, + }, nil + case ProxyStatusResponseID: + ipVersion := make([]byte, 1) + + if _, err := conn.Read(ipVersion); err != nil { + return "", nil, fmt.Errorf("couldn't read ip version") + } + + var ipSize uint8 + + if ipVersion[0] == 4 { + ipSize = IPv4Size + } else if ipVersion[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version recieved") + } + + ip := make(net.IP, ipSize) + + if _, err := conn.Read(ip); err != nil { + return "", nil, fmt.Errorf("couldn't read source IP") + } + + sourcePort := make([]byte, 2) + + if _, err := conn.Read(sourcePort); err != nil { + return "", nil, fmt.Errorf("couldn't read source port") + } + + destPort := make([]byte, 2) + + if _, err := conn.Read(destPort); err != nil { + return "", nil, fmt.Errorf("couldn't read destination port") + } + + protocolBytes := make([]byte, 1) + + if _, err := conn.Read(protocolBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read protocol") + } + + var protocol string + + if protocolBytes[0] == TCP { + protocol = "tcp" + } else if protocolBytes[1] == UDP { + protocol = "udp" + } else { + return "", nil, fmt.Errorf("invalid protocol") + } + + isActive := make([]byte, 1) + + if _, err := conn.Read(isActive); err != nil { + return "", nil, fmt.Errorf("couldn't read isActive field") + } + + return "proxyStatusResponse", &ProxyStatusResponse{ + Type: "proxyStatusResponse", + SourceIP: ip.String(), + SourcePort: binary.BigEndian.Uint16(sourcePort), + DestPort: binary.BigEndian.Uint16(destPort), + Protocol: protocol, + IsActive: isActive[0] == 1, + }, nil + case ProxyInstanceRequestID: + return "proxyInstanceRequest", &ProxyInstanceRequest{ + Type: "proxyInstanceRequest", + }, nil + case ProxyInstanceResponseID: + proxies := []*ProxyInstance{} + delimiter := make([]byte, 1) + var errorReturn error + + // Infinite loop because we don't know the length + for { + proxy, err := unmarshalIndividualProxyStruct(conn) + + if err != nil { + if err.Error() == "no data found" { + break + } + + return "", nil, err + } + + proxies = append(proxies, proxy) + + if _, err := conn.Read(delimiter); err != nil { + return "", nil, fmt.Errorf("couldn't read delimiter") + } + + if delimiter[0] == '\r' { + continue + } else if delimiter[0] == '\n' { + break + } else { + // WTF? This shouldn't happen. Break out and return, but give an error + errorReturn = fmt.Errorf("invalid delimiter recieved while processing stream") + break + } + } + + return "proxyInstanceResponse", &ProxyInstanceResponse{ + Type: "proxyInstanceResponse", + Proxies: proxies, + }, errorReturn + case ProxyConnectionsRequestID: + return "proxyConnectionsRequest", &ProxyConnectionsRequest{ + Type: "proxyConnectionsRequest", + }, nil + } + + return "", nil, fmt.Errorf("couldn't match command ID") +} From a35602a6f24ad6e2c03fcc2e1dc9f9a3446a0f06 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 24 Jan 2025 13:26:25 -0500 Subject: [PATCH 26/45] chore: Initialize sshappbackend. --- .gitignore | 4 +- backend/build.sh | 53 ++++++++++++++++------- backend/sshappbackend/local-code/main.go | 7 +++ backend/sshappbackend/remote-code/main.go | 7 +++ 4 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 backend/sshappbackend/local-code/main.go create mode 100644 backend/sshappbackend/remote-code/main.go diff --git a/.gitignore b/.gitignore index cf34e00..7e60755 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # Go artifacts +backend/api/api backend/sshbackend/sshbackend backend/dummybackend/dummybackend +backend/sshappbackend/remote-code/bin +backend/sshappbackend/local-code/sshappbackend backend/externalbackendlauncher/externalbackendlauncher -backend/api/api frontend/frontend # Backup artifacts diff --git a/backend/build.sh b/backend/build.sh index ecb7537..6e52fef 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -1,20 +1,41 @@ #!/usr/bin/env bash -pushd sshbackend -GOOS=linux go build . -strip sshbackend -popd +pushd sshbackend > /dev/null +echo "building sshbackend" +go build -ldflags="-s -w" -trimpath . +popd > /dev/null -pushd dummybackend -GOOS=linux go build . -strip dummybackend -popd +pushd dummybackend > /dev/null +echo "building dummybackend" +go build -ldflags="-s -w" -trimpath . +popd > /dev/null -pushd externalbackendlauncher -go build . -strip externalbackendlauncher -popd +pushd externalbackendlauncher > /dev/null +echo "building externalbackendlauncher" +go build -ldflags="-s -w" -trimpath . +popd > /dev/null -pushd api -GOOS=linux go build . -strip api -popd +pushd sshappbackend/remote-code > /dev/null +echo "building sshappbackend/remote-code" +if [ ! -d bin ]; then + mkdir bin +fi + +echo " - building for arm64" +GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o bin/rt-arm64 . +echo " - building for arm" +GOOS=linux GOARCH=arm go build -ldflags="-s -w" -trimpath -o bin/rt-arm . +echo " - building for amd64" +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o bin/rt-amd64 . +echo " - building for i386" +GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o bin/rt-386 . +popd > /dev/null + +pushd sshappbackend/local-code > /dev/null +echo "building sshappbackend/local-code" +go build -ldflags="-s -w" -trimpath -o sshappbackend . +popd > /dev/null + +pushd api > /dev/null +echo "building api" +go build -ldflags="-s -w" -trimpath . +popd > /dev/null diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go new file mode 100644 index 0000000..2f8a01e --- /dev/null +++ b/backend/sshappbackend/local-code/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("lokkuh code") +} diff --git a/backend/sshappbackend/remote-code/main.go b/backend/sshappbackend/remote-code/main.go new file mode 100644 index 0000000..8a3f1cc --- /dev/null +++ b/backend/sshappbackend/remote-code/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("remottuh code") +} From ede4d528aab16c414ab7fc2546d2dd589188bb90 Mon Sep 17 00:00:00 2001 From: imterah Date: Mon, 27 Jan 2025 07:36:29 -0500 Subject: [PATCH 27/45] feature: Adds basic backend starting for sshappbackend. --- .gitignore | 2 +- backend/build.sh | 8 +- backend/externalbackendlauncher/main.go | 20 +- backend/sshappbackend/local-code/fs.go | 8 + backend/sshappbackend/local-code/logger.go | 23 ++ backend/sshappbackend/local-code/main.go | 352 ++++++++++++++++++++- go.mod | 2 + go.sum | 42 +++ 8 files changed, 434 insertions(+), 23 deletions(-) create mode 100644 backend/sshappbackend/local-code/fs.go create mode 100644 backend/sshappbackend/local-code/logger.go diff --git a/.gitignore b/.gitignore index 7e60755..d5920a3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ backend/api/api backend/sshbackend/sshbackend backend/dummybackend/dummybackend -backend/sshappbackend/remote-code/bin +backend/sshappbackend/local-code/remote-bin backend/sshappbackend/local-code/sshappbackend backend/externalbackendlauncher/externalbackendlauncher frontend/frontend diff --git a/backend/build.sh b/backend/build.sh index 6e52fef..aa5d2ca 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -21,13 +21,13 @@ if [ ! -d bin ]; then fi echo " - building for arm64" -GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o bin/rt-arm64 . +GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm64 . echo " - building for arm" -GOOS=linux GOARCH=arm go build -ldflags="-s -w" -trimpath -o bin/rt-arm . +GOOS=linux GOARCH=arm go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm . echo " - building for amd64" -GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o bin/rt-amd64 . +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-amd64 . echo " - building for i386" -GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o bin/rt-386 . +GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-386 . popd > /dev/null pushd sshappbackend/local-code > /dev/null diff --git a/backend/externalbackendlauncher/main.go b/backend/externalbackendlauncher/main.go index 4c66c6a..d33d6cd 100644 --- a/backend/externalbackendlauncher/main.go +++ b/backend/externalbackendlauncher/main.go @@ -21,11 +21,8 @@ type ProxyInstance struct { Protocol string `json:"protocol"` } -type WriteLogger struct { - UseError bool -} +type WriteLogger struct{} -// TODO: deprecate UseError switching func (writer WriteLogger) Write(p []byte) (n int, err error) { logSplit := strings.Split(string(p), "\n") @@ -34,11 +31,7 @@ func (writer WriteLogger) Write(p []byte) (n int, err error) { continue } - if writer.UseError { - log.Errorf("application: %s", line) - } else { - log.Infof("application: %s", line) - } + log.Infof("application: %s", line) } return len(p), err @@ -242,13 +235,8 @@ func entrypoint(cCtx *cli.Context) error { log.Debug("entering execution loop (in main goroutine)...") - stdout := WriteLogger{ - UseError: false, - } - - stderr := WriteLogger{ - UseError: true, - } + stdout := WriteLogger{} + stderr := WriteLogger{} for { log.Info("starting process...") diff --git a/backend/sshappbackend/local-code/fs.go b/backend/sshappbackend/local-code/fs.go new file mode 100644 index 0000000..7fe43e4 --- /dev/null +++ b/backend/sshappbackend/local-code/fs.go @@ -0,0 +1,8 @@ +package main + +import ( + "embed" +) + +//go:embed remote-bin +var binFiles embed.FS diff --git a/backend/sshappbackend/local-code/logger.go b/backend/sshappbackend/local-code/logger.go new file mode 100644 index 0000000..d8ed3f9 --- /dev/null +++ b/backend/sshappbackend/local-code/logger.go @@ -0,0 +1,23 @@ +package main + +import ( + "strings" + + "github.com/charmbracelet/log" +) + +type WriteLogger struct{} + +func (writer WriteLogger) Write(p []byte) (n int, err error) { + logSplit := strings.Split(string(p), "\n") + + for _, line := range logSplit { + if line == "" { + continue + } + + log.Infof("Process: %s", line) + } + + return len(p), err +} diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go index 2f8a01e..98a2370 100644 --- a/backend/sshappbackend/local-code/main.go +++ b/backend/sshappbackend/local-code/main.go @@ -1,7 +1,355 @@ package main -import "fmt" +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + + "git.terah.dev/imterah/hermes/backend/backendutil" + "git.terah.dev/imterah/hermes/backend/commonbackend" + "github.com/charmbracelet/log" + "github.com/go-playground/validator/v10" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" +) + +type SSHAppBackendData struct { + IP string `json:"ip" validate:"required"` + Port uint16 `json:"port" validate:"required"` + Username string `json:"username" validate:"required"` + PrivateKey string `json:"privateKey" validate:"required"` + ListenOnIPs []string `json:"listenOnIPs"` +} + +type SSHAppBackend struct { + config *SSHAppBackendData + conn *ssh.Client + clients []*commonbackend.ProxyClientConnection + arrayPropMutex sync.Mutex +} + +func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { + log.Info("SSHAppBackend is initializing...") + var backendData SSHAppBackendData + + if err := json.Unmarshal(configBytes, &backendData); err != nil { + return false, err + } + + if err := validator.New().Struct(&backendData); err != nil { + return false, err + } + + backend.config = &backendData + + if len(backend.config.ListenOnIPs) == 0 { + backend.config.ListenOnIPs = []string{"0.0.0.0"} + } + + signer, err := ssh.ParsePrivateKey([]byte(backendData.PrivateKey)) + + if err != nil { + log.Warnf("Failed to initialize: %s", err.Error()) + return false, err + } + + auth := ssh.PublicKeys(signer) + + config := &ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + User: backendData.Username, + Auth: []ssh.AuthMethod{ + auth, + }, + } + + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config) + + if err != nil { + log.Warnf("Failed to initialize: %s", err.Error()) + return false, err + } + + backend.conn = conn + + log.Info("SSHAppBackend has connected successfully.") + log.Info("Getting CPU architecture...") + + session, err := backend.conn.NewSession() + + if err != nil { + log.Warnf("Failed to create session: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + var stdoutBuf bytes.Buffer + session.Stdout = &stdoutBuf + + err = session.Run("uname -m") + + if err != nil { + log.Warnf("Failed to run uname command: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + cpuArchBytes := make([]byte, stdoutBuf.Len()) + stdoutBuf.Read(cpuArchBytes) + + cpuArch := string(cpuArchBytes) + cpuArch = cpuArch[:len(cpuArch)-1] + + var backendBinary string + + // Ordered in (subjective) popularity + if cpuArch == "x86_64" { + backendBinary = "remote-bin/rt-amd64" + } else if cpuArch == "aarch64" { + backendBinary = "remote-bin/rt-arm64" + } else if cpuArch == "arm" { + backendBinary = "remote-bin/rt-arm" + } else if len(cpuArch) == 4 && string(cpuArch[0]) == "i" && strings.HasSuffix(cpuArch, "86") { + backendBinary = "remote-bin/rt-386" + } else { + log.Warn("Failed to determine executable to use: CPU architecture not compiled/supported currently") + conn.Close() + backend.conn = nil + return false, fmt.Errorf("CPU architecture not compiled/supported currently") + } + + log.Info("Checking if we need to copy the application...") + + var binary []byte + needsToCopyBinary := true + + session, err = backend.conn.NewSession() + + if err != nil { + log.Warnf("Failed to create session: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + session.Stdout = &stdoutBuf + + err = session.Start("[ -f /tmp/sshappbackend.runtime ] && md5sum /tmp/sshappbackend.runtime | cut -d \" \" -f 1") + + if err != nil { + log.Warnf("Failed to calculate hash of possibly existing backend: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + fileExists := stdoutBuf.Len() != 0 + + if fileExists { + remoteMD5HashStringBuf := make([]byte, stdoutBuf.Len()) + stdoutBuf.Read(remoteMD5HashStringBuf) + + remoteMD5HashString := string(remoteMD5HashStringBuf) + remoteMD5HashString = remoteMD5HashString[:len(remoteMD5HashString)-1] + + remoteMD5Hash, err := hex.DecodeString(remoteMD5HashString) + + if err != nil { + log.Warnf("Failed to decode hex: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + binary, err = binFiles.ReadFile(backendBinary) + + if err != nil { + log.Warnf("Failed to read file in the embedded FS: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, fmt.Errorf("(embedded FS): %s", err.Error()) + } + + localMD5Hash := md5.Sum(binary) + + log.Infof("remote: %s, local: %s", remoteMD5HashString, hex.EncodeToString(localMD5Hash[:])) + + if bytes.Compare(localMD5Hash[:], remoteMD5Hash) == 0 { + needsToCopyBinary = false + } + } + + if needsToCopyBinary { + log.Info("Copying binary...") + sftpInstance, err := sftp.NewClient(conn) + + if err != nil { + log.Warnf("Failed to initialize SFTP: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + defer sftpInstance.Close() + + if len(binary) == 0 { + binary, err = binFiles.ReadFile(backendBinary) + + if err != nil { + log.Warnf("Failed to read file in the embedded FS: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, fmt.Errorf("(embedded FS): %s", err.Error()) + } + } + + var file *sftp.File + + if fileExists { + file, err = sftpInstance.Create("/tmp/sshappbackend.runtime") + } else { + file, err = sftpInstance.OpenFile("/tmp/sshappbackend.runtime", os.O_WRONLY) + } + + if err != nil { + log.Warnf("Failed to create file: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + _, err = file.Write(binary) + + if err != nil { + log.Warnf("Failed to write file: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + err = file.Chmod(775) + + if err != nil { + log.Warnf("Failed to change permissions on file: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + log.Info("Done copying file.") + } else { + log.Info("Skipping copying as there's a copy on disk already.") + } + + log.Info("Starting process...") + + session, err = backend.conn.NewSession() + + if err != nil { + log.Warnf("Failed to create session: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + session.Stdout = WriteLogger{} + session.Stderr = WriteLogger{} + + go session.Run("/tmp/sshappbackend.runtime") + log.Info("SSHAppBackend has initialized successfully.") + + return true, nil +} + +func (backend *SSHAppBackend) StopBackend() (bool, error) { + err := backend.conn.Close() + + if err != nil { + return false, err + } + + return true, nil +} + +func (backend *SSHAppBackend) GetBackendStatus() (bool, error) { + return backend.conn != nil, nil +} + +func (backend *SSHAppBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { + return true, nil +} + +func (backend *SSHAppBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, error) { + return false, fmt.Errorf("could not find the proxy") +} + +func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection { + return backend.clients +} + +func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { + return &commonbackend.CheckParametersResponse{ + IsValid: true, + } +} + +func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { + var backendData SSHAppBackendData + + if err := json.Unmarshal(arguments, &backendData); err != nil { + return &commonbackend.CheckParametersResponse{ + IsValid: false, + Message: fmt.Sprintf("could not read json: %s", err.Error()), + } + } + + if err := validator.New().Struct(&backendData); err != nil { + return &commonbackend.CheckParametersResponse{ + IsValid: false, + Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()), + } + } + + return &commonbackend.CheckParametersResponse{ + IsValid: true, + } +} func main() { - fmt.Println("lokkuh code") + logLevel := os.Getenv("HERMES_LOG_LEVEL") + + if logLevel != "" { + switch logLevel { + case "debug": + log.SetLevel(log.DebugLevel) + + case "info": + log.SetLevel(log.InfoLevel) + + case "warn": + log.SetLevel(log.WarnLevel) + + case "error": + log.SetLevel(log.ErrorLevel) + + case "fatal": + log.SetLevel(log.FatalLevel) + } + } + + backend := &SSHAppBackend{} + + application := backendutil.NewHelper(backend) + err := application.Start() + + if err != nil { + log.Fatalf("failed execution in application: %s", err.Error()) + } } diff --git a/go.mod b/go.mod index 0942d36..98f0e12 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -49,6 +50,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/sftp v1.13.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index da8d74e..dd30942 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -86,6 +88,8 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= +github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -114,23 +118,61 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 17e1491f96e614197c03c782a02e2cedcaadf2d3 Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 16 Feb 2025 15:02:50 -0500 Subject: [PATCH 28/45] feature: Adds basic data command support. --- backend/backendutil/application.go | 2 +- backend/backendutil/profiling_disabled.go | 2 +- backend/backendutil/profiling_enabled.go | 2 +- backend/build.sh | 10 +- backend/commonbackend/marshal.go | 250 ++---- backend/commonbackend/marshalling_test.go | 28 +- .../sshappbackend/datacommands/constants.go | 103 +++ backend/sshappbackend/datacommands/marshal.go | 323 +++++++ .../datacommands/marshalling_test.go | 828 ++++++++++++++++++ .../sshappbackend/datacommands/unmarshal.go | 435 +++++++++ .../backendutil_custom/application.go | 310 +++++++ .../backendutil_custom/structure.go | 22 + backend/sshappbackend/remote-code/main.go | 117 ++- go.mod | 4 +- 14 files changed, 2241 insertions(+), 195 deletions(-) create mode 100644 backend/sshappbackend/datacommands/constants.go create mode 100644 backend/sshappbackend/datacommands/marshal.go create mode 100644 backend/sshappbackend/datacommands/marshalling_test.go create mode 100644 backend/sshappbackend/datacommands/unmarshal.go create mode 100644 backend/sshappbackend/remote-code/backendutil_custom/application.go create mode 100644 backend/sshappbackend/remote-code/backendutil_custom/structure.go diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index 802e8c7..c6797ce 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -18,7 +18,7 @@ type BackendApplicationHelper struct { func (helper *BackendApplicationHelper) Start() error { log.Debug("BackendApplicationHelper is starting") - err := configureAndLaunchBackgroundProfilingTasks() + err := ConfigureProfiling() if err != nil { return err diff --git a/backend/backendutil/profiling_disabled.go b/backend/backendutil/profiling_disabled.go index d93cbdd..8538407 100644 --- a/backend/backendutil/profiling_disabled.go +++ b/backend/backendutil/profiling_disabled.go @@ -4,6 +4,6 @@ package backendutil var endProfileFunc func() -func configureAndLaunchBackgroundProfilingTasks() error { +func ConfigureProfiling() error { return nil } diff --git a/backend/backendutil/profiling_enabled.go b/backend/backendutil/profiling_enabled.go index a3be527..6fcb189 100644 --- a/backend/backendutil/profiling_enabled.go +++ b/backend/backendutil/profiling_enabled.go @@ -15,7 +15,7 @@ import ( "golang.org/x/exp/rand" ) -func configureAndLaunchBackgroundProfilingTasks() error { +func ConfigureProfiling() error { profilingMode, err := os.ReadFile("/tmp/hermes.backendlauncher.profilebackends") if err != nil && errors.Is(err, os.ErrNotExist) { diff --git a/backend/build.sh b/backend/build.sh index aa5d2ca..413455a 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -20,14 +20,16 @@ if [ ! -d bin ]; then mkdir bin fi +# Disable dynamic linking by disabling CGo. +# We need to make the remote code as generic as possible, so we do this echo " - building for arm64" -GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm64 . +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm64 . echo " - building for arm" -GOOS=linux GOARCH=arm go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm . +CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-arm . echo " - building for amd64" -GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-amd64 . +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-amd64 . echo " - building for i386" -GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-386 . +CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="-s -w" -trimpath -o ../local-code/remote-bin/rt-386 . popd > /dev/null pushd sshappbackend/local-code > /dev/null diff --git a/backend/commonbackend/marshal.go b/backend/commonbackend/marshal.go index 6baf02e..4496494 100644 --- a/backend/commonbackend/marshal.go +++ b/backend/commonbackend/marshal.go @@ -84,37 +84,19 @@ func marshalIndividualProxyStruct(conn *ProxyInstance) ([]byte, error) { return proxyBlock, nil } -func Marshal(commandType string, command interface{}) ([]byte, error) { - switch commandType { - case "start": - startCommand, ok := command.(*Start) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - startCommandBytes := make([]byte, 1+2+len(startCommand.Arguments)) +func Marshal(_ string, command interface{}) ([]byte, error) { + switch command := command.(type) { + case *Start: + startCommandBytes := make([]byte, 1+2+len(command.Arguments)) startCommandBytes[0] = StartID - binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(startCommand.Arguments))) - copy(startCommandBytes[3:], startCommand.Arguments) + binary.BigEndian.PutUint16(startCommandBytes[1:3], uint16(len(command.Arguments))) + copy(startCommandBytes[3:], command.Arguments) return startCommandBytes, nil - case "stop": - _, ok := command.(*Stop) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - + case *Stop: return []byte{StopID}, nil - case "addProxy": - addConnectionCommand, ok := command.(*AddProxy) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(addConnectionCommand.SourceIP) + case *AddProxy: + sourceIP := net.ParseIP(command.SourceIP) var ipVer uint8 var ipBytes []byte @@ -134,14 +116,14 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { copy(addConnectionBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], addConnectionCommand.SourcePort) - binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], addConnectionCommand.DestPort) + binary.BigEndian.PutUint16(addConnectionBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) + binary.BigEndian.PutUint16(addConnectionBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) var protocol uint8 - if addConnectionCommand.Protocol == "tcp" { + if command.Protocol == "tcp" { protocol = TCP - } else if addConnectionCommand.Protocol == "udp" { + } else if command.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") @@ -150,14 +132,8 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { addConnectionBytes[6+len(ipBytes)] = protocol return addConnectionBytes, nil - case "removeProxy": - removeConnectionCommand, ok := command.(*RemoveProxy) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(removeConnectionCommand.SourceIP) + case *RemoveProxy: + sourceIP := net.ParseIP(command.SourceIP) var ipVer uint8 var ipBytes []byte @@ -175,14 +151,14 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { removeConnectionBytes[0] = RemoveProxyID removeConnectionBytes[1] = ipVer copy(removeConnectionBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], removeConnectionCommand.SourcePort) - binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], removeConnectionCommand.DestPort) + binary.BigEndian.PutUint16(removeConnectionBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) + binary.BigEndian.PutUint16(removeConnectionBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) var protocol uint8 - if removeConnectionCommand.Protocol == "tcp" { + if command.Protocol == "tcp" { protocol = TCP - } else if removeConnectionCommand.Protocol == "udp" { + } else if command.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") @@ -191,17 +167,11 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { removeConnectionBytes[6+len(ipBytes)] = protocol return removeConnectionBytes, nil - case "proxyConnectionsResponse": - allConnectionsCommand, ok := command.(*ProxyConnectionsResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - connectionsArray := make([][]byte, len(allConnectionsCommand.Connections)) + case *ProxyConnectionsResponse: + connectionsArray := make([][]byte, len(command.Connections)) totalSize := 0 - for connIndex, conn := range allConnectionsCommand.Connections { + for connIndex, conn := range command.Connections { connectionsArray[connIndex] = marshalIndividualConnectionStruct(conn) totalSize += len(connectionsArray[connIndex]) + 1 } @@ -223,14 +193,8 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { connectionCommandArray[totalSize] = '\n' return connectionCommandArray, nil - case "checkClientParameters": - checkClientCommand, ok := command.(*CheckClientParameters) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(checkClientCommand.SourceIP) + case *CheckClientParameters: + sourceIP := net.ParseIP(command.SourceIP) var ipVer uint8 var ipBytes []byte @@ -248,14 +212,14 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { checkClientBytes[0] = CheckClientParametersID checkClientBytes[1] = ipVer copy(checkClientBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], checkClientCommand.SourcePort) - binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], checkClientCommand.DestPort) + binary.BigEndian.PutUint16(checkClientBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) + binary.BigEndian.PutUint16(checkClientBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) var protocol uint8 - if checkClientCommand.Protocol == "tcp" { + if command.Protocol == "tcp" { protocol = TCP - } else if checkClientCommand.Protocol == "udp" { + } else if command.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") @@ -264,31 +228,19 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { checkClientBytes[6+len(ipBytes)] = protocol return checkClientBytes, nil - case "checkServerParameters": - checkServerCommand, ok := command.(*CheckServerParameters) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - serverCommandBytes := make([]byte, 1+2+len(checkServerCommand.Arguments)) + case *CheckServerParameters: + serverCommandBytes := make([]byte, 1+2+len(command.Arguments)) serverCommandBytes[0] = CheckServerParametersID - binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(checkServerCommand.Arguments))) - copy(serverCommandBytes[3:], checkServerCommand.Arguments) + binary.BigEndian.PutUint16(serverCommandBytes[1:3], uint16(len(command.Arguments))) + copy(serverCommandBytes[3:], command.Arguments) return serverCommandBytes, nil - case "checkParametersResponse": - checkParametersCommand, ok := command.(*CheckParametersResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - + case *CheckParametersResponse: var checkMethod uint8 - if checkParametersCommand.InResponseTo == "checkClientParameters" { + if command.InResponseTo == "checkClientParameters" { checkMethod = CheckClientParametersID - } else if checkParametersCommand.InResponseTo == "checkServerParameters" { + } else if command.InResponseTo == "checkServerParameters" { checkMethod = CheckServerParametersID } else { return nil, fmt.Errorf("invalid mode recieved (must be either checkClientParameters or checkServerParameters)") @@ -296,68 +248,50 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { var isValid uint8 - if checkParametersCommand.IsValid { + if command.IsValid { isValid = 1 } - checkResponseBytes := make([]byte, 3+2+len(checkParametersCommand.Message)) + checkResponseBytes := make([]byte, 3+2+len(command.Message)) checkResponseBytes[0] = CheckParametersResponseID checkResponseBytes[1] = checkMethod checkResponseBytes[2] = isValid - binary.BigEndian.PutUint16(checkResponseBytes[3:5], uint16(len(checkParametersCommand.Message))) + binary.BigEndian.PutUint16(checkResponseBytes[3:5], uint16(len(command.Message))) - if len(checkParametersCommand.Message) != 0 { - copy(checkResponseBytes[5:], []byte(checkParametersCommand.Message)) + if len(command.Message) != 0 { + copy(checkResponseBytes[5:], []byte(command.Message)) } return checkResponseBytes, nil - case "backendStatusResponse": - backendStatusResponse, ok := command.(*BackendStatusResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - + case *BackendStatusResponse: var isRunning uint8 - if backendStatusResponse.IsRunning { + if command.IsRunning { isRunning = 1 } else { isRunning = 0 } - statusResponseBytes := make([]byte, 3+2+len(backendStatusResponse.Message)) + statusResponseBytes := make([]byte, 3+2+len(command.Message)) statusResponseBytes[0] = BackendStatusResponseID statusResponseBytes[1] = isRunning - statusResponseBytes[2] = byte(backendStatusResponse.StatusCode) + statusResponseBytes[2] = byte(command.StatusCode) - binary.BigEndian.PutUint16(statusResponseBytes[3:5], uint16(len(backendStatusResponse.Message))) + binary.BigEndian.PutUint16(statusResponseBytes[3:5], uint16(len(command.Message))) - if len(backendStatusResponse.Message) != 0 { - copy(statusResponseBytes[5:], []byte(backendStatusResponse.Message)) + if len(command.Message) != 0 { + copy(statusResponseBytes[5:], []byte(command.Message)) } return statusResponseBytes, nil - case "backendStatusRequest": - _, ok := command.(*BackendStatusRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - + case *BackendStatusRequest: statusRequestBytes := make([]byte, 1) statusRequestBytes[0] = BackendStatusRequestID return statusRequestBytes, nil - case "proxyStatusRequest": - proxyStatusRequest, ok := command.(*ProxyStatusRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(proxyStatusRequest.SourceIP) + case *ProxyStatusRequest: + sourceIP := net.ParseIP(command.SourceIP) var ipVer uint8 var ipBytes []byte @@ -370,37 +304,31 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { ipVer = IPv4 } - proxyStatusRequestBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) + commandBytes := make([]byte, 1+1+len(ipBytes)+2+2+1) - proxyStatusRequestBytes[0] = ProxyStatusRequestID - proxyStatusRequestBytes[1] = ipVer + commandBytes[0] = ProxyStatusRequestID + commandBytes[1] = ipVer - copy(proxyStatusRequestBytes[2:2+len(ipBytes)], ipBytes) + copy(commandBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(proxyStatusRequestBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusRequest.SourcePort) - binary.BigEndian.PutUint16(proxyStatusRequestBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusRequest.DestPort) + binary.BigEndian.PutUint16(commandBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) + binary.BigEndian.PutUint16(commandBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) var protocol uint8 - if proxyStatusRequest.Protocol == "tcp" { + if command.Protocol == "tcp" { protocol = TCP - } else if proxyStatusRequest.Protocol == "udp" { + } else if command.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") } - proxyStatusRequestBytes[6+len(ipBytes)] = protocol + commandBytes[6+len(ipBytes)] = protocol - return proxyStatusRequestBytes, nil - case "proxyStatusResponse": - proxyStatusResponse, ok := command.(*ProxyStatusResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - sourceIP := net.ParseIP(proxyStatusResponse.SourceIP) + return commandBytes, nil + case *ProxyStatusResponse: + sourceIP := net.ParseIP(command.SourceIP) var ipVer uint8 var ipBytes []byte @@ -413,50 +341,44 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { ipVer = IPv4 } - proxyStatusResponseBytes := make([]byte, 1+1+len(ipBytes)+2+2+1+1) + commandBytes := make([]byte, 1+1+len(ipBytes)+2+2+1+1) - proxyStatusResponseBytes[0] = ProxyStatusResponseID - proxyStatusResponseBytes[1] = ipVer + commandBytes[0] = ProxyStatusResponseID + commandBytes[1] = ipVer - copy(proxyStatusResponseBytes[2:2+len(ipBytes)], ipBytes) + copy(commandBytes[2:2+len(ipBytes)], ipBytes) - binary.BigEndian.PutUint16(proxyStatusResponseBytes[2+len(ipBytes):4+len(ipBytes)], proxyStatusResponse.SourcePort) - binary.BigEndian.PutUint16(proxyStatusResponseBytes[4+len(ipBytes):6+len(ipBytes)], proxyStatusResponse.DestPort) + binary.BigEndian.PutUint16(commandBytes[2+len(ipBytes):4+len(ipBytes)], command.SourcePort) + binary.BigEndian.PutUint16(commandBytes[4+len(ipBytes):6+len(ipBytes)], command.DestPort) var protocol uint8 - if proxyStatusResponse.Protocol == "tcp" { + if command.Protocol == "tcp" { protocol = TCP - } else if proxyStatusResponse.Protocol == "udp" { + } else if command.Protocol == "udp" { protocol = UDP } else { return nil, fmt.Errorf("invalid protocol") } - proxyStatusResponseBytes[6+len(ipBytes)] = protocol + commandBytes[6+len(ipBytes)] = protocol var isActive uint8 - if proxyStatusResponse.IsActive { + if command.IsActive { isActive = 1 } else { isActive = 0 } - proxyStatusResponseBytes[7+len(ipBytes)] = isActive + commandBytes[7+len(ipBytes)] = isActive - return proxyStatusResponseBytes, nil - case "proxyInstanceResponse": - proxyConectionResponse, ok := command.(*ProxyInstanceResponse) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - - proxyArray := make([][]byte, len(proxyConectionResponse.Proxies)) + return commandBytes, nil + case *ProxyInstanceResponse: + proxyArray := make([][]byte, len(command.Proxies)) totalSize := 0 - for proxyIndex, proxy := range proxyConectionResponse.Proxies { + for proxyIndex, proxy := range command.Proxies { var err error proxyArray[proxyIndex], err = marshalIndividualProxyStruct(proxy) @@ -485,23 +407,11 @@ func Marshal(commandType string, command interface{}) ([]byte, error) { connectionCommandArray[totalSize] = '\n' return connectionCommandArray, nil - case "proxyInstanceRequest": - _, ok := command.(*ProxyInstanceRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - + case *ProxyInstanceRequest: return []byte{ProxyInstanceRequestID}, nil - case "proxyConnectionsRequest": - _, ok := command.(*ProxyConnectionsRequest) - - if !ok { - return nil, fmt.Errorf("failed to typecast") - } - + case *ProxyConnectionsRequest: return []byte{ProxyConnectionsRequestID}, nil } - return nil, fmt.Errorf("couldn't match command name") + return nil, fmt.Errorf("couldn't match command type") } diff --git a/backend/commonbackend/marshalling_test.go b/backend/commonbackend/marshalling_test.go index 1c93f94..e834041 100644 --- a/backend/commonbackend/marshalling_test.go +++ b/backend/commonbackend/marshalling_test.go @@ -9,7 +9,7 @@ import ( var logLevel = os.Getenv("HERMES_LOG_LEVEL") -func TestStartCommandMarshalSupport(t *testing.T) { +func TestStart(t *testing.T) { commandInput := &Start{ Type: "start", Arguments: []byte("Hello from automated testing"), @@ -53,7 +53,7 @@ func TestStartCommandMarshalSupport(t *testing.T) { } } -func TestStopCommandMarshalSupport(t *testing.T) { +func TestStop(t *testing.T) { commandInput := &Stop{ Type: "stop", } @@ -92,7 +92,7 @@ func TestStopCommandMarshalSupport(t *testing.T) { } } -func TestAddConnectionCommandMarshalSupport(t *testing.T) { +func TestAddConnection(t *testing.T) { commandInput := &AddProxy{ Type: "addProxy", SourceIP: "192.168.0.139", @@ -155,7 +155,7 @@ func TestAddConnectionCommandMarshalSupport(t *testing.T) { } } -func TestRemoveConnectionCommandMarshalSupport(t *testing.T) { +func TestRemoveConnection(t *testing.T) { commandInput := &RemoveProxy{ Type: "removeProxy", SourceIP: "192.168.0.139", @@ -218,7 +218,7 @@ func TestRemoveConnectionCommandMarshalSupport(t *testing.T) { } } -func TestGetAllConnectionsCommandMarshalSupport(t *testing.T) { +func TestGetAllConnections(t *testing.T) { commandInput := &ProxyConnectionsResponse{ Type: "proxyConnectionsResponse", Connections: []*ProxyClientConnection{ @@ -309,7 +309,7 @@ func TestGetAllConnectionsCommandMarshalSupport(t *testing.T) { } } -func TestCheckClientParametersMarshalSupport(t *testing.T) { +func TestCheckClientParameters(t *testing.T) { commandInput := &CheckClientParameters{ Type: "checkClientParameters", SourceIP: "192.168.0.139", @@ -372,7 +372,7 @@ func TestCheckClientParametersMarshalSupport(t *testing.T) { } } -func TestCheckServerParametersMarshalSupport(t *testing.T) { +func TestCheckServerParameters(t *testing.T) { commandInput := &CheckServerParameters{ Type: "checkServerParameters", Arguments: []byte("Hello from automated testing"), @@ -416,7 +416,7 @@ func TestCheckServerParametersMarshalSupport(t *testing.T) { } } -func TestCheckParametersResponseMarshalSupport(t *testing.T) { +func TestCheckParametersResponse(t *testing.T) { commandInput := &CheckParametersResponse{ Type: "checkParametersResponse", InResponseTo: "checkClientParameters", @@ -473,7 +473,7 @@ func TestCheckParametersResponseMarshalSupport(t *testing.T) { } } -func TestBackendStatusRequestMarshalSupport(t *testing.T) { +func TestBackendStatusRequest(t *testing.T) { commandInput := &BackendStatusRequest{ Type: "backendStatusRequest", } @@ -512,7 +512,7 @@ func TestBackendStatusRequestMarshalSupport(t *testing.T) { } } -func TestBackendStatusResponseMarshalSupport(t *testing.T) { +func TestBackendStatusResponse(t *testing.T) { commandInput := &BackendStatusResponse{ Type: "backendStatusResponse", IsRunning: true, @@ -569,7 +569,7 @@ func TestBackendStatusResponseMarshalSupport(t *testing.T) { } } -func TestProxyStatusRequestMarshalSupport(t *testing.T) { +func TestProxyStatusRequest(t *testing.T) { commandInput := &ProxyStatusRequest{ Type: "proxyStatusRequest", SourceIP: "192.168.0.139", @@ -632,7 +632,7 @@ func TestProxyStatusRequestMarshalSupport(t *testing.T) { } } -func TestProxyStatusResponseMarshalSupport(t *testing.T) { +func TestProxyStatusResponse(t *testing.T) { commandInput := &ProxyStatusResponse{ Type: "proxyStatusResponse", SourceIP: "192.168.0.139", @@ -701,7 +701,7 @@ func TestProxyStatusResponseMarshalSupport(t *testing.T) { } } -func TestProxyConnectionRequestMarshalSupport(t *testing.T) { +func TestProxyConnectionRequest(t *testing.T) { commandInput := &ProxyInstanceRequest{ Type: "proxyInstanceRequest", } @@ -740,7 +740,7 @@ func TestProxyConnectionRequestMarshalSupport(t *testing.T) { } } -func TestProxyConnectionResponseMarshalSupport(t *testing.T) { +func TestProxyConnectionResponse(t *testing.T) { commandInput := &ProxyInstanceResponse{ Type: "proxyInstanceResponse", Proxies: []*ProxyInstance{ diff --git a/backend/sshappbackend/datacommands/constants.go b/backend/sshappbackend/datacommands/constants.go new file mode 100644 index 0000000..7b7620b --- /dev/null +++ b/backend/sshappbackend/datacommands/constants.go @@ -0,0 +1,103 @@ +package datacommands + +type ProxyStatusRequest struct { + Type string + ProxyID uint16 +} + +type ProxyStatusResponse struct { + Type string + ProxyID uint16 + IsActive bool +} + +type RemoveProxy struct { + Type string + ProxyID uint16 +} + +type ProxyInstanceResponse struct { + Type string + Proxies []uint16 +} + +type ProxyConnectionsRequest struct { + Type string + ProxyID uint16 +} + +type ProxyConnectionsResponse struct { + Type string + Connections []uint16 +} + +type TCPConnectionOpened struct { + Type string + ProxyID uint16 + ConnectionID uint16 +} + +type TCPConnectionClosed struct { + Type string + ProxyID uint16 + ConnectionID uint16 +} + +type TCPProxyData struct { + Type string + ProxyID uint16 + ConnectionID uint16 + DataLength uint16 +} + +type UDPProxyData struct { + Type string + ProxyID uint16 + ClientIP string + ClientPort uint16 + DataLength uint16 +} + +type ProxyInformationRequest struct { + Type string + ProxyID uint16 +} + +type ProxyInformationResponse struct { + Type string + Exists bool + SourceIP string + SourcePort uint16 + DestPort uint16 + Protocol string // Will be either 'tcp' or 'udp' +} + +type ProxyConnectionInformationRequest struct { + Type string + ProxyID uint16 + ConnectionID uint16 +} + +type ProxyConnectionInformationResponse struct { + Type string + Exists bool + ClientIP string + ClientPort uint16 +} + +const ( + ProxyStatusRequestID = iota + 100 + ProxyStatusResponseID + RemoveProxyID + ProxyInstanceResponseID + ProxyConnectionsRequestID + ProxyConnectionsResponseID + TCPConnectionOpenedID + TCPConnectionClosedID + TCPProxyDataID + UDPProxyDataID + ProxyInformationRequestID + ProxyInformationResponseID + ProxyConnectionInformationRequestID + ProxyConnectionInformationResponseID +) diff --git a/backend/sshappbackend/datacommands/marshal.go b/backend/sshappbackend/datacommands/marshal.go new file mode 100644 index 0000000..bacbda1 --- /dev/null +++ b/backend/sshappbackend/datacommands/marshal.go @@ -0,0 +1,323 @@ +package datacommands + +import ( + "encoding/binary" + "fmt" + "net" +) + +// Example size and protocol constants — adjust as needed. +const ( + IPv4Size = 4 + IPv6Size = 16 + + TCP = 1 + UDP = 2 +) + +// Marshal takes a command (pointer to one of our structs) and converts it to a byte slice. +func Marshal(_ string, command interface{}) ([]byte, error) { + switch cmd := command.(type) { + // ProxyStatusRequest: 1 byte for the command ID + 2 bytes for the ProxyID. + case *ProxyStatusRequest: + buf := make([]byte, 1+2) + + buf[0] = ProxyStatusRequestID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + + return buf, nil + + // ProxyStatusResponse: 1 byte for the command ID, 2 bytes for ProxyID, and 1 byte for IsActive. + case *ProxyStatusResponse: + buf := make([]byte, 1+2+1) + + buf[0] = ProxyStatusResponseID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + + if cmd.IsActive { + buf[3] = 1 + } else { + buf[3] = 0 + } + + return buf, nil + + // RemoveProxy: 1 byte for the command ID + 2 bytes for the ProxyID. + case *RemoveProxy: + buf := make([]byte, 1+2) + + buf[0] = RemoveProxyID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + + return buf, nil + + // ProxyConnectionsRequest: 1 byte for the command ID + 2 bytes for the ProxyID. + case *ProxyConnectionsRequest: + buf := make([]byte, 1+2) + + buf[0] = ProxyConnectionsRequestID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + + return buf, nil + + // ProxyConnectionsResponse: 1 byte for the command ID + 2 bytes length of the Connections + 2 bytes for each + // number in the Connection array. + case *ProxyConnectionsResponse: + buf := make([]byte, 1+((len(cmd.Connections)+1)*2)) + + buf[0] = ProxyConnectionsResponseID + binary.BigEndian.PutUint16(buf[1:], uint16(len(cmd.Connections))) + + for connectionIndex, connection := range cmd.Connections { + binary.BigEndian.PutUint16(buf[3+(connectionIndex*2):], connection) + } + + return buf, nil + + // ProxyConnectionsResponse: 1 byte for the command ID + 2 bytes length of the Proxies + 2 bytes for each + // number in the Proxies array. + case *ProxyInstanceResponse: + buf := make([]byte, 1+((len(cmd.Proxies)+1)*2)) + + buf[0] = ProxyInstanceResponseID + binary.BigEndian.PutUint16(buf[1:], uint16(len(cmd.Proxies))) + + for connectionIndex, connection := range cmd.Proxies { + binary.BigEndian.PutUint16(buf[3+(connectionIndex*2):], connection) + } + + return buf, nil + + // TCPConnectionOpened: 1 byte for the command ID + 2 bytes ProxyID + 2 bytes ConnectionID. + case *TCPConnectionOpened: + buf := make([]byte, 1+2+2) + + buf[0] = TCPConnectionOpenedID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) + + return buf, nil + + // TCPConnectionClosed: 1 byte for the command ID + 2 bytes ProxyID + 2 bytes ConnectionID. + case *TCPConnectionClosed: + buf := make([]byte, 1+2+2) + + buf[0] = TCPConnectionClosedID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) + + return buf, nil + + // TCPProxyData: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + 2 bytes DataLength. + case *TCPProxyData: + buf := make([]byte, 1+2+2+2) + + buf[0] = TCPProxyDataID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) + binary.BigEndian.PutUint16(buf[5:], cmd.DataLength) + + return buf, nil + + // UDPProxyData: + // Format: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + + // 1 byte IP version + IP bytes + 2 bytes ClientPort + 2 bytes DataLength. + case *UDPProxyData: + ip := net.ParseIP(cmd.ClientIP) + if ip == nil { + return nil, fmt.Errorf("invalid client IP: %v", cmd.ClientIP) + } + + var ipVer uint8 + var ipBytes []byte + + if ip4 := ip.To4(); ip4 != nil { + ipBytes = ip4 + ipVer = 4 + } else if ip16 := ip.To16(); ip16 != nil { + ipBytes = ip16 + ipVer = 6 + } else { + return nil, fmt.Errorf("unable to detect IP version for: %v", cmd.ClientIP) + } + + totalSize := 1 + // id + 2 + // ProxyID + 1 + // IP version + len(ipBytes) + // client IP bytes + 2 + // ClientPort + 2 // DataLength + + buf := make([]byte, totalSize) + offset := 0 + buf[offset] = UDPProxyDataID + offset++ + + binary.BigEndian.PutUint16(buf[offset:], cmd.ProxyID) + offset += 2 + + buf[offset] = ipVer + offset++ + + copy(buf[offset:], ipBytes) + offset += len(ipBytes) + + binary.BigEndian.PutUint16(buf[offset:], cmd.ClientPort) + offset += 2 + + binary.BigEndian.PutUint16(buf[offset:], cmd.DataLength) + + return buf, nil + + // ProxyInformationRequest: 1 byte ID + 2 bytes ProxyID. + case *ProxyInformationRequest: + buf := make([]byte, 1+2) + buf[0] = ProxyInformationRequestID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + return buf, nil + + // ProxyInformationResponse: + // Format: 1 byte ID + 1 byte Exists + (if exists:) + // 1 byte IP version + IP bytes + 2 bytes SourcePort + 2 bytes DestPort + 1 byte Protocol. + // (For simplicity, this marshaller always writes the IP and port info even if !Exists.) + case *ProxyInformationResponse: + if !cmd.Exists { + buf := make([]byte, 1+1) + buf[0] = ProxyInformationResponseID + buf[1] = 0 /* false */ + + return buf, nil + } + + ip := net.ParseIP(cmd.SourceIP) + + if ip == nil { + return nil, fmt.Errorf("invalid source IP: %v", cmd.SourceIP) + } + + var ipVer uint8 + var ipBytes []byte + + if ip4 := ip.To4(); ip4 != nil { + ipBytes = ip4 + ipVer = 4 + } else if ip16 := ip.To16(); ip16 != nil { + ipBytes = ip16 + ipVer = 6 + } else { + return nil, fmt.Errorf("unable to detect IP version for: %v", cmd.SourceIP) + } + + totalSize := 1 + // id + 1 + // Exists flag + 1 + // IP version + len(ipBytes) + + 2 + // SourcePort + 2 + // DestPort + 1 // Protocol + + buf := make([]byte, totalSize) + + offset := 0 + buf[offset] = ProxyInformationResponseID + offset++ + + // We already handle this above + buf[offset] = 1 /* true */ + offset++ + + buf[offset] = ipVer + offset++ + + copy(buf[offset:], ipBytes) + offset += len(ipBytes) + + binary.BigEndian.PutUint16(buf[offset:], cmd.SourcePort) + offset += 2 + + binary.BigEndian.PutUint16(buf[offset:], cmd.DestPort) + offset += 2 + + // Encode protocol as 1 byte. + switch cmd.Protocol { + case "tcp": + buf[offset] = TCP + case "udp": + buf[offset] = UDP + default: + return nil, fmt.Errorf("invalid protocol: %v", cmd.Protocol) + } + + // offset++ (not needed since we are at the end) + return buf, nil + + // ProxyConnectionInformationRequest: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. + case *ProxyConnectionInformationRequest: + buf := make([]byte, 1+2+2) + + buf[0] = ProxyConnectionInformationRequestID + binary.BigEndian.PutUint16(buf[1:], cmd.ProxyID) + binary.BigEndian.PutUint16(buf[3:], cmd.ConnectionID) + + return buf, nil + + // ProxyConnectionInformationResponse: + // Format: 1 byte ID + 1 byte Exists + (if exists:) + // 1 byte IP version + IP bytes + 2 bytes ClientPort. + // This marshaller only writes the rest of the data if Exists. + case *ProxyConnectionInformationResponse: + if !cmd.Exists { + buf := make([]byte, 1+1) + buf[0] = ProxyConnectionInformationResponseID + buf[1] = 0 /* false */ + + return buf, nil + } + + ip := net.ParseIP(cmd.ClientIP) + + if ip == nil { + return nil, fmt.Errorf("invalid client IP: %v", cmd.ClientIP) + } + + var ipVer uint8 + var ipBytes []byte + if ip4 := ip.To4(); ip4 != nil { + ipBytes = ip4 + ipVer = 4 + } else if ip16 := ip.To16(); ip16 != nil { + ipBytes = ip16 + ipVer = 6 + } else { + return nil, fmt.Errorf("unable to detect IP version for: %v", cmd.ClientIP) + } + + totalSize := 1 + // id + 1 + // Exists flag + 1 + // IP version + len(ipBytes) + + 2 // ClientPort + + buf := make([]byte, totalSize) + offset := 0 + buf[offset] = ProxyConnectionInformationResponseID + offset++ + + // We already handle this above + buf[offset] = 1 /* true */ + offset++ + + buf[offset] = ipVer + offset++ + + copy(buf[offset:], ipBytes) + offset += len(ipBytes) + + binary.BigEndian.PutUint16(buf[offset:], cmd.ClientPort) + + return buf, nil + + default: + return nil, fmt.Errorf("unsupported command type") + } +} diff --git a/backend/sshappbackend/datacommands/marshalling_test.go b/backend/sshappbackend/datacommands/marshalling_test.go new file mode 100644 index 0000000..81e7101 --- /dev/null +++ b/backend/sshappbackend/datacommands/marshalling_test.go @@ -0,0 +1,828 @@ +package datacommands + +import ( + "bytes" + "log" + "os" + "testing" +) + +var logLevel = os.Getenv("HERMES_LOG_LEVEL") + +func TestProxyStatusRequest(t *testing.T) { + commandInput := &ProxyStatusRequest{ + Type: "proxyStatusRequest", + ProxyID: 19132, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } +} + +func TestProxyStatusResponse(t *testing.T) { + commandInput := &ProxyStatusResponse{ + Type: "proxyStatusResponse", + ProxyID: 19132, + IsActive: true, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } + + if commandInput.IsActive != commandUnmarshalled.IsActive { + t.Fail() + log.Printf("IsActive's are not equal (orig: '%t', unmsh: '%t')", commandInput.IsActive, commandUnmarshalled.IsActive) + } +} + +func TestRemoveProxy(t *testing.T) { + commandInput := &RemoveProxy{ + Type: "removeProxy", + ProxyID: 19132, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } +} + +func TestProxyConnectionsRequest(t *testing.T) { + commandInput := &ProxyConnectionsRequest{ + Type: "proxyConnectionsRequest", + ProxyID: 19132, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } +} + +func TestProxyConnectionsResponse(t *testing.T) { + commandInput := &ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", + Connections: []uint16{12831, 9455, 64219, 12, 32}, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + for connectionIndex, originalConnection := range commandInput.Connections { + remoteConnection := commandUnmarshalled.Connections[connectionIndex] + + if originalConnection != remoteConnection { + t.Fail() + log.Printf("(in #%d) SourceIP's are not equal (orig: %d, unmsh: %d)", connectionIndex, originalConnection, connectionIndex) + } + } +} + +func TestProxyInstanceResponse(t *testing.T) { + commandInput := &ProxyInstanceResponse{ + Type: "proxyInstanceResponse", + Proxies: []uint16{12831, 9455, 64219, 12, 32}, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + for proxyIndex, originalProxy := range commandInput.Proxies { + remoteProxy := commandUnmarshalled.Proxies[proxyIndex] + + if originalProxy != remoteProxy { + t.Fail() + log.Printf("(in #%d) Proxy IDs are not equal (orig: %d, unmsh: %d)", proxyIndex, originalProxy, remoteProxy) + } + } +} + +func TestTCPConnectionOpened(t *testing.T) { + commandInput := &TCPConnectionOpened{ + Type: "tcpConnectionOpened", + ProxyID: 19132, + ConnectionID: 25565, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPConnectionOpened) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } + + if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { + t.Fail() + log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) + } +} + +func TestTCPConnectionClosed(t *testing.T) { + commandInput := &TCPConnectionClosed{ + Type: "tcpConnectionClosed", + ProxyID: 19132, + ConnectionID: 25565, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPConnectionClosed) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } + + if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { + t.Fail() + log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) + } +} + +func TestTCPProxyData(t *testing.T) { + commandInput := &TCPProxyData{ + Type: "tcpProxyData", + ProxyID: 19132, + ConnectionID: 25565, + DataLength: 1234, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPProxyData) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } + + if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { + t.Fail() + log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) + } + + if commandInput.DataLength != commandUnmarshalled.DataLength { + t.Fail() + log.Printf("DataLength's are not equal (orig: '%d', unmsh: '%d')", commandInput.DataLength, commandUnmarshalled.DataLength) + } +} + +func TestUDPProxyData(t *testing.T) { + commandInput := &UDPProxyData{ + Type: "udpProxyData", + ProxyID: 19132, + ClientIP: "68.51.23.54", + ClientPort: 28173, + DataLength: 1234, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*UDPProxyData) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } + + if commandInput.ClientIP != commandUnmarshalled.ClientIP { + t.Fail() + log.Printf("ClientIP's are not equal (orig: '%s', unmsh: '%s')", commandInput.ClientIP, commandUnmarshalled.ClientIP) + } + + if commandInput.ClientPort != commandUnmarshalled.ClientPort { + t.Fail() + log.Printf("ClientPort's are not equal (orig: '%d', unmsh: '%d')", commandInput.ClientPort, commandUnmarshalled.ClientPort) + } + + if commandInput.DataLength != commandUnmarshalled.DataLength { + t.Fail() + log.Printf("DataLength's are not equal (orig: '%d', unmsh: '%d')", commandInput.DataLength, commandUnmarshalled.DataLength) + } +} + +func TestProxyInformationRequest(t *testing.T) { + commandInput := &ProxyInformationRequest{ + Type: "proxyInformationRequest", + ProxyID: 19132, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } +} + +func TestProxyInformationResponseExists(t *testing.T) { + commandInput := &ProxyInformationResponse{ + Type: "proxyInformationResponse", + Exists: true, + SourceIP: "192.168.0.139", + SourcePort: 19132, + DestPort: 19132, + Protocol: "tcp", + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.Exists != commandUnmarshalled.Exists { + t.Fail() + log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) + } + + if commandInput.SourceIP != commandUnmarshalled.SourceIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) + } + + if commandInput.SourcePort != commandUnmarshalled.SourcePort { + t.Fail() + log.Printf("SourcePort's are not equal (orig: %d, unmsh: %d)", commandInput.SourcePort, commandUnmarshalled.SourcePort) + } + + if commandInput.DestPort != commandUnmarshalled.DestPort { + t.Fail() + log.Printf("DestPort's are not equal (orig: %d, unmsh: %d)", commandInput.DestPort, commandUnmarshalled.DestPort) + } + + if commandInput.Protocol != commandUnmarshalled.Protocol { + t.Fail() + log.Printf("Protocols are not equal (orig: %s, unmsh: %s)", commandInput.Protocol, commandUnmarshalled.Protocol) + } +} + +func TestProxyInformationResponseNoExist(t *testing.T) { + commandInput := &ProxyInformationResponse{ + Type: "proxyInformationResponse", + Exists: false, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.Exists != commandUnmarshalled.Exists { + t.Fail() + log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) + } +} + +func TestProxyConnectionInformationRequest(t *testing.T) { + commandInput := &ProxyConnectionInformationRequest{ + Type: "proxyConnectionInformationRequest", + ProxyID: 19132, + ConnectionID: 25565, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + if err != nil { + t.Fatal(err.Error()) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationRequest) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.ProxyID != commandUnmarshalled.ProxyID { + t.Fail() + log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) + } + + if commandInput.ConnectionID != commandUnmarshalled.ConnectionID { + t.Fail() + log.Printf("ConnectionID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ConnectionID, commandUnmarshalled.ConnectionID) + } +} + +func TestProxyConnectionInformationResponseExists(t *testing.T) { + commandInput := &ProxyConnectionInformationResponse{ + Type: "proxyConnectionInformationResponse", + Exists: true, + ClientIP: "192.168.0.139", + ClientPort: 19132, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.Exists != commandUnmarshalled.Exists { + t.Fail() + log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) + } + + if commandInput.ClientIP != commandUnmarshalled.ClientIP { + t.Fail() + log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.ClientIP, commandUnmarshalled.ClientIP) + } + + if commandInput.ClientPort != commandUnmarshalled.ClientPort { + t.Fail() + log.Printf("ClientPort's are not equal (orig: %d, unmsh: %d)", commandInput.ClientPort, commandUnmarshalled.ClientPort) + } +} + +func TestProxyConnectionInformationResponseNoExists(t *testing.T) { + commandInput := &ProxyConnectionInformationResponse{ + Type: "proxyConnectionInformationResponse", + Exists: false, + } + + commandMarshalled, err := Marshal(commandInput.Type, commandInput) + + if err != nil { + t.Fatal(err.Error()) + } + + if logLevel == "debug" { + log.Printf("Generated array contents: %v", commandMarshalled) + } + + buf := bytes.NewBuffer(commandMarshalled) + commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + + if err != nil { + t.Fatal(err.Error()) + } + + if commandType != commandInput.Type { + t.Fail() + log.Print("command type does not match up!") + } + + commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationResponse) + + if !ok { + t.Fatal("failed typecast") + } + + if commandInput.Type != commandUnmarshalled.Type { + t.Fail() + log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) + } + + if commandInput.Exists != commandUnmarshalled.Exists { + t.Fail() + log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) + } +} diff --git a/backend/sshappbackend/datacommands/unmarshal.go b/backend/sshappbackend/datacommands/unmarshal.go new file mode 100644 index 0000000..7e97c3f --- /dev/null +++ b/backend/sshappbackend/datacommands/unmarshal.go @@ -0,0 +1,435 @@ +package datacommands + +import ( + "encoding/binary" + "fmt" + "io" + "net" +) + +// Unmarshal reads from the provided connection and returns +// the message type (as a string), the unmarshalled struct, or an error. +func Unmarshal(conn io.Reader) (string, interface{}, error) { + // Every command starts with a 1-byte command ID. + header := make([]byte, 1) + if _, err := io.ReadFull(conn, header); err != nil { + return "", nil, fmt.Errorf("couldn't read command ID: %w", err) + } + + cmdID := header[0] + switch cmdID { + // ProxyStatusRequest: 1 byte ID + 2 bytes ProxyID. + case ProxyStatusRequestID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyStatusRequest ProxyID: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf) + + return "proxyStatusRequest", &ProxyStatusRequest{ + Type: "proxyStatusRequest", + ProxyID: proxyID, + }, nil + + // ProxyStatusResponse: 1 byte ID + 2 bytes ProxyID + 1 byte IsActive. + case ProxyStatusResponseID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyStatusResponse ProxyID: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf) + boolBuf := make([]byte, 1) + + if _, err := io.ReadFull(conn, boolBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyStatusResponse IsActive: %w", err) + } + + isActive := boolBuf[0] != 0 + + return "proxyStatusResponse", &ProxyStatusResponse{ + Type: "proxyStatusResponse", + ProxyID: proxyID, + IsActive: isActive, + }, nil + + // RemoveProxy: 1 byte ID + 2 bytes ProxyID. + case RemoveProxyID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read RemoveProxy ProxyID: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf) + + return "removeProxy", &RemoveProxy{ + Type: "removeProxy", + ProxyID: proxyID, + }, nil + + // ProxyConnectionsRequest: 1 byte ID + 2 bytes ProxyID. + case ProxyConnectionsRequestID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionsRequest ProxyID: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf) + + return "proxyConnectionsRequest", &ProxyConnectionsRequest{ + Type: "proxyConnectionsRequest", + ProxyID: proxyID, + }, nil + + // ProxyConnectionsResponse: 1 byte ID + 2 bytes Connections length + 2 bytes for each Connection in Connections. + case ProxyConnectionsResponseID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) + } + + length := binary.BigEndian.Uint16(buf) + connections := make([]uint16, length) + + var failedDuringReading error + + for connectionIndex := range connections { + if _, err := io.ReadFull(conn, buf); err != nil { + failedDuringReading = fmt.Errorf("couldn't read ProxyConnectionsResponse with position of %d: %w", connectionIndex, err) + break + } + + connections[connectionIndex] = binary.BigEndian.Uint16(buf) + } + + return "proxyConnectionsResponse", &ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", + Connections: connections, + }, failedDuringReading + + // ProxyInstanceResponse: 1 byte ID + 2 bytes Proxies length + 2 bytes for each Proxy in Proxies. + case ProxyInstanceResponseID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) + } + + length := binary.BigEndian.Uint16(buf) + proxies := make([]uint16, length) + + var failedDuringReading error + + for connectionIndex := range proxies { + if _, err := io.ReadFull(conn, buf); err != nil { + failedDuringReading = fmt.Errorf("couldn't read ProxyConnectionsResponse with position of %d: %w", connectionIndex, err) + break + } + + proxies[connectionIndex] = binary.BigEndian.Uint16(buf) + } + + return "proxyInstanceResponse", &ProxyInstanceResponse{ + Type: "proxyInstanceResponse", + Proxies: proxies, + }, failedDuringReading + + // TCPConnectionOpened: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. + case TCPConnectionOpenedID: + buf := make([]byte, 2+2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read TCPConnectionOpened fields: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf[0:2]) + connectionID := binary.BigEndian.Uint16(buf[2:4]) + + return "tcpConnectionOpened", &TCPConnectionOpened{ + Type: "tcpConnectionOpened", + ProxyID: proxyID, + ConnectionID: connectionID, + }, nil + + // TCPConnectionClosed: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. + case TCPConnectionClosedID: + buf := make([]byte, 2+2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read TCPConnectionClosed fields: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf[0:2]) + connectionID := binary.BigEndian.Uint16(buf[2:4]) + + return "tcpConnectionClosed", &TCPConnectionClosed{ + Type: "tcpConnectionClosed", + ProxyID: proxyID, + ConnectionID: connectionID, + }, nil + + // TCPProxyData: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + 2 bytes DataLength. + case TCPProxyDataID: + buf := make([]byte, 2+2+2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read TCPProxyData fields: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf[0:2]) + connectionID := binary.BigEndian.Uint16(buf[2:4]) + dataLength := binary.BigEndian.Uint16(buf[4:6]) + + return "tcpProxyData", &TCPProxyData{ + Type: "tcpProxyData", + ProxyID: proxyID, + ConnectionID: connectionID, + DataLength: dataLength, + }, nil + + // UDPProxyData: + // Format: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID + + // 1 byte IP version + IP bytes + 2 bytes ClientPort + 2 bytes DataLength. + case UDPProxyDataID: + // Read 2 bytes ProxyID + 2 bytes ConnectionID. + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read UDPProxyData ProxyID/ConnectionID: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf) + + // Read IP version. + ipVerBuf := make([]byte, 1) + + if _, err := io.ReadFull(conn, ipVerBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read UDPProxyData IP version: %w", err) + } + + var ipSize int + + if ipVerBuf[0] == 4 { + ipSize = IPv4Size + } else if ipVerBuf[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version received: %v", ipVerBuf[0]) + } + + // Read the IP bytes. + ipBytes := make([]byte, ipSize) + if _, err := io.ReadFull(conn, ipBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read UDPProxyData IP bytes: %w", err) + } + clientIP := net.IP(ipBytes).String() + + // Read ClientPort. + portBuf := make([]byte, 2) + + if _, err := io.ReadFull(conn, portBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read UDPProxyData ClientPort: %w", err) + } + + clientPort := binary.BigEndian.Uint16(portBuf) + + // Read DataLength. + dataLengthBuf := make([]byte, 2) + + if _, err := io.ReadFull(conn, dataLengthBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read UDPProxyData DataLength: %w", err) + } + + dataLength := binary.BigEndian.Uint16(dataLengthBuf) + + return "udpProxyData", &UDPProxyData{ + Type: "udpProxyData", + ProxyID: proxyID, + ClientIP: clientIP, + ClientPort: clientPort, + DataLength: dataLength, + }, nil + + // ProxyInformationRequest: 1 byte ID + 2 bytes ProxyID. + case ProxyInformationRequestID: + buf := make([]byte, 2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyInformationRequest ProxyID: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf) + + return "proxyInformationRequest", &ProxyInformationRequest{ + Type: "proxyInformationRequest", + ProxyID: proxyID, + }, nil + + // ProxyInformationResponse: + // Format: 1 byte ID + 1 byte Exists + + // 1 byte IP version + IP bytes + 2 bytes SourcePort + 2 bytes DestPort + 1 byte Protocol. + case ProxyInformationResponseID: + // Read Exists flag. + boolBuf := make([]byte, 1) + + if _, err := io.ReadFull(conn, boolBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse Exists flag: %w", err) + } + + exists := boolBuf[0] != 0 + + if !exists { + return "proxyInformationResponse", &ProxyInformationResponse{ + Type: "proxyInformationResponse", + Exists: exists, + }, nil + } + + // Read IP version. + ipVerBuf := make([]byte, 1) + + if _, err := io.ReadFull(conn, ipVerBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse IP version: %w", err) + } + + var ipSize int + + if ipVerBuf[0] == 4 { + ipSize = IPv4Size + } else if ipVerBuf[0] == 6 { + ipSize = IPv6Size + } else { + return "", nil, fmt.Errorf("invalid IP version in ProxyInformationResponse: %v", ipVerBuf[0]) + } + + // Read the source IP bytes. + ipBytes := make([]byte, ipSize) + + if _, err := io.ReadFull(conn, ipBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse IP bytes: %w", err) + } + + sourceIP := net.IP(ipBytes).String() + + // Read SourcePort and DestPort. + portsBuf := make([]byte, 2+2) + + if _, err := io.ReadFull(conn, portsBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse ports: %w", err) + } + + sourcePort := binary.BigEndian.Uint16(portsBuf[0:2]) + destPort := binary.BigEndian.Uint16(portsBuf[2:4]) + + // Read protocol. + protoBuf := make([]byte, 1) + + if _, err := io.ReadFull(conn, protoBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse protocol: %w", err) + } + var protocol string + if protoBuf[0] == TCP { + protocol = "tcp" + } else if protoBuf[0] == UDP { + protocol = "udp" + } else { + return "", nil, fmt.Errorf("invalid protocol value in ProxyInformationResponse: %d", protoBuf[0]) + } + + return "proxyInformationResponse", &ProxyInformationResponse{ + Type: "proxyInformationResponse", + Exists: exists, + SourceIP: sourceIP, + SourcePort: sourcePort, + DestPort: destPort, + Protocol: protocol, + }, nil + + // ProxyConnectionInformationRequest: 1 byte ID + 2 bytes ProxyID + 2 bytes ConnectionID. + case ProxyConnectionInformationRequestID: + buf := make([]byte, 2+2) + + if _, err := io.ReadFull(conn, buf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationRequest fields: %w", err) + } + + proxyID := binary.BigEndian.Uint16(buf[0:2]) + connectionID := binary.BigEndian.Uint16(buf[2:4]) + + return "proxyConnectionInformationRequest", &ProxyConnectionInformationRequest{ + Type: "proxyConnectionInformationRequest", + ProxyID: proxyID, + ConnectionID: connectionID, + }, nil + + // ProxyConnectionInformationResponse: + // Format: 1 byte ID + 1 byte Exists + 1 byte IP version + IP bytes + 2 bytes ClientPort. + case ProxyConnectionInformationResponseID: + // Read Exists flag. + boolBuf := make([]byte, 1) + if _, err := io.ReadFull(conn, boolBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse Exists flag: %w", err) + } + + exists := boolBuf[0] != 0 + + if !exists { + return "proxyConnectionInformationResponse", &ProxyConnectionInformationResponse{ + Type: "proxyConnectionInformationResponse", + Exists: exists, + }, nil + } + + // Read IP version. + ipVerBuf := make([]byte, 1) + + if _, err := io.ReadFull(conn, ipVerBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP version: %w", err) + } + + if ipVerBuf[0] != 4 && ipVerBuf[0] != 6 { + return "", nil, fmt.Errorf("invalid IP version in ProxyConnectionInformationResponse: %v", ipVerBuf[0]) + } + + var ipSize int + + if ipVerBuf[0] == 4 { + ipSize = IPv4Size + } else { + ipSize = IPv6Size + } + + // Read IP bytes. + ipBytes := make([]byte, ipSize) + + if _, err := io.ReadFull(conn, ipBytes); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP bytes: %w", err) + } + + clientIP := net.IP(ipBytes).String() + + // Read ClientPort. + portBuf := make([]byte, 2) + + if _, err := io.ReadFull(conn, portBuf); err != nil { + return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse ClientPort: %w", err) + } + + clientPort := binary.BigEndian.Uint16(portBuf) + + return "proxyConnectionInformationResponse", &ProxyConnectionInformationResponse{ + Type: "proxyConnectionInformationResponse", + Exists: exists, + ClientIP: clientIP, + ClientPort: clientPort, + }, nil + default: + return "", nil, fmt.Errorf("unknown command id: %v", cmdID) + } +} diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go new file mode 100644 index 0000000..6d46a7b --- /dev/null +++ b/backend/sshappbackend/remote-code/backendutil_custom/application.go @@ -0,0 +1,310 @@ +package backendutil_custom + +import ( + "fmt" + "net" + "os" + + "git.terah.dev/imterah/hermes/backend/backendutil" + "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" + "github.com/charmbracelet/log" +) + +type BackendApplicationHelper struct { + Backend BackendInterface + SocketPath string + + socket net.Conn +} + +func (helper *BackendApplicationHelper) Start() error { + log.Debug("BackendApplicationHelper is starting") + err := backendutil.ConfigureProfiling() + + if err != nil { + return err + } + + log.Debug("Currently waiting for Unix socket connection...") + + helper.socket, err = net.Dial("unix", helper.SocketPath) + + if err != nil { + return err + } + + log.Debug("Sucessfully connected") + + for { + commandType, commandRaw, err := datacommands.Unmarshal(helper.socket) + + if err != nil && err.Error() != "couldn't match command ID" { + return err + } + + switch commandType { + case "proxyConnectionsRequest": + proxyConnectionRequest, ok := commandRaw.(*datacommands.ProxyConnectionsRequest) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + connections := helper.Backend.GetAllClientConnections(proxyConnectionRequest.ProxyID) + + serverParams := &datacommands.ProxyConnectionsResponse{ + Type: "proxyConnectionsResponse", + Connections: connections, + } + + byteData, err := datacommands.Marshal(serverParams.Type, serverParams) + + if err != nil { + return err + } + + if _, err = helper.socket.Write(byteData); err != nil { + return err + } + case "removeProxy": + command, ok := commandRaw.(*datacommands.RemoveProxy) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StopProxy(command) + var hasAnyFailed bool + + if !ok { + log.Warnf("failed to remove proxy (ID %d): RemoveProxy returned into failure state", command.ProxyID) + hasAnyFailed = true + } else if err != nil { + log.Warnf("failed to remove proxy (ID %d): %s", command.ProxyID, err.Error()) + hasAnyFailed = true + } + + response := &datacommands.ProxyStatusResponse{ + Type: "proxyStatusResponse", + ProxyID: command.ProxyID, + IsActive: hasAnyFailed, + } + + responseMarshalled, err := commonbackend.Marshal(response.Type, response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + default: + commandType, commandRaw, err := commonbackend.Unmarshal(helper.socket) + + if err != nil { + return err + } + + switch commandType { + case "start": + command, ok := commandRaw.(*commonbackend.Start) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StartBackend(command.Arguments) + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + Type: "backendStatusResponse", + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response.Type, response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case "stop": + _, ok := commandRaw.(*commonbackend.Stop) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err = helper.Backend.StopBackend() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + Type: "backendStatusResponse", + IsRunning: !ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response.Type, response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case "backendStatusRequest": + _, ok := commandRaw.(*commonbackend.BackendStatusRequest) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + ok, err := helper.Backend.GetBackendStatus() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + Type: "backendStatusResponse", + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response.Type, response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case "addProxy": + command, ok := commandRaw.(*commonbackend.AddProxy) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + id, ok, err := helper.Backend.StartProxy(command) + var hasAnyFailed bool + + if !ok { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true + } else if err != nil { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) + hasAnyFailed = true + } + + response := &datacommands.ProxyStatusResponse{ + Type: "proxyStatusResponse", + ProxyID: id, + IsActive: !hasAnyFailed, + } + + responseMarshalled, err := commonbackend.Marshal(response.Type, response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case "checkClientParameters": + command, ok := commandRaw.(*commonbackend.CheckClientParameters) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + resp := helper.Backend.CheckParametersForConnections(command) + resp.Type = "checkParametersResponse" + resp.InResponseTo = "checkClientParameters" + + byteData, err := commonbackend.Marshal(resp.Type, resp) + + if err != nil { + return err + } + + if _, err = helper.socket.Write(byteData); err != nil { + return err + } + case "checkServerParameters": + command, ok := commandRaw.(*commonbackend.CheckServerParameters) + + if !ok { + return fmt.Errorf("failed to typecast") + } + + resp := helper.Backend.CheckParametersForBackend(command.Arguments) + resp.Type = "checkParametersResponse" + resp.InResponseTo = "checkServerParameters" + + byteData, err := commonbackend.Marshal(resp.Type, resp) + + if err != nil { + return err + } + + if _, err = helper.socket.Write(byteData); err != nil { + return err + } + default: + log.Warn("Unsupported command recieved: %s", commandType) + } + } + } +} + +func NewHelper(backend BackendInterface) *BackendApplicationHelper { + socketPath, ok := os.LookupEnv("HERMES_API_SOCK") + + if !ok { + log.Warn("HERMES_API_SOCK is not defined! This will cause an issue unless the backend manually overwrites it") + } + + helper := &BackendApplicationHelper{ + Backend: backend, + SocketPath: socketPath, + } + + return helper +} diff --git a/backend/sshappbackend/remote-code/backendutil_custom/structure.go b/backend/sshappbackend/remote-code/backendutil_custom/structure.go new file mode 100644 index 0000000..96bcbf8 --- /dev/null +++ b/backend/sshappbackend/remote-code/backendutil_custom/structure.go @@ -0,0 +1,22 @@ +package backendutil_custom + +import ( + "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" +) + +type BackendInterface interface { + StartBackend(arguments []byte) (bool, error) + StopBackend() (bool, error) + GetBackendStatus() (bool, error) + StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) + StopProxy(command *datacommands.RemoveProxy) (bool, error) + GetAllProxies() []uint16 + ResolveProxy(proxyID uint16) *datacommands.ProxyInformationResponse + GetAllClientConnections(proxyID uint16) []uint16 + ResolveConnection(connectionID uint16) *datacommands.ProxyConnectionsResponse + CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse + CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse + HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) + HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) +} diff --git a/backend/sshappbackend/remote-code/main.go b/backend/sshappbackend/remote-code/main.go index 8a3f1cc..0fd11ee 100644 --- a/backend/sshappbackend/remote-code/main.go +++ b/backend/sshappbackend/remote-code/main.go @@ -1,7 +1,120 @@ package main -import "fmt" +import ( + "os" + "sync" + + "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" + "git.terah.dev/imterah/hermes/backend/sshappbackend/remote-code/backendutil_custom" + "github.com/charmbracelet/log" +) + +type TCPProxy struct { + proxyIDIndex uint16 + proxyIDLock sync.Mutex +} + +type UDPProxy struct { +} + +type SSHRemoteAppBackend struct { + connectionIDIndex uint16 + connectionIDLock sync.Mutex + + tcpProxies map[uint16]*TCPProxy + udpProxies map[uint16]*UDPProxy +} + +func (backend *SSHRemoteAppBackend) StartBackend(byte []byte) (bool, error) { + backend.tcpProxies = map[uint16]*TCPProxy{} + backend.udpProxies = map[uint16]*UDPProxy{} + + return true, nil +} + +func (backend *SSHRemoteAppBackend) StopBackend() (bool, error) { + return true, nil +} + +func (backend *SSHRemoteAppBackend) GetBackendStatus() (bool, error) { + return true, nil +} + +func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) { + return 0, true, nil +} + +func (backend *SSHRemoteAppBackend) StopProxy(command *datacommands.RemoveProxy) (bool, error) { + return true, nil +} + +func (backend *SSHRemoteAppBackend) GetAllProxies() []uint16 { + return []uint16{} +} + +func (backend *SSHRemoteAppBackend) ResolveProxy(proxyID uint16) *datacommands.ProxyInformationResponse { + return &datacommands.ProxyInformationResponse{} +} + +func (backend *SSHRemoteAppBackend) GetAllClientConnections(proxyID uint16) []uint16 { + return []uint16{} +} + +func (backend *SSHRemoteAppBackend) ResolveConnection(proxyID uint16) *datacommands.ProxyConnectionsResponse { + return &datacommands.ProxyConnectionsResponse{} +} + +func (backend *SSHRemoteAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { + return &commonbackend.CheckParametersResponse{ + IsValid: true, + Message: "Valid!", + } +} + +func (backend *SSHRemoteAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { + return &commonbackend.CheckParametersResponse{ + IsValid: true, + Message: "Valid!", + } +} + +func (backend *SSHRemoteAppBackend) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) { + +} + +func (backend *SSHRemoteAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) { + +} func main() { - fmt.Println("remottuh code") + logLevel := os.Getenv("HERMES_LOG_LEVEL") + + if logLevel != "" { + switch logLevel { + case "debug": + log.SetLevel(log.DebugLevel) + + case "info": + log.SetLevel(log.InfoLevel) + + case "warn": + log.SetLevel(log.WarnLevel) + + case "error": + log.SetLevel(log.ErrorLevel) + + case "fatal": + log.SetLevel(log.FatalLevel) + } + } + + backend := &SSHRemoteAppBackend{} + + application := backendutil_custom.NewHelper(backend) + err := application.Start() + + if err != nil { + log.Fatalf("failed execution in application: %s", err.Error()) + } } diff --git a/go.mod b/go.mod index 98f0e12..b390542 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,10 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.23.0 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/pkg/sftp v1.13.7 github.com/urfave/cli/v2 v2.27.5 golang.org/x/crypto v0.31.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/term v0.28.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.11 @@ -50,7 +52,6 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pkg/sftp v1.13.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -58,7 +59,6 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/arch v0.12.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect From 62cc8b39ad67b507383e553af51a17d69a91a4aa Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 16 Feb 2025 18:11:01 -0500 Subject: [PATCH 29/45] chore: Cleanup code by switching to type switching instead of string switching. --- backend/backendutil/application.go | 79 ++++-------------- backend/build.sh | 8 +- .../backendutil_custom/application.go | 83 ++++--------------- 3 files changed, 37 insertions(+), 133 deletions(-) diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index c6797ce..f9fead0 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -1,7 +1,6 @@ package backendutil import ( - "fmt" "net" "os" @@ -35,21 +34,15 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") for { - commandType, commandRaw, err := commonbackend.Unmarshal(helper.socket) + _, commandRaw, err := commonbackend.Unmarshal(helper.socket) if err != nil { return err } - switch commandType { - case "start": - command, ok := commandRaw.(*commonbackend.Start) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StartBackend(command.Arguments) + switch command := commandRaw.(type) { + case *commonbackend.Start: + ok, err := helper.Backend.StartBackend(command.Arguments) var ( message string @@ -78,13 +71,7 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "backendStatusRequest": - _, ok := commandRaw.(*commonbackend.BackendStatusRequest) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.BackendStatusRequest: ok, err := helper.Backend.GetBackendStatus() var ( @@ -114,14 +101,8 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "stop": - _, ok := commandRaw.(*commonbackend.Stop) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StopBackend() + case *commonbackend.Stop: + ok, err := helper.Backend.StopBackend() var ( message string @@ -150,14 +131,8 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "addProxy": - command, ok := commandRaw.(*commonbackend.AddProxy) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StartProxy(command) + case *commonbackend.AddProxy: + ok, err := helper.Backend.StartProxy(command) var hasAnyFailed bool if !ok { @@ -185,14 +160,8 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "removeProxy": - command, ok := commandRaw.(*commonbackend.RemoveProxy) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StopProxy(command) + case *commonbackend.RemoveProxy: + ok, err := helper.Backend.StopProxy(command) var hasAnyFailed bool if !ok { @@ -220,13 +189,7 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "proxyConnectionsRequest": - _, ok := commandRaw.(*commonbackend.ProxyConnectionsRequest) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.ProxyConnectionsRequest: connections := helper.Backend.GetAllClientConnections() serverParams := &commonbackend.ProxyConnectionsResponse{ @@ -243,13 +206,7 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - case "checkClientParameters": - command, ok := commandRaw.(*commonbackend.CheckClientParameters) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.CheckClientParameters: resp := helper.Backend.CheckParametersForConnections(command) resp.Type = "checkParametersResponse" resp.InResponseTo = "checkClientParameters" @@ -263,13 +220,7 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - case "checkServerParameters": - command, ok := commandRaw.(*commonbackend.CheckServerParameters) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.CheckServerParameters: resp := helper.Backend.CheckParametersForBackend(command.Arguments) resp.Type = "checkParametersResponse" resp.InResponseTo = "checkServerParameters" @@ -283,6 +234,8 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } + default: + log.Warnf("Unsupported command recieved: %T", command) } } } diff --git a/backend/build.sh b/backend/build.sh index 413455a..cee4440 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -14,12 +14,12 @@ echo "building externalbackendlauncher" go build -ldflags="-s -w" -trimpath . popd > /dev/null -pushd sshappbackend/remote-code > /dev/null -echo "building sshappbackend/remote-code" -if [ ! -d bin ]; then - mkdir bin +if [ ! -d "sshappbackend/local-code/remote-bin" ]; then + mkdir "sshappbackend/local-code/remote-bin" fi +pushd sshappbackend/remote-code > /dev/null +echo "building sshappbackend/remote-code" # Disable dynamic linking by disabling CGo. # We need to make the remote code as generic as possible, so we do this echo " - building for arm64" diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go index 6d46a7b..e058648 100644 --- a/backend/sshappbackend/remote-code/backendutil_custom/application.go +++ b/backend/sshappbackend/remote-code/backendutil_custom/application.go @@ -1,7 +1,6 @@ package backendutil_custom import ( - "fmt" "net" "os" @@ -37,21 +36,15 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") for { - commandType, commandRaw, err := datacommands.Unmarshal(helper.socket) + _, commandRaw, err := datacommands.Unmarshal(helper.socket) if err != nil && err.Error() != "couldn't match command ID" { return err } - switch commandType { - case "proxyConnectionsRequest": - proxyConnectionRequest, ok := commandRaw.(*datacommands.ProxyConnectionsRequest) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - connections := helper.Backend.GetAllClientConnections(proxyConnectionRequest.ProxyID) + switch command := commandRaw.(type) { + case *datacommands.ProxyConnectionsRequest: + connections := helper.Backend.GetAllClientConnections(command.ProxyID) serverParams := &datacommands.ProxyConnectionsResponse{ Type: "proxyConnectionsResponse", @@ -67,14 +60,8 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - case "removeProxy": - command, ok := commandRaw.(*datacommands.RemoveProxy) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StopProxy(command) + case *datacommands.RemoveProxy: + ok, err := helper.Backend.StopProxy(command) var hasAnyFailed bool if !ok { @@ -100,21 +87,15 @@ func (helper *BackendApplicationHelper) Start() error { helper.socket.Write(responseMarshalled) default: - commandType, commandRaw, err := commonbackend.Unmarshal(helper.socket) + _, commandRaw, err := commonbackend.Unmarshal(helper.socket) if err != nil { return err } - switch commandType { - case "start": - command, ok := commandRaw.(*commonbackend.Start) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StartBackend(command.Arguments) + switch command := commandRaw.(type) { + case *commonbackend.Start: + ok, err := helper.Backend.StartBackend(command.Arguments) var ( message string @@ -143,14 +124,8 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "stop": - _, ok := commandRaw.(*commonbackend.Stop) - - if !ok { - return fmt.Errorf("failed to typecast") - } - - ok, err = helper.Backend.StopBackend() + case *commonbackend.Stop: + ok, err := helper.Backend.StopBackend() var ( message string @@ -179,13 +154,7 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "backendStatusRequest": - _, ok := commandRaw.(*commonbackend.BackendStatusRequest) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.BackendStatusRequest: ok, err := helper.Backend.GetBackendStatus() var ( @@ -215,13 +184,7 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "addProxy": - command, ok := commandRaw.(*commonbackend.AddProxy) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.AddProxy: id, ok, err := helper.Backend.StartProxy(command) var hasAnyFailed bool @@ -247,13 +210,7 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) - case "checkClientParameters": - command, ok := commandRaw.(*commonbackend.CheckClientParameters) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.CheckClientParameters: resp := helper.Backend.CheckParametersForConnections(command) resp.Type = "checkParametersResponse" resp.InResponseTo = "checkClientParameters" @@ -267,13 +224,7 @@ func (helper *BackendApplicationHelper) Start() error { if _, err = helper.socket.Write(byteData); err != nil { return err } - case "checkServerParameters": - command, ok := commandRaw.(*commonbackend.CheckServerParameters) - - if !ok { - return fmt.Errorf("failed to typecast") - } - + case *commonbackend.CheckServerParameters: resp := helper.Backend.CheckParametersForBackend(command.Arguments) resp.Type = "checkParametersResponse" resp.InResponseTo = "checkServerParameters" @@ -288,7 +239,7 @@ func (helper *BackendApplicationHelper) Start() error { return err } default: - log.Warn("Unsupported command recieved: %s", commandType) + log.Warnf("Unsupported command recieved: %T", command) } } } From cf90ddb104a273566f64a813cbb82663c88d3339 Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 16 Feb 2025 19:12:17 -0500 Subject: [PATCH 30/45] chore: Strip unneeded components from code. --- backend/api/backendruntime/runtime.go | 116 +-------- backend/api/controllers/v1/backends/create.go | 2 - .../api/controllers/v1/proxies/connections.go | 4 +- backend/api/controllers/v1/proxies/create.go | 1 - backend/api/controllers/v1/proxies/remove.go | 1 - backend/api/controllers/v1/proxies/start.go | 1 - backend/api/controllers/v1/proxies/stop.go | 1 - backend/api/main.go | 12 +- backend/backendutil/application.go | 26 +- backend/commonbackend/constants.go | 15 -- backend/commonbackend/marshal.go | 2 +- backend/commonbackend/marshalling_test.go | 226 +++-------------- backend/commonbackend/unmarshal.go | 161 ++++++------ backend/externalbackendlauncher/main.go | 21 +- .../sshappbackend/datacommands/constants.go | 14 - backend/sshappbackend/datacommands/marshal.go | 2 +- .../datacommands/marshalling_test.go | 240 +++--------------- .../sshappbackend/datacommands/unmarshal.go | 117 ++++----- .../backendutil_custom/application.go | 28 +- 19 files changed, 228 insertions(+), 762 deletions(-) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index a65c077..52e280d 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -15,8 +15,8 @@ import ( "github.com/charmbracelet/log" ) -func handleCommand(commandType string, command interface{}, sock net.Conn, rtcChan chan interface{}) error { - bytes, err := commonbackend.Marshal(commandType, command) +func handleCommand(command interface{}, sock net.Conn, rtcChan chan interface{}) error { + bytes, err := commonbackend.Marshal(command) if err != nil { log.Warnf("Failed to marshal message: %s", err.Error()) @@ -32,7 +32,7 @@ func handleCommand(commandType string, command interface{}, sock net.Conn, rtcCh return fmt.Errorf("failed to write message: %s", err.Error()) } - _, data, err := commonbackend.Unmarshal(sock) + data, err := commonbackend.Unmarshal(sock) if err != nil { log.Warnf("Failed to unmarshal message: %s", err.Error()) @@ -117,9 +117,7 @@ func (runtime *Runtime) goRoutineHandler() error { // To be safe here, we have to use the proper (yet annoying) facilities to prevent cross-talk, since we're in // a goroutine, and can't talk directly. This actually has benefits, as the OuterLoop should exit on its own, if we // encounter a critical error. - statusResponse, err := runtime.ProcessCommand(&commonbackend.BackendStatusRequest{ - Type: "backendStatusRequest", - }) + statusResponse, err := runtime.ProcessCommand(&commonbackend.BackendStatusRequest{}) if err != nil { log.Warnf("Failed to get response for backend (in backend runtime keep alive): %s", err.Error()) @@ -167,110 +165,14 @@ func (runtime *Runtime) goRoutineHandler() error { continue } - switch command := messageData.Message.(type) { - case *commonbackend.AddProxy: - err := handleCommand("addProxy", command, sock, messageData.Channel) + err := handleCommand(messageData.Message, sock, messageData.Channel) - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) + if err != nil { + log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } + if strings.HasPrefix(err.Error(), "failed to write message") { + break OuterLoop } - case *commonbackend.BackendStatusRequest: - err := handleCommand("backendStatusRequest", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.CheckClientParameters: - err := handleCommand("checkClientParameters", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.CheckServerParameters: - err := handleCommand("checkServerParameters", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.ProxyConnectionsRequest: - err := handleCommand("proxyConnectionsRequest", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.ProxyInstanceRequest: - err := handleCommand("proxyInstanceRequest", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.ProxyStatusRequest: - err := handleCommand("proxyStatusRequest", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.RemoveProxy: - err := handleCommand("removeProxy", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.Start: - err := handleCommand("start", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - case *commonbackend.Stop: - err := handleCommand("stop", command, sock, messageData.Channel) - - if err != nil { - log.Warnf("failed to handle command in backend runtime instance: %s", err.Error()) - - if strings.HasPrefix(err.Error(), "failed to write message") { - break OuterLoop - } - } - default: - log.Warnf("Recieved unknown command type from channel: %T", command) - messageData.Channel <- fmt.Errorf("unknown command recieved") } runtime.messageBuffer[chanIndex] = nil diff --git a/backend/api/controllers/v1/backends/create.go b/backend/api/controllers/v1/backends/create.go index 0d8c614..287f314 100644 --- a/backend/api/controllers/v1/backends/create.go +++ b/backend/api/controllers/v1/backends/create.go @@ -126,7 +126,6 @@ func CreateBackend(c *gin.Context) { } backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{ - Type: "checkServerParameters", Arguments: backendParameters, }) @@ -216,7 +215,6 @@ func CreateBackend(c *gin.Context) { } backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{ - Type: "start", Arguments: backendParameters, }) diff --git a/backend/api/controllers/v1/proxies/connections.go b/backend/api/controllers/v1/proxies/connections.go index f46c284..5eb3db9 100644 --- a/backend/api/controllers/v1/proxies/connections.go +++ b/backend/api/controllers/v1/proxies/connections.go @@ -118,9 +118,7 @@ func GetConnections(c *gin.Context) { return } - backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{ - Type: "proxyConnectionsRequest", - }) + backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{}) if err != nil { log.Warnf("Failed to get response for backend: %s", err.Error()) diff --git a/backend/api/controllers/v1/proxies/create.go b/backend/api/controllers/v1/proxies/create.go index 20ad144..28e8c95 100644 --- a/backend/api/controllers/v1/proxies/create.go +++ b/backend/api/controllers/v1/proxies/create.go @@ -142,7 +142,6 @@ func CreateProxy(c *gin.Context) { } backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ - Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, diff --git a/backend/api/controllers/v1/proxies/remove.go b/backend/api/controllers/v1/proxies/remove.go index 8087e29..e41fa84 100644 --- a/backend/api/controllers/v1/proxies/remove.go +++ b/backend/api/controllers/v1/proxies/remove.go @@ -111,7 +111,6 @@ func RemoveProxy(c *gin.Context) { } backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ - Type: "removeProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, diff --git a/backend/api/controllers/v1/proxies/start.go b/backend/api/controllers/v1/proxies/start.go index d0cd5e0..1573382 100644 --- a/backend/api/controllers/v1/proxies/start.go +++ b/backend/api/controllers/v1/proxies/start.go @@ -101,7 +101,6 @@ func StartProxy(c *gin.Context) { } backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ - Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, diff --git a/backend/api/controllers/v1/proxies/stop.go b/backend/api/controllers/v1/proxies/stop.go index 1f5f525..820cec4 100644 --- a/backend/api/controllers/v1/proxies/stop.go +++ b/backend/api/controllers/v1/proxies/stop.go @@ -101,7 +101,6 @@ func StopProxy(c *gin.Context) { } backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ - Type: "removeProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, diff --git a/backend/api/main.go b/backend/api/main.go index 2969e9b..c88f1ae 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -108,8 +108,7 @@ func apiEntrypoint(cCtx *cli.Context) error { return } - marshalledStartCommand, err := commonbackend.Marshal("start", &commonbackend.Start{ - Type: "start", + marshalledStartCommand, err := commonbackend.Marshal(&commonbackend.Start{ Arguments: backendParameters, }) @@ -123,7 +122,7 @@ func apiEntrypoint(cCtx *cli.Context) error { return } - _, backendResponse, err := commonbackend.Unmarshal(conn) + backendResponse, err := commonbackend.Unmarshal(conn) if err != nil { log.Errorf("Failed to get start command response for backend #%d: %s", backend.ID, err.Error()) @@ -152,8 +151,7 @@ func apiEntrypoint(cCtx *cli.Context) error { for _, proxy := range autoStartProxies { log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) - marhalledCommand, err := commonbackend.Marshal("addProxy", &commonbackend.AddProxy{ - Type: "addProxy", + marhalledCommand, err := commonbackend.Marshal(&commonbackend.AddProxy{ SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, @@ -170,7 +168,7 @@ func apiEntrypoint(cCtx *cli.Context) error { continue } - _, backendResponse, err := commonbackend.Unmarshal(conn) + backendResponse, err := commonbackend.Unmarshal(conn) if err != nil { log.Errorf("Failed to get response for backend #%d and route #%d: %s", proxy.BackendID, proxy.ID, err.Error()) @@ -204,7 +202,6 @@ func apiEntrypoint(cCtx *cli.Context) error { } backendStartResponse, err := backendInstance.ProcessCommand(&commonbackend.Start{ - Type: "start", Arguments: backendParameters, }) @@ -257,7 +254,6 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Infof("Starting up route #%d for backend #%d: %s", proxy.ID, backend.ID, proxy.Name) backendResponse, err := backendInstance.ProcessCommand(&commonbackend.AddProxy{ - Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestinationPort, diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index f9fead0..7f134a2 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -34,7 +34,7 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") for { - _, commandRaw, err := commonbackend.Unmarshal(helper.socket) + commandRaw, err := commonbackend.Unmarshal(helper.socket) if err != nil { return err @@ -57,13 +57,12 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -87,13 +86,12 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -117,13 +115,12 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: !ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -144,7 +141,6 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.ProxyStatusResponse{ - Type: "proxyStatusResponse", SourceIP: command.SourceIP, SourcePort: command.SourcePort, DestPort: command.DestPort, @@ -152,7 +148,7 @@ func (helper *BackendApplicationHelper) Start() error { IsActive: !hasAnyFailed, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -173,7 +169,6 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.ProxyStatusResponse{ - Type: "proxyStatusResponse", SourceIP: command.SourceIP, SourcePort: command.SourcePort, DestPort: command.DestPort, @@ -181,7 +176,7 @@ func (helper *BackendApplicationHelper) Start() error { IsActive: hasAnyFailed, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -193,11 +188,10 @@ func (helper *BackendApplicationHelper) Start() error { connections := helper.Backend.GetAllClientConnections() serverParams := &commonbackend.ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", Connections: connections, } - byteData, err := commonbackend.Marshal(serverParams.Type, serverParams) + byteData, err := commonbackend.Marshal(serverParams) if err != nil { return err @@ -208,10 +202,9 @@ func (helper *BackendApplicationHelper) Start() error { } case *commonbackend.CheckClientParameters: resp := helper.Backend.CheckParametersForConnections(command) - resp.Type = "checkParametersResponse" resp.InResponseTo = "checkClientParameters" - byteData, err := commonbackend.Marshal(resp.Type, resp) + byteData, err := commonbackend.Marshal(resp) if err != nil { return err @@ -222,10 +215,9 @@ func (helper *BackendApplicationHelper) Start() error { } case *commonbackend.CheckServerParameters: resp := helper.Backend.CheckParametersForBackend(command.Arguments) - resp.Type = "checkParametersResponse" resp.InResponseTo = "checkServerParameters" - byteData, err := commonbackend.Marshal(resp.Type, resp) + byteData, err := commonbackend.Marshal(resp) if err != nil { return err diff --git a/backend/commonbackend/constants.go b/backend/commonbackend/constants.go index cdb68f2..6d5362b 100644 --- a/backend/commonbackend/constants.go +++ b/backend/commonbackend/constants.go @@ -1,16 +1,13 @@ package commonbackend type Start struct { - Type string // Will be 'start' always Arguments []byte } type Stop struct { - Type string // Will be 'stop' always } type AddProxy struct { - Type string // Will be 'addProxy' always SourceIP string SourcePort uint16 DestPort uint16 @@ -18,7 +15,6 @@ type AddProxy struct { } type RemoveProxy struct { - Type string // Will be 'removeProxy' always SourceIP string SourcePort uint16 DestPort uint16 @@ -26,7 +22,6 @@ type RemoveProxy struct { } type ProxyStatusRequest struct { - Type string // Will be 'proxyStatusRequest' always SourceIP string SourcePort uint16 DestPort uint16 @@ -34,7 +29,6 @@ type ProxyStatusRequest struct { } type ProxyStatusResponse struct { - Type string // Will be 'proxyStatusResponse' always SourceIP string SourcePort uint16 DestPort uint16 @@ -50,27 +44,22 @@ type ProxyInstance struct { } type ProxyInstanceResponse struct { - Type string // Will be 'proxyConnectionResponse' always Proxies []*ProxyInstance // List of connections } type ProxyInstanceRequest struct { - Type string // Will be 'proxyConnectionRequest' always } type BackendStatusResponse struct { - Type string // Will be 'backendStatusResponse' always IsRunning bool // True if running, false if not running StatusCode int // Either the 'Success' or 'Failure' constant Message string // String message from the client (ex. failed to dial TCP) } type BackendStatusRequest struct { - Type string // Will be 'backendStatusRequest' always } type ProxyConnectionsRequest struct { - Type string // Will be 'proxyConnectionsRequest' always } // Client's connection to a specific proxy @@ -83,12 +72,10 @@ type ProxyClientConnection struct { } type ProxyConnectionsResponse struct { - Type string // Will be 'proxyConnectionsResponse' always Connections []*ProxyClientConnection // List of connections } type CheckClientParameters struct { - Type string // Will be 'checkClientParameters' always SourceIP string SourcePort uint16 DestPort uint16 @@ -96,13 +83,11 @@ type CheckClientParameters struct { } type CheckServerParameters struct { - Type string // Will be 'checkServerParameters' always Arguments []byte } // Sent as a response to either CheckClientParameters or CheckBackendParameters type CheckParametersResponse struct { - Type string // Will be 'checkParametersResponse' always InResponseTo string // Will be either 'checkClientParameters' or 'checkServerParameters' IsValid bool // If true, valid, and if false, invalid Message string // String message from the client (ex. failed to unmarshal JSON: x is not defined) diff --git a/backend/commonbackend/marshal.go b/backend/commonbackend/marshal.go index 4496494..7203ee3 100644 --- a/backend/commonbackend/marshal.go +++ b/backend/commonbackend/marshal.go @@ -84,7 +84,7 @@ func marshalIndividualProxyStruct(conn *ProxyInstance) ([]byte, error) { return proxyBlock, nil } -func Marshal(_ string, command interface{}) ([]byte, error) { +func Marshal(command interface{}) ([]byte, error) { switch command := command.(type) { case *Start: startCommandBytes := make([]byte, 1+2+len(command.Arguments)) diff --git a/backend/commonbackend/marshalling_test.go b/backend/commonbackend/marshalling_test.go index e834041..c2b6375 100644 --- a/backend/commonbackend/marshalling_test.go +++ b/backend/commonbackend/marshalling_test.go @@ -11,11 +11,10 @@ var logLevel = os.Getenv("HERMES_LOG_LEVEL") func TestStart(t *testing.T) { commandInput := &Start{ - Type: "start", Arguments: []byte("Hello from automated testing"), } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -26,39 +25,27 @@ func TestStart(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*Start) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) } } func TestStop(t *testing.T) { - commandInput := &Stop{ - Type: "stop", - } + commandInput := &Stop{} - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -69,39 +56,28 @@ func TestStop(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*Stop) + _, ok := commandUnmarshalledRaw.(*Stop) if !ok { t.Fatal("failed typecast") } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } } func TestAddConnection(t *testing.T) { commandInput := &AddProxy{ - Type: "addProxy", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -112,28 +88,18 @@ func TestAddConnection(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*AddProxy) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -157,14 +123,13 @@ func TestAddConnection(t *testing.T) { func TestRemoveConnection(t *testing.T) { commandInput := &RemoveProxy{ - Type: "removeProxy", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -175,28 +140,18 @@ func TestRemoveConnection(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -220,7 +175,6 @@ func TestRemoveConnection(t *testing.T) { func TestGetAllConnections(t *testing.T) { commandInput := &ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", Connections: []*ProxyClientConnection{ { SourceIP: "127.0.0.1", @@ -246,7 +200,7 @@ func TestGetAllConnections(t *testing.T) { }, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -257,28 +211,18 @@ func TestGetAllConnections(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - for commandIndex, originalConnection := range commandInput.Connections { remoteConnection := commandUnmarshalled.Connections[commandIndex] @@ -311,14 +255,13 @@ func TestGetAllConnections(t *testing.T) { func TestCheckClientParameters(t *testing.T) { commandInput := &CheckClientParameters{ - Type: "checkClientParameters", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -329,28 +272,18 @@ func TestCheckClientParameters(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckClientParameters) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -374,11 +307,10 @@ func TestCheckClientParameters(t *testing.T) { func TestCheckServerParameters(t *testing.T) { commandInput := &CheckServerParameters{ - Type: "checkServerParameters", Arguments: []byte("Hello from automated testing"), } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -389,28 +321,18 @@ func TestCheckServerParameters(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckServerParameters) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if !bytes.Equal(commandInput.Arguments, commandUnmarshalled.Arguments) { log.Fatalf("Arguments are not equal (orig: '%s', unmsh: '%s')", string(commandInput.Arguments), string(commandUnmarshalled.Arguments)) } @@ -418,13 +340,12 @@ func TestCheckServerParameters(t *testing.T) { func TestCheckParametersResponse(t *testing.T) { commandInput := &CheckParametersResponse{ - Type: "checkParametersResponse", InResponseTo: "checkClientParameters", IsValid: true, Message: "Hello from automated testing", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -435,28 +356,18 @@ func TestCheckParametersResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Printf("command type does not match up! (orig: %s, unmsh: %s)", commandType, commandInput.Type) - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*CheckParametersResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.InResponseTo != commandUnmarshalled.InResponseTo { t.Fail() log.Printf("InResponseTo's are not equal (orig: %s, unmsh: %s)", commandInput.InResponseTo, commandUnmarshalled.InResponseTo) @@ -474,11 +385,8 @@ func TestCheckParametersResponse(t *testing.T) { } func TestBackendStatusRequest(t *testing.T) { - commandInput := &BackendStatusRequest{ - Type: "backendStatusRequest", - } - - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandInput := &BackendStatusRequest{} + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -489,38 +397,27 @@ func TestBackendStatusRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusRequest) + _, ok := commandUnmarshalledRaw.(*BackendStatusRequest) if !ok { t.Fatal("failed typecast") } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } } func TestBackendStatusResponse(t *testing.T) { commandInput := &BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: true, StatusCode: StatusFailure, Message: "Hello from automated testing", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -531,28 +428,18 @@ func TestBackendStatusResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*BackendStatusResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.IsRunning != commandUnmarshalled.IsRunning { t.Fail() log.Printf("IsRunning's are not equal (orig: %t, unmsh: %t)", commandInput.IsRunning, commandUnmarshalled.IsRunning) @@ -571,14 +458,13 @@ func TestBackendStatusResponse(t *testing.T) { func TestProxyStatusRequest(t *testing.T) { commandInput := &ProxyStatusRequest{ - Type: "proxyStatusRequest", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -589,28 +475,18 @@ func TestProxyStatusRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -634,7 +510,6 @@ func TestProxyStatusRequest(t *testing.T) { func TestProxyStatusResponse(t *testing.T) { commandInput := &ProxyStatusResponse{ - Type: "proxyStatusResponse", SourceIP: "192.168.0.139", SourcePort: 19132, DestPort: 19132, @@ -642,7 +517,7 @@ func TestProxyStatusResponse(t *testing.T) { IsActive: true, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -653,28 +528,18 @@ func TestProxyStatusResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.SourceIP != commandUnmarshalled.SourceIP { t.Fail() log.Printf("SourceIP's are not equal (orig: %s, unmsh: %s)", commandInput.SourceIP, commandUnmarshalled.SourceIP) @@ -702,11 +567,9 @@ func TestProxyStatusResponse(t *testing.T) { } func TestProxyConnectionRequest(t *testing.T) { - commandInput := &ProxyInstanceRequest{ - Type: "proxyInstanceRequest", - } + commandInput := &ProxyInstanceRequest{} - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -717,32 +580,21 @@ func TestProxyConnectionRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceRequest) + _, ok := commandUnmarshalledRaw.(*ProxyInstanceRequest) if !ok { t.Fatal("failed typecast") } - - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } } func TestProxyConnectionResponse(t *testing.T) { commandInput := &ProxyInstanceResponse{ - Type: "proxyInstanceResponse", Proxies: []*ProxyInstance{ { SourceIP: "192.168.0.168", @@ -765,7 +617,7 @@ func TestProxyConnectionResponse(t *testing.T) { }, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -776,28 +628,18 @@ func TestProxyConnectionResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - for proxyIndex, originalProxy := range commandInput.Proxies { remoteProxy := commandUnmarshalled.Proxies[proxyIndex] diff --git a/backend/commonbackend/unmarshal.go b/backend/commonbackend/unmarshal.go index b8500dd..8e338c6 100644 --- a/backend/commonbackend/unmarshal.go +++ b/backend/commonbackend/unmarshal.go @@ -142,11 +142,11 @@ func unmarshalIndividualProxyStruct(conn io.Reader) (*ProxyInstance, error) { }, nil } -func Unmarshal(conn io.Reader) (string, interface{}, error) { +func Unmarshal(conn io.Reader) (interface{}, error) { commandType := make([]byte, 1) if _, err := conn.Read(commandType); err != nil { - return "", nil, fmt.Errorf("couldn't read command") + return nil, fmt.Errorf("couldn't read command") } switch commandType[0] { @@ -154,28 +154,25 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { argumentsLength := make([]byte, 2) if _, err := conn.Read(argumentsLength); err != nil { - return "", nil, fmt.Errorf("couldn't read argument length") + return nil, fmt.Errorf("couldn't read argument length") } arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) if _, err := conn.Read(arguments); err != nil { - return "", nil, fmt.Errorf("couldn't read arguments") + return nil, fmt.Errorf("couldn't read arguments") } - return "start", &Start{ - Type: "start", + return &Start{ Arguments: arguments, }, nil case StopID: - return "stop", &Stop{ - Type: "stop", - }, nil + return &Stop{}, nil case AddProxyID: ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") + return nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -185,31 +182,31 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version recieved") + return nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") + return nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") + return nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") + return nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") + return nil, fmt.Errorf("couldn't read protocol") } var protocol string @@ -219,11 +216,10 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return "", nil, fmt.Errorf("invalid protocol") + return nil, fmt.Errorf("invalid protocol") } - return "addProxy", &AddProxy{ - Type: "addProxy", + return &AddProxy{ SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -233,7 +229,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") + return nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -243,31 +239,31 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version recieved") + return nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") + return nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") + return nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") + return nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") + return nil, fmt.Errorf("couldn't read protocol") } var protocol string @@ -277,11 +273,10 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return "", nil, fmt.Errorf("invalid protocol") + return nil, fmt.Errorf("invalid protocol") } - return "removeProxy", &RemoveProxy{ - Type: "removeProxy", + return &RemoveProxy{ SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -301,13 +296,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { break } - return "", nil, err + return nil, err } connections = append(connections, connection) if _, err := conn.Read(delimiter); err != nil { - return "", nil, fmt.Errorf("couldn't read delimiter") + return nil, fmt.Errorf("couldn't read delimiter") } if delimiter[0] == '\r' { @@ -321,15 +316,14 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } } - return "proxyConnectionsResponse", &ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", + return &ProxyConnectionsResponse{ Connections: connections, }, errorReturn case CheckClientParametersID: ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") + return nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -339,31 +333,31 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version recieved") + return nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") + return nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") + return nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") + return nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") + return nil, fmt.Errorf("couldn't read protocol") } var protocol string @@ -373,11 +367,10 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return "", nil, fmt.Errorf("invalid protocol") + return nil, fmt.Errorf("invalid protocol") } - return "checkClientParameters", &CheckClientParameters{ - Type: "checkClientParameters", + return &CheckClientParameters{ SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -387,24 +380,23 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { argumentsLength := make([]byte, 2) if _, err := conn.Read(argumentsLength); err != nil { - return "", nil, fmt.Errorf("couldn't read argument length") + return nil, fmt.Errorf("couldn't read argument length") } arguments := make([]byte, binary.BigEndian.Uint16(argumentsLength)) if _, err := conn.Read(arguments); err != nil { - return "", nil, fmt.Errorf("couldn't read arguments") + return nil, fmt.Errorf("couldn't read arguments") } - return "checkServerParameters", &CheckServerParameters{ - Type: "checkServerParameters", + return &CheckServerParameters{ Arguments: arguments, }, nil case CheckParametersResponseID: checkMethodByte := make([]byte, 1) if _, err := conn.Read(checkMethodByte); err != nil { - return "", nil, fmt.Errorf("couldn't read check method byte") + return nil, fmt.Errorf("couldn't read check method byte") } var checkMethod string @@ -414,19 +406,19 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if checkMethodByte[0] == CheckServerParametersID { checkMethod = "checkServerParameters" } else { - return "", nil, fmt.Errorf("invalid check method recieved") + return nil, fmt.Errorf("invalid check method recieved") } isValid := make([]byte, 1) if _, err := conn.Read(isValid); err != nil { - return "", nil, fmt.Errorf("couldn't read isValid byte") + return nil, fmt.Errorf("couldn't read isValid byte") } messageLengthBytes := make([]byte, 2) if _, err := conn.Read(messageLengthBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message length") + return nil, fmt.Errorf("couldn't read message length") } messageLength := binary.BigEndian.Uint16(messageLengthBytes) @@ -436,14 +428,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { messageBytes := make([]byte, messageLength) if _, err := conn.Read(messageBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message") + return nil, fmt.Errorf("couldn't read message") } message = string(messageBytes) } - return "checkParametersResponse", &CheckParametersResponse{ - Type: "checkParametersResponse", + return &CheckParametersResponse{ InResponseTo: checkMethod, IsValid: isValid[0] == 1, Message: message, @@ -452,19 +443,19 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { isRunning := make([]byte, 1) if _, err := conn.Read(isRunning); err != nil { - return "", nil, fmt.Errorf("couldn't read isRunning field") + return nil, fmt.Errorf("couldn't read isRunning field") } statusCode := make([]byte, 1) if _, err := conn.Read(statusCode); err != nil { - return "", nil, fmt.Errorf("couldn't read status code field") + return nil, fmt.Errorf("couldn't read status code field") } messageLengthBytes := make([]byte, 2) if _, err := conn.Read(messageLengthBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message length") + return nil, fmt.Errorf("couldn't read message length") } messageLength := binary.BigEndian.Uint16(messageLengthBytes) @@ -474,27 +465,24 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { messageBytes := make([]byte, messageLength) if _, err := conn.Read(messageBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read message") + return nil, fmt.Errorf("couldn't read message") } message = string(messageBytes) } - return "backendStatusResponse", &BackendStatusResponse{ - Type: "backendStatusResponse", + return &BackendStatusResponse{ IsRunning: isRunning[0] == 1, StatusCode: int(statusCode[0]), Message: message, }, nil case BackendStatusRequestID: - return "backendStatusRequest", &BackendStatusRequest{ - Type: "backendStatusRequest", - }, nil + return &BackendStatusRequest{}, nil case ProxyStatusRequestID: ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") + return nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -504,31 +492,31 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version recieved") + return nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") + return nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") + return nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") + return nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") + return nil, fmt.Errorf("couldn't read protocol") } var protocol string @@ -538,11 +526,10 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return "", nil, fmt.Errorf("invalid protocol") + return nil, fmt.Errorf("invalid protocol") } - return "proxyStatusRequest", &ProxyStatusRequest{ - Type: "proxyStatusRequest", + return &ProxyStatusRequest{ SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -552,7 +539,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { ipVersion := make([]byte, 1) if _, err := conn.Read(ipVersion); err != nil { - return "", nil, fmt.Errorf("couldn't read ip version") + return nil, fmt.Errorf("couldn't read ip version") } var ipSize uint8 @@ -562,31 +549,31 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVersion[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version recieved") + return nil, fmt.Errorf("invalid IP version recieved") } ip := make(net.IP, ipSize) if _, err := conn.Read(ip); err != nil { - return "", nil, fmt.Errorf("couldn't read source IP") + return nil, fmt.Errorf("couldn't read source IP") } sourcePort := make([]byte, 2) if _, err := conn.Read(sourcePort); err != nil { - return "", nil, fmt.Errorf("couldn't read source port") + return nil, fmt.Errorf("couldn't read source port") } destPort := make([]byte, 2) if _, err := conn.Read(destPort); err != nil { - return "", nil, fmt.Errorf("couldn't read destination port") + return nil, fmt.Errorf("couldn't read destination port") } protocolBytes := make([]byte, 1) if _, err := conn.Read(protocolBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read protocol") + return nil, fmt.Errorf("couldn't read protocol") } var protocol string @@ -596,17 +583,16 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if protocolBytes[1] == UDP { protocol = "udp" } else { - return "", nil, fmt.Errorf("invalid protocol") + return nil, fmt.Errorf("invalid protocol") } isActive := make([]byte, 1) if _, err := conn.Read(isActive); err != nil { - return "", nil, fmt.Errorf("couldn't read isActive field") + return nil, fmt.Errorf("couldn't read isActive field") } - return "proxyStatusResponse", &ProxyStatusResponse{ - Type: "proxyStatusResponse", + return &ProxyStatusResponse{ SourceIP: ip.String(), SourcePort: binary.BigEndian.Uint16(sourcePort), DestPort: binary.BigEndian.Uint16(destPort), @@ -614,9 +600,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { IsActive: isActive[0] == 1, }, nil case ProxyInstanceRequestID: - return "proxyInstanceRequest", &ProxyInstanceRequest{ - Type: "proxyInstanceRequest", - }, nil + return &ProxyInstanceRequest{}, nil case ProxyInstanceResponseID: proxies := []*ProxyInstance{} delimiter := make([]byte, 1) @@ -631,13 +615,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { break } - return "", nil, err + return nil, err } proxies = append(proxies, proxy) if _, err := conn.Read(delimiter); err != nil { - return "", nil, fmt.Errorf("couldn't read delimiter") + return nil, fmt.Errorf("couldn't read delimiter") } if delimiter[0] == '\r' { @@ -651,15 +635,12 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } } - return "proxyInstanceResponse", &ProxyInstanceResponse{ - Type: "proxyInstanceResponse", + return &ProxyInstanceResponse{ Proxies: proxies, }, errorReturn case ProxyConnectionsRequestID: - return "proxyConnectionsRequest", &ProxyConnectionsRequest{ - Type: "proxyConnectionsRequest", - }, nil + return &ProxyConnectionsRequest{}, nil } - return "", nil, fmt.Errorf("couldn't match command ID") + return nil, fmt.Errorf("couldn't match command ID") } diff --git a/backend/externalbackendlauncher/main.go b/backend/externalbackendlauncher/main.go index d33d6cd..c196866 100644 --- a/backend/externalbackendlauncher/main.go +++ b/backend/externalbackendlauncher/main.go @@ -112,11 +112,10 @@ func entrypoint(cCtx *cli.Context) error { defer sock.Close() startCommand := &commonbackend.Start{ - Type: "start", Arguments: backendParameters, } - startMarshalledCommand, err := commonbackend.Marshal("start", startCommand) + startMarshalledCommand, err := commonbackend.Marshal(startCommand) if err != nil { log.Errorf("failed to generate start command: %s", err.Error()) @@ -128,18 +127,13 @@ func entrypoint(cCtx *cli.Context) error { continue } - commandType, commandRaw, err := commonbackend.Unmarshal(sock) + commandRaw, err := commonbackend.Unmarshal(sock) if err != nil { log.Errorf("failed to read from/unmarshal from socket: %s", err.Error()) continue } - if commandType != "backendStatusResponse" { - log.Errorf("recieved commandType '%s', expecting 'backendStatusResponse'", commandType) - continue - } - command, ok := commandRaw.(*commonbackend.BackendStatusResponse) if !ok { @@ -168,14 +162,13 @@ func entrypoint(cCtx *cli.Context) error { log.Infof("initializing proxy %s:%d -> remote:%d", proxy.SourceIP, proxy.SourcePort, proxy.DestPort) proxyAddCommand := &commonbackend.AddProxy{ - Type: "addProxy", SourceIP: proxy.SourceIP, SourcePort: proxy.SourcePort, DestPort: proxy.DestPort, Protocol: proxy.Protocol, } - marshalledProxyCommand, err := commonbackend.Marshal("addProxy", proxyAddCommand) + marshalledProxyCommand, err := commonbackend.Marshal(proxyAddCommand) if err != nil { log.Errorf("failed to generate start command: %s", err.Error()) @@ -189,7 +182,7 @@ func entrypoint(cCtx *cli.Context) error { continue } - commandType, commandRaw, err := commonbackend.Unmarshal(sock) + commandRaw, err := commonbackend.Unmarshal(sock) if err != nil { log.Errorf("failed to read from/unmarshal from socket: %s", err.Error()) @@ -197,12 +190,6 @@ func entrypoint(cCtx *cli.Context) error { continue } - if commandType != "proxyStatusResponse" { - log.Errorf("recieved commandType '%s', expecting 'proxyStatusResponse'", commandType) - hasAnyFailed = true - continue - } - command, ok := commandRaw.(*commonbackend.ProxyStatusResponse) if !ok { diff --git a/backend/sshappbackend/datacommands/constants.go b/backend/sshappbackend/datacommands/constants.go index 7b7620b..9630d43 100644 --- a/backend/sshappbackend/datacommands/constants.go +++ b/backend/sshappbackend/datacommands/constants.go @@ -1,57 +1,47 @@ package datacommands type ProxyStatusRequest struct { - Type string ProxyID uint16 } type ProxyStatusResponse struct { - Type string ProxyID uint16 IsActive bool } type RemoveProxy struct { - Type string ProxyID uint16 } type ProxyInstanceResponse struct { - Type string Proxies []uint16 } type ProxyConnectionsRequest struct { - Type string ProxyID uint16 } type ProxyConnectionsResponse struct { - Type string Connections []uint16 } type TCPConnectionOpened struct { - Type string ProxyID uint16 ConnectionID uint16 } type TCPConnectionClosed struct { - Type string ProxyID uint16 ConnectionID uint16 } type TCPProxyData struct { - Type string ProxyID uint16 ConnectionID uint16 DataLength uint16 } type UDPProxyData struct { - Type string ProxyID uint16 ClientIP string ClientPort uint16 @@ -59,12 +49,10 @@ type UDPProxyData struct { } type ProxyInformationRequest struct { - Type string ProxyID uint16 } type ProxyInformationResponse struct { - Type string Exists bool SourceIP string SourcePort uint16 @@ -73,13 +61,11 @@ type ProxyInformationResponse struct { } type ProxyConnectionInformationRequest struct { - Type string ProxyID uint16 ConnectionID uint16 } type ProxyConnectionInformationResponse struct { - Type string Exists bool ClientIP string ClientPort uint16 diff --git a/backend/sshappbackend/datacommands/marshal.go b/backend/sshappbackend/datacommands/marshal.go index bacbda1..a2c13bf 100644 --- a/backend/sshappbackend/datacommands/marshal.go +++ b/backend/sshappbackend/datacommands/marshal.go @@ -16,7 +16,7 @@ const ( ) // Marshal takes a command (pointer to one of our structs) and converts it to a byte slice. -func Marshal(_ string, command interface{}) ([]byte, error) { +func Marshal(command interface{}) ([]byte, error) { switch cmd := command.(type) { // ProxyStatusRequest: 1 byte for the command ID + 2 bytes for the ProxyID. case *ProxyStatusRequest: diff --git a/backend/sshappbackend/datacommands/marshalling_test.go b/backend/sshappbackend/datacommands/marshalling_test.go index 81e7101..5b2e5ab 100644 --- a/backend/sshappbackend/datacommands/marshalling_test.go +++ b/backend/sshappbackend/datacommands/marshalling_test.go @@ -11,11 +11,10 @@ var logLevel = os.Getenv("HERMES_LOG_LEVEL") func TestProxyStatusRequest(t *testing.T) { commandInput := &ProxyStatusRequest{ - Type: "proxyStatusRequest", ProxyID: 19132, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -26,28 +25,18 @@ func TestProxyStatusRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusRequest) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -56,12 +45,11 @@ func TestProxyStatusRequest(t *testing.T) { func TestProxyStatusResponse(t *testing.T) { commandInput := &ProxyStatusResponse{ - Type: "proxyStatusResponse", ProxyID: 19132, IsActive: true, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -72,28 +60,18 @@ func TestProxyStatusResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyStatusResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -107,11 +85,10 @@ func TestProxyStatusResponse(t *testing.T) { func TestRemoveProxy(t *testing.T) { commandInput := &RemoveProxy{ - Type: "removeProxy", ProxyID: 19132, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -122,28 +99,18 @@ func TestRemoveProxy(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*RemoveProxy) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -152,11 +119,10 @@ func TestRemoveProxy(t *testing.T) { func TestProxyConnectionsRequest(t *testing.T) { commandInput := &ProxyConnectionsRequest{ - Type: "proxyConnectionsRequest", ProxyID: 19132, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -167,28 +133,18 @@ func TestProxyConnectionsRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsRequest) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -197,11 +153,10 @@ func TestProxyConnectionsRequest(t *testing.T) { func TestProxyConnectionsResponse(t *testing.T) { commandInput := &ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", Connections: []uint16{12831, 9455, 64219, 12, 32}, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -212,28 +167,18 @@ func TestProxyConnectionsResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionsResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - for connectionIndex, originalConnection := range commandInput.Connections { remoteConnection := commandUnmarshalled.Connections[connectionIndex] @@ -246,11 +191,10 @@ func TestProxyConnectionsResponse(t *testing.T) { func TestProxyInstanceResponse(t *testing.T) { commandInput := &ProxyInstanceResponse{ - Type: "proxyInstanceResponse", Proxies: []uint16{12831, 9455, 64219, 12, 32}, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -261,28 +205,18 @@ func TestProxyInstanceResponse(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInstanceResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - for proxyIndex, originalProxy := range commandInput.Proxies { remoteProxy := commandUnmarshalled.Proxies[proxyIndex] @@ -295,12 +229,11 @@ func TestProxyInstanceResponse(t *testing.T) { func TestTCPConnectionOpened(t *testing.T) { commandInput := &TCPConnectionOpened{ - Type: "tcpConnectionOpened", ProxyID: 19132, ConnectionID: 25565, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -311,28 +244,18 @@ func TestTCPConnectionOpened(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPConnectionOpened) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -346,12 +269,11 @@ func TestTCPConnectionOpened(t *testing.T) { func TestTCPConnectionClosed(t *testing.T) { commandInput := &TCPConnectionClosed{ - Type: "tcpConnectionClosed", ProxyID: 19132, ConnectionID: 25565, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -362,28 +284,18 @@ func TestTCPConnectionClosed(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPConnectionClosed) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -397,13 +309,12 @@ func TestTCPConnectionClosed(t *testing.T) { func TestTCPProxyData(t *testing.T) { commandInput := &TCPProxyData{ - Type: "tcpProxyData", ProxyID: 19132, ConnectionID: 25565, DataLength: 1234, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -414,28 +325,18 @@ func TestTCPProxyData(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*TCPProxyData) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -454,14 +355,13 @@ func TestTCPProxyData(t *testing.T) { func TestUDPProxyData(t *testing.T) { commandInput := &UDPProxyData{ - Type: "udpProxyData", ProxyID: 19132, ClientIP: "68.51.23.54", ClientPort: 28173, DataLength: 1234, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -472,28 +372,18 @@ func TestUDPProxyData(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*UDPProxyData) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -517,11 +407,10 @@ func TestUDPProxyData(t *testing.T) { func TestProxyInformationRequest(t *testing.T) { commandInput := &ProxyInformationRequest{ - Type: "proxyInformationRequest", ProxyID: 19132, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -532,28 +421,18 @@ func TestProxyInformationRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationRequest) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -562,7 +441,6 @@ func TestProxyInformationRequest(t *testing.T) { func TestProxyInformationResponseExists(t *testing.T) { commandInput := &ProxyInformationResponse{ - Type: "proxyInformationResponse", Exists: true, SourceIP: "192.168.0.139", SourcePort: 19132, @@ -570,7 +448,7 @@ func TestProxyInformationResponseExists(t *testing.T) { Protocol: "tcp", } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -581,28 +459,18 @@ func TestProxyInformationResponseExists(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.Exists != commandUnmarshalled.Exists { t.Fail() log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) @@ -631,11 +499,10 @@ func TestProxyInformationResponseExists(t *testing.T) { func TestProxyInformationResponseNoExist(t *testing.T) { commandInput := &ProxyInformationResponse{ - Type: "proxyInformationResponse", Exists: false, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -646,28 +513,18 @@ func TestProxyInformationResponseNoExist(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyInformationResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.Exists != commandUnmarshalled.Exists { t.Fail() log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) @@ -676,12 +533,11 @@ func TestProxyInformationResponseNoExist(t *testing.T) { func TestProxyConnectionInformationRequest(t *testing.T) { commandInput := &ProxyConnectionInformationRequest{ - Type: "proxyConnectionInformationRequest", ProxyID: 19132, ConnectionID: 25565, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if logLevel == "debug" { log.Printf("Generated array contents: %v", commandMarshalled) @@ -692,28 +548,18 @@ func TestProxyConnectionInformationRequest(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationRequest) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.ProxyID != commandUnmarshalled.ProxyID { t.Fail() log.Printf("ProxyID's are not equal (orig: '%d', unmsh: '%d')", commandInput.ProxyID, commandUnmarshalled.ProxyID) @@ -727,13 +573,12 @@ func TestProxyConnectionInformationRequest(t *testing.T) { func TestProxyConnectionInformationResponseExists(t *testing.T) { commandInput := &ProxyConnectionInformationResponse{ - Type: "proxyConnectionInformationResponse", Exists: true, ClientIP: "192.168.0.139", ClientPort: 19132, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -744,28 +589,18 @@ func TestProxyConnectionInformationResponseExists(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.Exists != commandUnmarshalled.Exists { t.Fail() log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) @@ -784,11 +619,10 @@ func TestProxyConnectionInformationResponseExists(t *testing.T) { func TestProxyConnectionInformationResponseNoExists(t *testing.T) { commandInput := &ProxyConnectionInformationResponse{ - Type: "proxyConnectionInformationResponse", Exists: false, } - commandMarshalled, err := Marshal(commandInput.Type, commandInput) + commandMarshalled, err := Marshal(commandInput) if err != nil { t.Fatal(err.Error()) @@ -799,28 +633,18 @@ func TestProxyConnectionInformationResponseNoExists(t *testing.T) { } buf := bytes.NewBuffer(commandMarshalled) - commandType, commandUnmarshalledRaw, err := Unmarshal(buf) + commandUnmarshalledRaw, err := Unmarshal(buf) if err != nil { t.Fatal(err.Error()) } - if commandType != commandInput.Type { - t.Fail() - log.Print("command type does not match up!") - } - commandUnmarshalled, ok := commandUnmarshalledRaw.(*ProxyConnectionInformationResponse) if !ok { t.Fatal("failed typecast") } - if commandInput.Type != commandUnmarshalled.Type { - t.Fail() - log.Printf("Types are not equal (orig: %s, unmsh: %s)", commandInput.Type, commandUnmarshalled.Type) - } - if commandInput.Exists != commandUnmarshalled.Exists { t.Fail() log.Printf("Exists's are not equal (orig: '%t', unmsh: '%t')", commandInput.Exists, commandUnmarshalled.Exists) diff --git a/backend/sshappbackend/datacommands/unmarshal.go b/backend/sshappbackend/datacommands/unmarshal.go index 7e97c3f..d9d0523 100644 --- a/backend/sshappbackend/datacommands/unmarshal.go +++ b/backend/sshappbackend/datacommands/unmarshal.go @@ -9,11 +9,12 @@ import ( // Unmarshal reads from the provided connection and returns // the message type (as a string), the unmarshalled struct, or an error. -func Unmarshal(conn io.Reader) (string, interface{}, error) { +func Unmarshal(conn io.Reader) (interface{}, error) { // Every command starts with a 1-byte command ID. header := make([]byte, 1) + if _, err := io.ReadFull(conn, header); err != nil { - return "", nil, fmt.Errorf("couldn't read command ID: %w", err) + return nil, fmt.Errorf("couldn't read command ID: %w", err) } cmdID := header[0] @@ -23,13 +24,12 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyStatusRequest ProxyID: %w", err) + return nil, fmt.Errorf("couldn't read ProxyStatusRequest ProxyID: %w", err) } proxyID := binary.BigEndian.Uint16(buf) - return "proxyStatusRequest", &ProxyStatusRequest{ - Type: "proxyStatusRequest", + return &ProxyStatusRequest{ ProxyID: proxyID, }, nil @@ -38,20 +38,19 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyStatusResponse ProxyID: %w", err) + return nil, fmt.Errorf("couldn't read ProxyStatusResponse ProxyID: %w", err) } proxyID := binary.BigEndian.Uint16(buf) boolBuf := make([]byte, 1) if _, err := io.ReadFull(conn, boolBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyStatusResponse IsActive: %w", err) + return nil, fmt.Errorf("couldn't read ProxyStatusResponse IsActive: %w", err) } isActive := boolBuf[0] != 0 - return "proxyStatusResponse", &ProxyStatusResponse{ - Type: "proxyStatusResponse", + return &ProxyStatusResponse{ ProxyID: proxyID, IsActive: isActive, }, nil @@ -61,13 +60,12 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read RemoveProxy ProxyID: %w", err) + return nil, fmt.Errorf("couldn't read RemoveProxy ProxyID: %w", err) } proxyID := binary.BigEndian.Uint16(buf) - return "removeProxy", &RemoveProxy{ - Type: "removeProxy", + return &RemoveProxy{ ProxyID: proxyID, }, nil @@ -76,13 +74,12 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionsRequest ProxyID: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionsRequest ProxyID: %w", err) } proxyID := binary.BigEndian.Uint16(buf) - return "proxyConnectionsRequest", &ProxyConnectionsRequest{ - Type: "proxyConnectionsRequest", + return &ProxyConnectionsRequest{ ProxyID: proxyID, }, nil @@ -91,7 +88,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) } length := binary.BigEndian.Uint16(buf) @@ -108,8 +105,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { connections[connectionIndex] = binary.BigEndian.Uint16(buf) } - return "proxyConnectionsResponse", &ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", + return &ProxyConnectionsResponse{ Connections: connections, }, failedDuringReading @@ -118,7 +114,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionsResponse length: %w", err) } length := binary.BigEndian.Uint16(buf) @@ -135,8 +131,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { proxies[connectionIndex] = binary.BigEndian.Uint16(buf) } - return "proxyInstanceResponse", &ProxyInstanceResponse{ - Type: "proxyInstanceResponse", + return &ProxyInstanceResponse{ Proxies: proxies, }, failedDuringReading @@ -145,14 +140,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2+2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read TCPConnectionOpened fields: %w", err) + return nil, fmt.Errorf("couldn't read TCPConnectionOpened fields: %w", err) } proxyID := binary.BigEndian.Uint16(buf[0:2]) connectionID := binary.BigEndian.Uint16(buf[2:4]) - return "tcpConnectionOpened", &TCPConnectionOpened{ - Type: "tcpConnectionOpened", + return &TCPConnectionOpened{ ProxyID: proxyID, ConnectionID: connectionID, }, nil @@ -162,14 +156,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2+2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read TCPConnectionClosed fields: %w", err) + return nil, fmt.Errorf("couldn't read TCPConnectionClosed fields: %w", err) } proxyID := binary.BigEndian.Uint16(buf[0:2]) connectionID := binary.BigEndian.Uint16(buf[2:4]) - return "tcpConnectionClosed", &TCPConnectionClosed{ - Type: "tcpConnectionClosed", + return &TCPConnectionClosed{ ProxyID: proxyID, ConnectionID: connectionID, }, nil @@ -179,15 +172,14 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2+2+2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read TCPProxyData fields: %w", err) + return nil, fmt.Errorf("couldn't read TCPProxyData fields: %w", err) } proxyID := binary.BigEndian.Uint16(buf[0:2]) connectionID := binary.BigEndian.Uint16(buf[2:4]) dataLength := binary.BigEndian.Uint16(buf[4:6]) - return "tcpProxyData", &TCPProxyData{ - Type: "tcpProxyData", + return &TCPProxyData{ ProxyID: proxyID, ConnectionID: connectionID, DataLength: dataLength, @@ -201,7 +193,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read UDPProxyData ProxyID/ConnectionID: %w", err) + return nil, fmt.Errorf("couldn't read UDPProxyData ProxyID/ConnectionID: %w", err) } proxyID := binary.BigEndian.Uint16(buf) @@ -210,7 +202,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { ipVerBuf := make([]byte, 1) if _, err := io.ReadFull(conn, ipVerBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read UDPProxyData IP version: %w", err) + return nil, fmt.Errorf("couldn't read UDPProxyData IP version: %w", err) } var ipSize int @@ -220,13 +212,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVerBuf[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version received: %v", ipVerBuf[0]) + return nil, fmt.Errorf("invalid IP version received: %v", ipVerBuf[0]) } // Read the IP bytes. ipBytes := make([]byte, ipSize) if _, err := io.ReadFull(conn, ipBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read UDPProxyData IP bytes: %w", err) + return nil, fmt.Errorf("couldn't read UDPProxyData IP bytes: %w", err) } clientIP := net.IP(ipBytes).String() @@ -234,7 +226,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { portBuf := make([]byte, 2) if _, err := io.ReadFull(conn, portBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read UDPProxyData ClientPort: %w", err) + return nil, fmt.Errorf("couldn't read UDPProxyData ClientPort: %w", err) } clientPort := binary.BigEndian.Uint16(portBuf) @@ -243,13 +235,12 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { dataLengthBuf := make([]byte, 2) if _, err := io.ReadFull(conn, dataLengthBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read UDPProxyData DataLength: %w", err) + return nil, fmt.Errorf("couldn't read UDPProxyData DataLength: %w", err) } dataLength := binary.BigEndian.Uint16(dataLengthBuf) - return "udpProxyData", &UDPProxyData{ - Type: "udpProxyData", + return &UDPProxyData{ ProxyID: proxyID, ClientIP: clientIP, ClientPort: clientPort, @@ -261,13 +252,12 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyInformationRequest ProxyID: %w", err) + return nil, fmt.Errorf("couldn't read ProxyInformationRequest ProxyID: %w", err) } proxyID := binary.BigEndian.Uint16(buf) - return "proxyInformationRequest", &ProxyInformationRequest{ - Type: "proxyInformationRequest", + return &ProxyInformationRequest{ ProxyID: proxyID, }, nil @@ -279,14 +269,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { boolBuf := make([]byte, 1) if _, err := io.ReadFull(conn, boolBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse Exists flag: %w", err) + return nil, fmt.Errorf("couldn't read ProxyInformationResponse Exists flag: %w", err) } exists := boolBuf[0] != 0 if !exists { - return "proxyInformationResponse", &ProxyInformationResponse{ - Type: "proxyInformationResponse", + return &ProxyInformationResponse{ Exists: exists, }, nil } @@ -295,7 +284,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { ipVerBuf := make([]byte, 1) if _, err := io.ReadFull(conn, ipVerBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse IP version: %w", err) + return nil, fmt.Errorf("couldn't read ProxyInformationResponse IP version: %w", err) } var ipSize int @@ -305,14 +294,14 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { } else if ipVerBuf[0] == 6 { ipSize = IPv6Size } else { - return "", nil, fmt.Errorf("invalid IP version in ProxyInformationResponse: %v", ipVerBuf[0]) + return nil, fmt.Errorf("invalid IP version in ProxyInformationResponse: %v", ipVerBuf[0]) } // Read the source IP bytes. ipBytes := make([]byte, ipSize) if _, err := io.ReadFull(conn, ipBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse IP bytes: %w", err) + return nil, fmt.Errorf("couldn't read ProxyInformationResponse IP bytes: %w", err) } sourceIP := net.IP(ipBytes).String() @@ -321,7 +310,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { portsBuf := make([]byte, 2+2) if _, err := io.ReadFull(conn, portsBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse ports: %w", err) + return nil, fmt.Errorf("couldn't read ProxyInformationResponse ports: %w", err) } sourcePort := binary.BigEndian.Uint16(portsBuf[0:2]) @@ -331,19 +320,20 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { protoBuf := make([]byte, 1) if _, err := io.ReadFull(conn, protoBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyInformationResponse protocol: %w", err) + return nil, fmt.Errorf("couldn't read ProxyInformationResponse protocol: %w", err) } + var protocol string + if protoBuf[0] == TCP { protocol = "tcp" } else if protoBuf[0] == UDP { protocol = "udp" } else { - return "", nil, fmt.Errorf("invalid protocol value in ProxyInformationResponse: %d", protoBuf[0]) + return nil, fmt.Errorf("invalid protocol value in ProxyInformationResponse: %d", protoBuf[0]) } - return "proxyInformationResponse", &ProxyInformationResponse{ - Type: "proxyInformationResponse", + return &ProxyInformationResponse{ Exists: exists, SourceIP: sourceIP, SourcePort: sourcePort, @@ -356,14 +346,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { buf := make([]byte, 2+2) if _, err := io.ReadFull(conn, buf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationRequest fields: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionInformationRequest fields: %w", err) } proxyID := binary.BigEndian.Uint16(buf[0:2]) connectionID := binary.BigEndian.Uint16(buf[2:4]) - return "proxyConnectionInformationRequest", &ProxyConnectionInformationRequest{ - Type: "proxyConnectionInformationRequest", + return &ProxyConnectionInformationRequest{ ProxyID: proxyID, ConnectionID: connectionID, }, nil @@ -374,14 +363,13 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { // Read Exists flag. boolBuf := make([]byte, 1) if _, err := io.ReadFull(conn, boolBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse Exists flag: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse Exists flag: %w", err) } exists := boolBuf[0] != 0 if !exists { - return "proxyConnectionInformationResponse", &ProxyConnectionInformationResponse{ - Type: "proxyConnectionInformationResponse", + return &ProxyConnectionInformationResponse{ Exists: exists, }, nil } @@ -390,11 +378,11 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { ipVerBuf := make([]byte, 1) if _, err := io.ReadFull(conn, ipVerBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP version: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP version: %w", err) } if ipVerBuf[0] != 4 && ipVerBuf[0] != 6 { - return "", nil, fmt.Errorf("invalid IP version in ProxyConnectionInformationResponse: %v", ipVerBuf[0]) + return nil, fmt.Errorf("invalid IP version in ProxyConnectionInformationResponse: %v", ipVerBuf[0]) } var ipSize int @@ -409,7 +397,7 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { ipBytes := make([]byte, ipSize) if _, err := io.ReadFull(conn, ipBytes); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP bytes: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse IP bytes: %w", err) } clientIP := net.IP(ipBytes).String() @@ -418,18 +406,17 @@ func Unmarshal(conn io.Reader) (string, interface{}, error) { portBuf := make([]byte, 2) if _, err := io.ReadFull(conn, portBuf); err != nil { - return "", nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse ClientPort: %w", err) + return nil, fmt.Errorf("couldn't read ProxyConnectionInformationResponse ClientPort: %w", err) } clientPort := binary.BigEndian.Uint16(portBuf) - return "proxyConnectionInformationResponse", &ProxyConnectionInformationResponse{ - Type: "proxyConnectionInformationResponse", + return &ProxyConnectionInformationResponse{ Exists: exists, ClientIP: clientIP, ClientPort: clientPort, }, nil default: - return "", nil, fmt.Errorf("unknown command id: %v", cmdID) + return nil, fmt.Errorf("unknown command id: %v", cmdID) } } diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go index e058648..5ad2246 100644 --- a/backend/sshappbackend/remote-code/backendutil_custom/application.go +++ b/backend/sshappbackend/remote-code/backendutil_custom/application.go @@ -36,7 +36,7 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") for { - _, commandRaw, err := datacommands.Unmarshal(helper.socket) + commandRaw, err := datacommands.Unmarshal(helper.socket) if err != nil && err.Error() != "couldn't match command ID" { return err @@ -47,11 +47,10 @@ func (helper *BackendApplicationHelper) Start() error { connections := helper.Backend.GetAllClientConnections(command.ProxyID) serverParams := &datacommands.ProxyConnectionsResponse{ - Type: "proxyConnectionsResponse", Connections: connections, } - byteData, err := datacommands.Marshal(serverParams.Type, serverParams) + byteData, err := datacommands.Marshal(serverParams) if err != nil { return err @@ -73,12 +72,11 @@ func (helper *BackendApplicationHelper) Start() error { } response := &datacommands.ProxyStatusResponse{ - Type: "proxyStatusResponse", ProxyID: command.ProxyID, IsActive: hasAnyFailed, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := datacommands.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -87,7 +85,7 @@ func (helper *BackendApplicationHelper) Start() error { helper.socket.Write(responseMarshalled) default: - _, commandRaw, err := commonbackend.Unmarshal(helper.socket) + commandRaw, err := commonbackend.Unmarshal(helper.socket) if err != nil { return err @@ -110,13 +108,12 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -140,13 +137,12 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: !ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -170,13 +166,12 @@ func (helper *BackendApplicationHelper) Start() error { } response := &commonbackend.BackendStatusResponse{ - Type: "backendStatusResponse", IsRunning: ok, StatusCode: statusCode, Message: message, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := commonbackend.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -197,12 +192,11 @@ func (helper *BackendApplicationHelper) Start() error { } response := &datacommands.ProxyStatusResponse{ - Type: "proxyStatusResponse", ProxyID: id, IsActive: !hasAnyFailed, } - responseMarshalled, err := commonbackend.Marshal(response.Type, response) + responseMarshalled, err := datacommands.Marshal(response) if err != nil { log.Error("failed to marshal response: %s", err.Error()) @@ -212,10 +206,9 @@ func (helper *BackendApplicationHelper) Start() error { helper.socket.Write(responseMarshalled) case *commonbackend.CheckClientParameters: resp := helper.Backend.CheckParametersForConnections(command) - resp.Type = "checkParametersResponse" resp.InResponseTo = "checkClientParameters" - byteData, err := commonbackend.Marshal(resp.Type, resp) + byteData, err := commonbackend.Marshal(resp) if err != nil { return err @@ -226,10 +219,9 @@ func (helper *BackendApplicationHelper) Start() error { } case *commonbackend.CheckServerParameters: resp := helper.Backend.CheckParametersForBackend(command.Arguments) - resp.Type = "checkParametersResponse" resp.InResponseTo = "checkServerParameters" - byteData, err := commonbackend.Marshal(resp.Type, resp) + byteData, err := commonbackend.Marshal(resp) if err != nil { return err From 432d457ad7746fc3fe9247930cd173939fa4f6db Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 16 Feb 2025 21:51:33 -0500 Subject: [PATCH 31/45] feature: Adds remote implementation of code. --- .../sshappbackend/datacommands/constants.go | 1 + .../backendutil_custom/application.go | 43 +++ .../backendutil_custom/structure.go | 6 +- backend/sshappbackend/remote-code/main.go | 363 +++++++++++++++++- 4 files changed, 396 insertions(+), 17 deletions(-) diff --git a/backend/sshappbackend/datacommands/constants.go b/backend/sshappbackend/datacommands/constants.go index 9630d43..6385e98 100644 --- a/backend/sshappbackend/datacommands/constants.go +++ b/backend/sshappbackend/datacommands/constants.go @@ -1,5 +1,6 @@ package datacommands +// DO NOT USE type ProxyStatusRequest struct { ProxyID uint16 } diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go index 5ad2246..0e00da7 100644 --- a/backend/sshappbackend/remote-code/backendutil_custom/application.go +++ b/backend/sshappbackend/remote-code/backendutil_custom/application.go @@ -1,6 +1,7 @@ package backendutil_custom import ( + "io" "net" "os" @@ -33,6 +34,8 @@ func (helper *BackendApplicationHelper) Start() error { return err } + helper.Backend.OnSocketConnection(helper.socket) + log.Debug("Sucessfully connected") for { @@ -84,6 +87,46 @@ func (helper *BackendApplicationHelper) Start() error { } helper.socket.Write(responseMarshalled) + case *datacommands.ProxyInformationRequest: + response := helper.Backend.ResolveProxy(command.ProxyID) + responseMarshalled, err := datacommands.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *datacommands.ProxyConnectionInformationRequest: + response := helper.Backend.ResolveConnection(command.ProxyID, command.ConnectionID) + responseMarshalled, err := datacommands.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *datacommands.TCPConnectionClosed: + helper.Backend.OnTCPConnectionClosed(command.ProxyID, command.ConnectionID) + case *datacommands.TCPProxyData: + bytes := make([]byte, command.DataLength) + _, err := io.ReadFull(helper.socket, bytes) + + if err != nil { + log.Warn("failed to read TCP data") + } + + helper.Backend.HandleTCPMessage(command, bytes) + case *datacommands.UDPProxyData: + bytes := make([]byte, command.DataLength) + _, err := io.ReadFull(helper.socket, bytes) + + if err != nil { + log.Warn("failed to read TCP data") + } + + helper.Backend.HandleUDPMessage(command, bytes) default: commandRaw, err := commonbackend.Unmarshal(helper.socket) diff --git a/backend/sshappbackend/remote-code/backendutil_custom/structure.go b/backend/sshappbackend/remote-code/backendutil_custom/structure.go index 96bcbf8..65c5a23 100644 --- a/backend/sshappbackend/remote-code/backendutil_custom/structure.go +++ b/backend/sshappbackend/remote-code/backendutil_custom/structure.go @@ -1,6 +1,8 @@ package backendutil_custom import ( + "net" + "git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" ) @@ -14,9 +16,11 @@ type BackendInterface interface { GetAllProxies() []uint16 ResolveProxy(proxyID uint16) *datacommands.ProxyInformationResponse GetAllClientConnections(proxyID uint16) []uint16 - ResolveConnection(connectionID uint16) *datacommands.ProxyConnectionsResponse + ResolveConnection(proxyID, connectionID uint16) *datacommands.ProxyConnectionInformationResponse CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse + OnTCPConnectionClosed(proxyID, connectionID uint16) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) + OnSocketConnection(sock net.Conn) } diff --git a/backend/sshappbackend/remote-code/main.go b/backend/sshappbackend/remote-code/main.go index 0fd11ee..a1d9bb7 100644 --- a/backend/sshappbackend/remote-code/main.go +++ b/backend/sshappbackend/remote-code/main.go @@ -1,7 +1,12 @@ package main import ( + "errors" + "fmt" + "net" "os" + "strconv" + "strings" "sync" "git.terah.dev/imterah/hermes/backend/commonbackend" @@ -11,19 +16,27 @@ import ( ) type TCPProxy struct { - proxyIDIndex uint16 - proxyIDLock sync.Mutex -} - -type UDPProxy struct { -} - -type SSHRemoteAppBackend struct { connectionIDIndex uint16 connectionIDLock sync.Mutex + proxyInformation *commonbackend.AddProxy + connections map[uint16]net.Conn + server net.Listener +} + +type UDPProxy struct { + server *net.UDPConn + proxyInformation *commonbackend.AddProxy +} + +type SSHRemoteAppBackend struct { + proxyIDIndex uint16 + proxyIDLock sync.Mutex + tcpProxies map[uint16]*TCPProxy udpProxies map[uint16]*UDPProxy + + sock net.Conn } func (backend *SSHRemoteAppBackend) StartBackend(byte []byte) (bool, error) { @@ -34,6 +47,20 @@ func (backend *SSHRemoteAppBackend) StartBackend(byte []byte) (bool, error) { } func (backend *SSHRemoteAppBackend) StopBackend() (bool, error) { + for tcpProxyIndex, tcpProxy := range backend.tcpProxies { + for _, tcpConnection := range tcpProxy.connections { + tcpConnection.Close() + } + + tcpProxy.server.Close() + delete(backend.tcpProxies, tcpProxyIndex) + } + + for udpProxyIndex, udpProxy := range backend.udpProxies { + udpProxy.server.Close() + delete(backend.udpProxies, udpProxyIndex) + } + return true, nil } @@ -42,49 +69,353 @@ func (backend *SSHRemoteAppBackend) GetBackendStatus() (bool, error) { } func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) { - return 0, true, nil + // Allocate a new proxy ID + backend.proxyIDLock.Lock() + proxyID := backend.proxyIDIndex + backend.proxyIDIndex++ + backend.proxyIDLock.Unlock() + + if command.Protocol == "tcp" { + backend.tcpProxies[proxyID] = &TCPProxy{ + connections: map[uint16]net.Conn{}, + proxyInformation: command, + } + + server, err := net.Listen("tcp", fmt.Sprintf(":%d", command.DestPort)) + + if err != nil { + return 0, false, fmt.Errorf("failed to open server: %s", err.Error()) + } + + backend.tcpProxies[proxyID].server = server + + go func() { + for { + conn, err := server.Accept() + + if err != nil { + log.Warnf("failed to accept connection: %s", err.Error()) + return + } + + go func() { + backend.tcpProxies[proxyID].connectionIDLock.Lock() + connectionID := backend.tcpProxies[proxyID].connectionIDIndex + backend.tcpProxies[proxyID].connectionIDIndex++ + backend.tcpProxies[proxyID].connectionIDLock.Unlock() + + dataBuf := make([]byte, 65535) + + onConnection := &datacommands.TCPConnectionOpened{ + ProxyID: proxyID, + ConnectionID: connectionID, + } + + connectionCommandMarshalled, err := datacommands.Marshal(onConnection) + + if err != nil { + log.Errorf("failed to marshal connection message: %s", err.Error()) + } + + backend.sock.Write(connectionCommandMarshalled) + + tcpData := &datacommands.TCPProxyData{ + ProxyID: proxyID, + ConnectionID: connectionID, + } + + for { + len, err := conn.Read(dataBuf) + + if err != nil { + if errors.Is(err, net.ErrClosed) { + return + } else if err.Error() != "EOF" { + log.Warnf("failed to read from sock: %s", err.Error()) + } + + conn.Close() + break + } + + tcpData.DataLength = uint16(len) + marshalledMessageCommand, err := datacommands.Marshal(tcpData) + + if err != nil { + log.Warnf("failed to marshal message data: %s", err.Error()) + + conn.Close() + break + } + + if _, err := backend.sock.Write(marshalledMessageCommand); err != nil { + log.Warnf("failed to send marshalled message data: %s", err.Error()) + + conn.Close() + break + } + + if _, err := backend.sock.Write(dataBuf[:len]); err != nil { + log.Warnf("failed to send raw message data: %s", err.Error()) + + conn.Close() + break + } + } + + onDisconnect := &datacommands.TCPConnectionClosed{ + ProxyID: proxyID, + ConnectionID: connectionID, + } + + disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect) + + if err != nil { + log.Errorf("failed to marshal disconnection message: %s", err.Error()) + } + + backend.sock.Write(disconnectionCommandMarshalled) + }() + } + }() + } else if command.Protocol == "udp" { + backend.udpProxies[proxyID] = &UDPProxy{ + proxyInformation: command, + } + + server, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: net.IPv4(0, 0, 0, 0), + Port: int(command.DestPort), + }) + + if err != nil { + return 0, false, fmt.Errorf("failed to open server: %s", err.Error()) + } + + backend.udpProxies[proxyID].server = server + dataBuf := make([]byte, 65535) + + udpProxyData := &datacommands.UDPProxyData{ + ProxyID: proxyID, + } + + go func() { + for { + len, addr, err := server.ReadFromUDP(dataBuf) + + if err != nil { + log.Warnf("failed to read from UDP socket: %s", err.Error()) + continue + } + + udpProxyData.ClientIP = addr.IP.String() + udpProxyData.ClientPort = uint16(addr.Port) + udpProxyData.DataLength = uint16(len) + + marshalledMessageCommand, err := datacommands.Marshal(udpProxyData) + + if err != nil { + log.Warnf("failed to marshal message data: %s", err.Error()) + continue + } + + if _, err := backend.sock.Write(marshalledMessageCommand); err != nil { + log.Warnf("failed to send marshalled message data: %s", err.Error()) + continue + } + + if _, err := backend.sock.Write(dataBuf[:len]); err != nil { + log.Warnf("failed to send raw message data: %s", err.Error()) + continue + } + } + }() + } + + return proxyID, true, nil } func (backend *SSHRemoteAppBackend) StopProxy(command *datacommands.RemoveProxy) (bool, error) { + tcpProxy, ok := backend.tcpProxies[command.ProxyID] + + if !ok { + udpProxy, ok := backend.udpProxies[command.ProxyID] + + if !ok { + return ok, fmt.Errorf("could not find proxy") + } + + udpProxy.server.Close() + delete(backend.udpProxies, command.ProxyID) + } else { + for _, tcpConnection := range tcpProxy.connections { + tcpConnection.Close() + } + + tcpProxy.server.Close() + delete(backend.tcpProxies, command.ProxyID) + } + return true, nil } func (backend *SSHRemoteAppBackend) GetAllProxies() []uint16 { - return []uint16{} + proxyList := make([]uint16, len(backend.tcpProxies)+len(backend.udpProxies)) + + currentPos := 0 + + for tcpProxy := range backend.tcpProxies { + proxyList[currentPos] = tcpProxy + currentPos += 1 + } + + for udpProxy := range backend.udpProxies { + proxyList[currentPos] = udpProxy + currentPos += 1 + } + + return proxyList } func (backend *SSHRemoteAppBackend) ResolveProxy(proxyID uint16) *datacommands.ProxyInformationResponse { - return &datacommands.ProxyInformationResponse{} + var proxyInformation *commonbackend.AddProxy + response := &datacommands.ProxyInformationResponse{} + + tcpProxy, ok := backend.tcpProxies[proxyID] + + if !ok { + udpProxy, ok := backend.udpProxies[proxyID] + + if !ok { + response.Exists = false + return response + } + + proxyInformation = udpProxy.proxyInformation + } else { + proxyInformation = tcpProxy.proxyInformation + } + + response.Exists = true + response.SourceIP = proxyInformation.SourceIP + response.SourcePort = proxyInformation.SourcePort + response.DestPort = proxyInformation.DestPort + response.Protocol = proxyInformation.Protocol + + return response } func (backend *SSHRemoteAppBackend) GetAllClientConnections(proxyID uint16) []uint16 { - return []uint16{} + tcpProxy, ok := backend.tcpProxies[proxyID] + + if !ok { + return []uint16{} + } + + connectionsArray := make([]uint16, len(tcpProxy.connections)) + currentPos := 0 + + for connectionIndex := range tcpProxy.connections { + connectionsArray[currentPos] = connectionIndex + currentPos++ + } + + return connectionsArray } -func (backend *SSHRemoteAppBackend) ResolveConnection(proxyID uint16) *datacommands.ProxyConnectionsResponse { - return &datacommands.ProxyConnectionsResponse{} +func (backend *SSHRemoteAppBackend) ResolveConnection(proxyID, connectionID uint16) *datacommands.ProxyConnectionInformationResponse { + response := &datacommands.ProxyConnectionInformationResponse{} + tcpProxy, ok := backend.tcpProxies[proxyID] + + if !ok { + response.Exists = false + return response + } + + connection, ok := tcpProxy.connections[connectionID] + + if !ok { + response.Exists = false + return response + } + + addr := connection.RemoteAddr().String() + ip := addr[:strings.LastIndex(addr, ":")] + port, err := strconv.Atoi(addr[strings.LastIndex(addr, ":")+1:]) + + if err != nil { + log.Warnf("failed to parse client port: %s", err.Error()) + response.Exists = false + + return response + } + + response.ClientIP = ip + response.ClientPort = uint16(port) + + return response } func (backend *SSHRemoteAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { return &commonbackend.CheckParametersResponse{ IsValid: true, - Message: "Valid!", } } func (backend *SSHRemoteAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { return &commonbackend.CheckParametersResponse{ IsValid: true, - Message: "Valid!", } } func (backend *SSHRemoteAppBackend) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) { + tcpProxy, ok := backend.tcpProxies[message.ProxyID] + if !ok { + return + } + + connection, ok := tcpProxy.connections[message.ConnectionID] + + if !ok { + return + } + + connection.Write(data) } func (backend *SSHRemoteAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) { + udpProxy, ok := backend.udpProxies[message.ProxyID] + if !ok { + return + } + + udpProxy.server.WriteToUDP(data, &net.UDPAddr{ + IP: net.ParseIP(message.ClientIP), + Port: int(message.ClientPort), + }) +} + +func (backend *SSHRemoteAppBackend) OnTCPConnectionClosed(proxyID, connectionID uint16) { + tcpProxy, ok := backend.tcpProxies[proxyID] + + if !ok { + return + } + + connection, ok := tcpProxy.connections[connectionID] + + if !ok { + return + } + + connection.Close() + delete(tcpProxy.connections, connectionID) +} + +func (backend *SSHRemoteAppBackend) OnSocketConnection(sock net.Conn) { + backend.sock = sock } func main() { From 15176831e6588da81e3aed667e07a697023f4e7b Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Feb 2025 13:15:09 -0500 Subject: [PATCH 32/45] feature: Adds basic TCP support for SSHAppBackend. --- backend/backendutil/application.go | 16 +- .../sshappbackend/gaslighter/gaslighter.go | 30 ++ backend/sshappbackend/local-code/main.go | 435 +++++++++++++++++- .../backendutil_custom/application.go | 302 ++++++------ backend/sshappbackend/remote-code/main.go | 11 +- 5 files changed, 622 insertions(+), 172 deletions(-) create mode 100644 backend/sshappbackend/gaslighter/gaslighter.go diff --git a/backend/backendutil/application.go b/backend/backendutil/application.go index 7f134a2..afa3147 100644 --- a/backend/backendutil/application.go +++ b/backend/backendutil/application.go @@ -132,12 +132,12 @@ func (helper *BackendApplicationHelper) Start() error { ok, err := helper.Backend.StartProxy(command) var hasAnyFailed bool - if !ok { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { + if err != nil { log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) hasAnyFailed = true + } else if !ok { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true } response := &commonbackend.ProxyStatusResponse{ @@ -160,12 +160,12 @@ func (helper *BackendApplicationHelper) Start() error { ok, err := helper.Backend.StopProxy(command) var hasAnyFailed bool - if !ok { - log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { + if err != nil { log.Warnf("failed to remove proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) hasAnyFailed = true + } else if !ok { + log.Warnf("failed to remove proxy (%s:%d -> remote:%d): RemoveProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true } response := &commonbackend.ProxyStatusResponse{ diff --git a/backend/sshappbackend/gaslighter/gaslighter.go b/backend/sshappbackend/gaslighter/gaslighter.go new file mode 100644 index 0000000..ecccec7 --- /dev/null +++ b/backend/sshappbackend/gaslighter/gaslighter.go @@ -0,0 +1,30 @@ +package gaslighter + +import "io" + +type Gaslighter struct { + Byte byte + HasGaslit bool + ProxiedReader io.Reader +} + +func (gaslighter *Gaslighter) Read(p []byte) (n int, err error) { + if gaslighter.HasGaslit { + return gaslighter.ProxiedReader.Read(p) + } + + if len(p) == 0 { + return 0, nil + } + + p[0] = gaslighter.Byte + gaslighter.HasGaslit = true + + if len(p) > 1 { + n, err := gaslighter.ProxiedReader.Read(p[1:]) + + return n + 1, err + } else { + return 1, nil + } +} diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go index 98a2370..75044b2 100644 --- a/backend/sshappbackend/local-code/main.go +++ b/backend/sshappbackend/local-code/main.go @@ -5,19 +5,35 @@ import ( "crypto/md5" "encoding/hex" "encoding/json" + "errors" "fmt" + "io" + "math/rand/v2" + "net" "os" "strings" "sync" + "time" "git.terah.dev/imterah/hermes/backend/backendutil" "git.terah.dev/imterah/hermes/backend/commonbackend" + "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" + "git.terah.dev/imterah/hermes/backend/sshappbackend/gaslighter" "github.com/charmbracelet/log" "github.com/go-playground/validator/v10" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) +type TCPProxy struct { + proxyInformation *commonbackend.AddProxy + connections map[uint16]net.Conn +} + +type UDPProxy struct { + proxyInformation *commonbackend.AddProxy +} + type SSHAppBackendData struct { IP string `json:"ip" validate:"required"` Port uint16 `json:"port" validate:"required"` @@ -27,14 +43,27 @@ type SSHAppBackendData struct { } type SSHAppBackend struct { - config *SSHAppBackendData - conn *ssh.Client - clients []*commonbackend.ProxyClientConnection - arrayPropMutex sync.Mutex + config *SSHAppBackendData + conn *ssh.Client + listener net.Listener + currentSock net.Conn + + tcpProxies map[uint16]*TCPProxy + udpProxies map[uint16]*UDPProxy + + // globalNonCriticalMessageLock: Locks all messages that don't need low-latency transmissions & high + // speed behind a lock. This ensures safety when it comes to handling messages correctly. + globalNonCriticalMessageLock sync.Mutex + // globalNonCriticalMessageChan: Channel for handling messages that need a reply / aren't critical. + globalNonCriticalMessageChan chan interface{} } func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { log.Info("SSHAppBackend is initializing...") + backend.globalNonCriticalMessageChan = make(chan interface{}) + backend.tcpProxies = map[uint16]*TCPProxy{} + backend.udpProxies = map[uint16]*UDPProxy{} + var backendData SSHAppBackendData if err := json.Unmarshal(configBytes, &backendData); err != nil { @@ -77,8 +106,8 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { backend.conn = conn - log.Info("SSHAppBackend has connected successfully.") - log.Info("Getting CPU architecture...") + log.Debug("SSHAppBackend has connected successfully.") + log.Debug("Getting CPU architecture...") session, err := backend.conn.NewSession() @@ -125,7 +154,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, fmt.Errorf("CPU architecture not compiled/supported currently") } - log.Info("Checking if we need to copy the application...") + log.Debug("Checking if we need to copy the application...") var binary []byte needsToCopyBinary := true @@ -187,7 +216,8 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { } if needsToCopyBinary { - log.Info("Copying binary...") + log.Debug("Copying binary...") + sftpInstance, err := sftp.NewClient(conn) if err != nil { @@ -213,13 +243,13 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { var file *sftp.File if fileExists { - file, err = sftpInstance.Create("/tmp/sshappbackend.runtime") - } else { file, err = sftpInstance.OpenFile("/tmp/sshappbackend.runtime", os.O_WRONLY) + } else { + file, err = sftpInstance.Create("/tmp/sshappbackend.runtime") } if err != nil { - log.Warnf("Failed to create file: %s", err.Error()) + log.Warnf("Failed to create (or open) file: %s", err.Error()) conn.Close() backend.conn = nil return false, err @@ -234,7 +264,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } - err = file.Chmod(775) + err = file.Chmod(0755) if err != nil { log.Warnf("Failed to change permissions on file: %s", err.Error()) @@ -243,12 +273,25 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } - log.Info("Done copying file.") + log.Debug("Done copying file.") + sftpInstance.Close() } else { - log.Info("Skipping copying as there's a copy on disk already.") + log.Debug("Skipping copying as there's a copy on disk already.") } - log.Info("Starting process...") + log.Debug("Initializing Unix socket...") + + socketPath := fmt.Sprintf("/tmp/sock-%d.sock", rand.Uint()) + listener, err := conn.ListenUnix(socketPath) + + if err != nil { + log.Warnf("Failed to listen on socket: %s", err.Error()) + conn.Close() + backend.conn = nil + return false, err + } + + log.Debug("Starting process...") session, err = backend.conn.NewSession() @@ -259,10 +302,56 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } + backend.listener = listener + session.Stdout = WriteLogger{} session.Stderr = WriteLogger{} - go session.Run("/tmp/sshappbackend.runtime") + go func() { + for { + err := session.Run(fmt.Sprintf("HERMES_LOG_LEVEL=\"%s\" HERMES_API_SOCK=\"%s\" /tmp/sshappbackend.runtime", os.Getenv("HERMES_LOG_LEVEL"), socketPath)) + + if err != nil && !errors.Is(err, &ssh.ExitError{}) && !errors.Is(err, &ssh.ExitMissingError{}) { + log.Errorf("Critically failed during execution of remote code: %s", err.Error()) + return + } else { + log.Warn("Remote code failed for an unknown reason. Restarting...") + } + } + }() + + go backend.sockServerHandler() + + log.Debug("Started process. Waiting for Unix socket connection...") + + for backend.currentSock == nil { + time.Sleep(10 * time.Millisecond) + } + + log.Debug("Detected connection. Sending initialization command...") + + proxyStatusRaw, err := backend.SendNonCriticalMessage(&commonbackend.Start{ + Arguments: []byte{}, + }) + + if err != nil { + return false, err + } + + proxyStatus, ok := proxyStatusRaw.(*commonbackend.BackendStatusResponse) + + if !ok { + return false, fmt.Errorf("recieved invalid response type: %T", proxyStatusRaw) + } + + if proxyStatus.StatusCode == commonbackend.StatusFailure { + if proxyStatus.Message == "" { + return false, fmt.Errorf("failed to initialize backend in remote code") + } else { + return false, fmt.Errorf("failed to initialize backend in remote code: %s", proxyStatus.Message) + } + } + log.Info("SSHAppBackend has initialized successfully.") return true, nil @@ -283,15 +372,117 @@ func (backend *SSHAppBackend) GetBackendStatus() (bool, error) { } func (backend *SSHAppBackend) StartProxy(command *commonbackend.AddProxy) (bool, error) { + proxyStatusRaw, err := backend.SendNonCriticalMessage(command) + + if err != nil { + return false, err + } + + proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse) + + if !ok { + return false, fmt.Errorf("recieved invalid response type: %T", proxyStatusRaw) + } + + if !proxyStatus.IsActive { + return false, fmt.Errorf("failed to initialize proxy in remote code") + } + + if command.Protocol == "tcp" { + backend.tcpProxies[proxyStatus.ProxyID] = &TCPProxy{ + proxyInformation: command, + } + + backend.tcpProxies[proxyStatus.ProxyID].connections = map[uint16]net.Conn{} + } else if command.Protocol == "udp" { + backend.udpProxies[proxyStatus.ProxyID] = &UDPProxy{ + proxyInformation: command, + } + } + return true, nil } func (backend *SSHAppBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, error) { + if command.Protocol == "tcp" { + for proxyIndex, proxy := range backend.tcpProxies { + if proxy.proxyInformation.DestPort != command.DestPort { + continue + } + + onDisconnect := &datacommands.TCPConnectionClosed{ + ProxyID: proxyIndex, + } + + for connectionIndex, connection := range proxy.connections { + connection.Close() + delete(proxy.connections, connectionIndex) + + onDisconnect.ConnectionID = connectionIndex + disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect) + + if err != nil { + log.Errorf("failed to marshal disconnection message: %s", err.Error()) + } + + backend.currentSock.Write(disconnectionCommandMarshalled) + } + + proxyStatusRaw, err := backend.SendNonCriticalMessage(&datacommands.RemoveProxy{ + ProxyID: proxyIndex, + }) + + if err != nil { + return false, err + } + + proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse) + + if !ok { + log.Warn("Failed to stop proxy: typecast failed") + return true, fmt.Errorf("failed to stop proxy: typecast failed") + } + + if proxyStatus.IsActive { + log.Warn("Failed to stop proxy: still running") + return true, fmt.Errorf("failed to stop proxy: still running") + } + } + } else if command.Protocol == "udp" { + for proxyIndex, proxy := range backend.udpProxies { + if proxy.proxyInformation.DestPort != command.DestPort { + continue + } + + proxyStatusRaw, err := backend.SendNonCriticalMessage(&datacommands.RemoveProxy{ + ProxyID: proxyIndex, + }) + + if err != nil { + return false, err + } + + proxyStatus, ok := proxyStatusRaw.(*datacommands.ProxyStatusResponse) + + if !ok { + log.Warn("Failed to stop proxy: typecast failed") + return true, fmt.Errorf("failed to stop proxy: typecast failed") + } + + if proxyStatus.IsActive { + log.Warn("Failed to stop proxy: still running") + return true, fmt.Errorf("failed to stop proxy: still running") + } + + // TODO: finish code for UDP + } + } + return false, fmt.Errorf("could not find the proxy") } func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection { - return backend.clients + return []*commonbackend.ProxyClientConnection{} } func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { @@ -322,6 +513,216 @@ func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commo } } +func (backend *SSHAppBackend) OnTCPConnectionOpened(proxyID, connectionID uint16) { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", backend.tcpProxies[proxyID].proxyInformation.SourceIP, backend.tcpProxies[proxyID].proxyInformation.SourcePort)) + + if err != nil { + log.Warnf("failed to dial sock: %s", err.Error()) + } + + go func() { + dataBuf := make([]byte, 65535) + + tcpData := &datacommands.TCPProxyData{ + ProxyID: proxyID, + ConnectionID: connectionID, + } + + for { + len, err := conn.Read(dataBuf) + + if err != nil { + if errors.Is(err, net.ErrClosed) { + return + } else if err.Error() != "EOF" { + log.Warnf("failed to read from sock: %s", err.Error()) + } + + conn.Close() + break + } + + tcpData.DataLength = uint16(len) + marshalledMessageCommand, err := datacommands.Marshal(tcpData) + + if err != nil { + log.Warnf("failed to marshal message data: %s", err.Error()) + + conn.Close() + break + } + + if _, err := backend.currentSock.Write(marshalledMessageCommand); err != nil { + log.Warnf("failed to send marshalled message data: %s", err.Error()) + + conn.Close() + break + } + + if _, err := backend.currentSock.Write(dataBuf[:len]); err != nil { + log.Warnf("failed to send raw message data: %s", err.Error()) + + conn.Close() + break + } + } + + onDisconnect := &datacommands.TCPConnectionClosed{ + ProxyID: proxyID, + ConnectionID: connectionID, + } + + disconnectionCommandMarshalled, err := datacommands.Marshal(onDisconnect) + + if err != nil { + log.Errorf("failed to marshal disconnection message: %s", err.Error()) + } + + backend.currentSock.Write(disconnectionCommandMarshalled) + }() + + backend.tcpProxies[proxyID].connections[connectionID] = conn +} + +func (backend *SSHAppBackend) OnTCPConnectionClosed(proxyID, connectionID uint16) { + proxy, ok := backend.tcpProxies[proxyID] + + if !ok { + log.Warn("Could not find TCP proxy") + } + + connection, ok := proxy.connections[connectionID] + + if !ok { + log.Warn("Could not find connection in TCP proxy") + } + + connection.Close() + delete(proxy.connections, connectionID) +} + +func (backend *SSHAppBackend) HandleTCPMessage(message *datacommands.TCPProxyData, data []byte) { + proxy, ok := backend.tcpProxies[message.ProxyID] + + if !ok { + log.Warn("Could not find TCP proxy") + } + + connection, ok := proxy.connections[message.ConnectionID] + + if !ok { + log.Warn("Could not find connection in TCP proxy") + } + + connection.Write(data) +} + +func (backend *SSHAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) {} + +func (backend *SSHAppBackend) SendNonCriticalMessage(iface interface{}) (interface{}, error) { + if backend.currentSock == nil { + return nil, fmt.Errorf("socket connection not initialized yet") + } + + bytes, err := datacommands.Marshal(iface) + + if err != nil && err.Error() == "unsupported command type" { + bytes, err = commonbackend.Marshal(iface) + + if err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } + + backend.globalNonCriticalMessageLock.Lock() + + if _, err := backend.currentSock.Write(bytes); err != nil { + backend.globalNonCriticalMessageLock.Unlock() + return nil, fmt.Errorf("failed to write message: %s", err.Error()) + } + + reply, ok := <-backend.globalNonCriticalMessageChan + + if !ok { + backend.globalNonCriticalMessageLock.Unlock() + return nil, fmt.Errorf("failed to get reply back: chan not OK") + } + + backend.globalNonCriticalMessageLock.Unlock() + return reply, nil +} + +func (backend *SSHAppBackend) sockServerHandler() { + for { + conn, err := backend.listener.Accept() + + if err != nil { + log.Warnf("Failed to accept remote connection: %s", err.Error()) + } + + log.Debug("Successfully connected.") + + backend.currentSock = conn + + commandID := make([]byte, 1) + + gaslighter := &gaslighter.Gaslighter{} + gaslighter.ProxiedReader = conn + + dataBuffer := make([]byte, 65535) + + var commandRaw interface{} + + for { + if _, err := conn.Read(commandID); err != nil { + log.Warnf("Failed to read command ID: %s", err.Error()) + return + } + + gaslighter.Byte = commandID[0] + gaslighter.HasGaslit = false + + if gaslighter.Byte > 100 { + commandRaw, err = datacommands.Unmarshal(gaslighter) + } else { + commandRaw, err = commonbackend.Unmarshal(gaslighter) + } + + if err != nil { + log.Warnf("Failed to parse command: %s", err.Error()) + } + + switch command := commandRaw.(type) { + case *datacommands.TCPConnectionOpened: + backend.OnTCPConnectionOpened(command.ProxyID, command.ConnectionID) + case *datacommands.TCPConnectionClosed: + backend.OnTCPConnectionClosed(command.ProxyID, command.ConnectionID) + case *datacommands.TCPProxyData: + if _, err := io.ReadFull(conn, dataBuffer[:command.DataLength]); err != nil { + log.Warnf("Failed to read entire data buffer: %s", err.Error()) + break + } + + backend.HandleTCPMessage(command, dataBuffer[:command.DataLength]) + case *datacommands.UDPProxyData: + if _, err := io.ReadFull(conn, dataBuffer[:command.DataLength]); err != nil { + log.Warnf("Failed to read entire data buffer: %s", err.Error()) + break + } + + backend.HandleUDPMessage(command, dataBuffer[:command.DataLength]) + default: + select { + case backend.globalNonCriticalMessageChan <- command: + default: + } + } + } + } +} + func main() { logLevel := os.Getenv("HERMES_LOG_LEVEL") diff --git a/backend/sshappbackend/remote-code/backendutil_custom/application.go b/backend/sshappbackend/remote-code/backendutil_custom/application.go index 0e00da7..2747f28 100644 --- a/backend/sshappbackend/remote-code/backendutil_custom/application.go +++ b/backend/sshappbackend/remote-code/backendutil_custom/application.go @@ -8,6 +8,7 @@ import ( "git.terah.dev/imterah/hermes/backend/backendutil" "git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" + "git.terah.dev/imterah/hermes/backend/sshappbackend/gaslighter" "github.com/charmbracelet/log" ) @@ -38,10 +39,28 @@ func (helper *BackendApplicationHelper) Start() error { log.Debug("Sucessfully connected") - for { - commandRaw, err := datacommands.Unmarshal(helper.socket) + gaslighter := &gaslighter.Gaslighter{} + gaslighter.ProxiedReader = helper.socket - if err != nil && err.Error() != "couldn't match command ID" { + commandID := make([]byte, 1) + + for { + if _, err := helper.socket.Read(commandID); err != nil { + return err + } + + gaslighter.Byte = commandID[0] + gaslighter.HasGaslit = false + + var commandRaw interface{} + + if gaslighter.Byte > 100 { + commandRaw, err = datacommands.Unmarshal(gaslighter) + } else { + commandRaw, err = commonbackend.Unmarshal(gaslighter) + } + + if err != nil { return err } @@ -127,155 +146,146 @@ func (helper *BackendApplicationHelper) Start() error { } helper.Backend.HandleUDPMessage(command, bytes) - default: - commandRaw, err := commonbackend.Unmarshal(helper.socket) + case *commonbackend.Start: + ok, err := helper.Backend.StartBackend(command.Arguments) + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.Stop: + ok, err := helper.Backend.StopBackend() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + IsRunning: !ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.BackendStatusRequest: + ok, err := helper.Backend.GetBackendStatus() + + var ( + message string + statusCode int + ) + + if err != nil { + message = err.Error() + statusCode = commonbackend.StatusFailure + } else { + statusCode = commonbackend.StatusSuccess + } + + response := &commonbackend.BackendStatusResponse{ + IsRunning: ok, + StatusCode: statusCode, + Message: message, + } + + responseMarshalled, err := commonbackend.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.AddProxy: + id, ok, err := helper.Backend.StartProxy(command) + var hasAnyFailed bool + + if !ok { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) + hasAnyFailed = true + } else if err != nil { + log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) + hasAnyFailed = true + } + + response := &datacommands.ProxyStatusResponse{ + ProxyID: id, + IsActive: !hasAnyFailed, + } + + responseMarshalled, err := datacommands.Marshal(response) + + if err != nil { + log.Error("failed to marshal response: %s", err.Error()) + continue + } + + helper.socket.Write(responseMarshalled) + case *commonbackend.CheckClientParameters: + resp := helper.Backend.CheckParametersForConnections(command) + resp.InResponseTo = "checkClientParameters" + + byteData, err := commonbackend.Marshal(resp) if err != nil { return err } - switch command := commandRaw.(type) { - case *commonbackend.Start: - ok, err := helper.Backend.StartBackend(command.Arguments) - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.Stop: - ok, err := helper.Backend.StopBackend() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: !ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.BackendStatusRequest: - ok, err := helper.Backend.GetBackendStatus() - - var ( - message string - statusCode int - ) - - if err != nil { - message = err.Error() - statusCode = commonbackend.StatusFailure - } else { - statusCode = commonbackend.StatusSuccess - } - - response := &commonbackend.BackendStatusResponse{ - IsRunning: ok, - StatusCode: statusCode, - Message: message, - } - - responseMarshalled, err := commonbackend.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.AddProxy: - id, ok, err := helper.Backend.StartProxy(command) - var hasAnyFailed bool - - if !ok { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): StartProxy returned into failure state", command.SourceIP, command.SourcePort, command.DestPort) - hasAnyFailed = true - } else if err != nil { - log.Warnf("failed to add proxy (%s:%d -> remote:%d): %s", command.SourceIP, command.SourcePort, command.DestPort, err.Error()) - hasAnyFailed = true - } - - response := &datacommands.ProxyStatusResponse{ - ProxyID: id, - IsActive: !hasAnyFailed, - } - - responseMarshalled, err := datacommands.Marshal(response) - - if err != nil { - log.Error("failed to marshal response: %s", err.Error()) - continue - } - - helper.socket.Write(responseMarshalled) - case *commonbackend.CheckClientParameters: - resp := helper.Backend.CheckParametersForConnections(command) - resp.InResponseTo = "checkClientParameters" - - byteData, err := commonbackend.Marshal(resp) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - case *commonbackend.CheckServerParameters: - resp := helper.Backend.CheckParametersForBackend(command.Arguments) - resp.InResponseTo = "checkServerParameters" - - byteData, err := commonbackend.Marshal(resp) - - if err != nil { - return err - } - - if _, err = helper.socket.Write(byteData); err != nil { - return err - } - default: - log.Warnf("Unsupported command recieved: %T", command) + if _, err = helper.socket.Write(byteData); err != nil { + return err } + case *commonbackend.CheckServerParameters: + resp := helper.Backend.CheckParametersForBackend(command.Arguments) + resp.InResponseTo = "checkServerParameters" + + byteData, err := commonbackend.Marshal(resp) + + if err != nil { + return err + } + + if _, err = helper.socket.Write(byteData); err != nil { + return err + } + default: + log.Warnf("Unsupported command recieved: %T", command) } } } diff --git a/backend/sshappbackend/remote-code/main.go b/backend/sshappbackend/remote-code/main.go index a1d9bb7..d56a7a3 100644 --- a/backend/sshappbackend/remote-code/main.go +++ b/backend/sshappbackend/remote-code/main.go @@ -36,6 +36,8 @@ type SSHRemoteAppBackend struct { tcpProxies map[uint16]*TCPProxy udpProxies map[uint16]*UDPProxy + isRunning bool + sock net.Conn } @@ -43,6 +45,8 @@ func (backend *SSHRemoteAppBackend) StartBackend(byte []byte) (bool, error) { backend.tcpProxies = map[uint16]*TCPProxy{} backend.udpProxies = map[uint16]*UDPProxy{} + backend.isRunning = true + return true, nil } @@ -61,11 +65,12 @@ func (backend *SSHRemoteAppBackend) StopBackend() (bool, error) { delete(backend.udpProxies, udpProxyIndex) } + backend.isRunning = false return true, nil } func (backend *SSHRemoteAppBackend) GetBackendStatus() (bool, error) { - return true, nil + return backend.isRunning, nil } func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) (uint16, bool, error) { @@ -104,6 +109,8 @@ func (backend *SSHRemoteAppBackend) StartProxy(command *commonbackend.AddProxy) backend.tcpProxies[proxyID].connectionIDIndex++ backend.tcpProxies[proxyID].connectionIDLock.Unlock() + backend.tcpProxies[proxyID].connections[connectionID] = conn + dataBuf := make([]byte, 65535) onConnection := &datacommands.TCPConnectionOpened{ @@ -372,12 +379,14 @@ func (backend *SSHRemoteAppBackend) HandleTCPMessage(message *datacommands.TCPPr tcpProxy, ok := backend.tcpProxies[message.ProxyID] if !ok { + log.Warnf("could not find tcp proxy (ID %d)", message.ProxyID) return } connection, ok := tcpProxy.connections[message.ConnectionID] if !ok { + log.Warnf("could not find tcp proxy (ID %d) with connection ID (%d)", message.ProxyID, message.ConnectionID) return } From f8a4fe00a0126d75bb07490a1667d5e9a6a8e65d Mon Sep 17 00:00:00 2001 From: imterah Date: Wed, 19 Feb 2025 07:58:42 -0500 Subject: [PATCH 33/45] feature: Adds basic UDP support. --- backend/commonbackend/unmarshal.go | 10 +- backend/sshappbackend/local-code/main.go | 68 ++++++++++- .../local-code/porttranslation/translation.go | 112 ++++++++++++++++++ 3 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 backend/sshappbackend/local-code/porttranslation/translation.go diff --git a/backend/commonbackend/unmarshal.go b/backend/commonbackend/unmarshal.go index 8e338c6..6bb5af4 100644 --- a/backend/commonbackend/unmarshal.go +++ b/backend/commonbackend/unmarshal.go @@ -213,7 +213,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -270,7 +270,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -364,7 +364,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -523,7 +523,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") @@ -580,7 +580,7 @@ func Unmarshal(conn io.Reader) (interface{}, error) { if protocolBytes[0] == TCP { protocol = "tcp" - } else if protocolBytes[1] == UDP { + } else if protocolBytes[0] == UDP { protocol = "udp" } else { return nil, fmt.Errorf("invalid protocol") diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go index 75044b2..abb0081 100644 --- a/backend/sshappbackend/local-code/main.go +++ b/backend/sshappbackend/local-code/main.go @@ -19,6 +19,7 @@ import ( "git.terah.dev/imterah/hermes/backend/commonbackend" "git.terah.dev/imterah/hermes/backend/sshappbackend/datacommands" "git.terah.dev/imterah/hermes/backend/sshappbackend/gaslighter" + "git.terah.dev/imterah/hermes/backend/sshappbackend/local-code/porttranslation" "github.com/charmbracelet/log" "github.com/go-playground/validator/v10" "github.com/pkg/sftp" @@ -32,6 +33,7 @@ type TCPProxy struct { type UDPProxy struct { proxyInformation *commonbackend.AddProxy + portTranslation *porttranslation.PortTranslation } type SSHAppBackendData struct { @@ -397,7 +399,56 @@ func (backend *SSHAppBackend) StartProxy(command *commonbackend.AddProxy) (bool, } else if command.Protocol == "udp" { backend.udpProxies[proxyStatus.ProxyID] = &UDPProxy{ proxyInformation: command, + portTranslation: &porttranslation.PortTranslation{}, } + + backend.udpProxies[proxyStatus.ProxyID].portTranslation.UDPAddr = &net.UDPAddr{ + IP: net.ParseIP(command.SourceIP), + Port: int(command.SourcePort), + } + + udpMessageCommand := &datacommands.UDPProxyData{} + udpMessageCommand.ProxyID = proxyStatus.ProxyID + + backend.udpProxies[proxyStatus.ProxyID].portTranslation.WriteFrom = func(ip string, port uint16, data []byte) { + udpMessageCommand.ClientIP = ip + udpMessageCommand.ClientPort = port + udpMessageCommand.DataLength = uint16(len(data)) + + marshalledCommand, err := datacommands.Marshal(udpMessageCommand) + + if err != nil { + log.Warnf("Failed to marshal UDP message header") + return + } + + if _, err := backend.currentSock.Write(marshalledCommand); err != nil { + log.Warnf("Failed to write UDP message header") + return + } + + if _, err := backend.currentSock.Write(data); err != nil { + log.Warnf("Failed to write UDP message") + return + } + } + + go func() { + for { + time.Sleep(3 * time.Minute) + + // Checks if the proxy still exists before continuing + _, ok := backend.udpProxies[proxyStatus.ProxyID] + + if !ok { + return + } + + // Then attempt to run cleanup tasks + log.Debug("Running UDP proxy cleanup tasks (invoking CleanupPorts() on portTranslation)") + backend.udpProxies[proxyStatus.ProxyID].portTranslation.CleanupPorts() + } + }() } return true, nil @@ -474,17 +525,20 @@ func (backend *SSHAppBackend) StopProxy(command *commonbackend.RemoveProxy) (boo return true, fmt.Errorf("failed to stop proxy: still running") } - // TODO: finish code for UDP + proxy.portTranslation.StopAllPorts() + delete(backend.udpProxies, proxyIndex) } } return false, fmt.Errorf("could not find the proxy") } +// TODO: implement! func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection { return []*commonbackend.ProxyClientConnection{} } +// We don't have any parameter limitations, so we should be good. func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *commonbackend.CheckClientParameters) *commonbackend.CheckParametersResponse { return &commonbackend.CheckParametersResponse{ IsValid: true, @@ -617,7 +671,17 @@ func (backend *SSHAppBackend) HandleTCPMessage(message *datacommands.TCPProxyDat connection.Write(data) } -func (backend *SSHAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) {} +func (backend *SSHAppBackend) HandleUDPMessage(message *datacommands.UDPProxyData, data []byte) { + proxy, ok := backend.udpProxies[message.ProxyID] + + if !ok { + log.Warn("Could not find UDP proxy") + } + + if _, err := proxy.portTranslation.WriteTo(message.ClientIP, message.ClientPort, data); err != nil { + log.Warnf("Failed to write to UDP: %s", err.Error()) + } +} func (backend *SSHAppBackend) SendNonCriticalMessage(iface interface{}) (interface{}, error) { if backend.currentSock == nil { diff --git a/backend/sshappbackend/local-code/porttranslation/translation.go b/backend/sshappbackend/local-code/porttranslation/translation.go new file mode 100644 index 0000000..b8c0454 --- /dev/null +++ b/backend/sshappbackend/local-code/porttranslation/translation.go @@ -0,0 +1,112 @@ +package porttranslation + +import ( + "fmt" + "net" + "sync" + "time" +) + +type connectionData struct { + udpConn *net.UDPConn + buf []byte + hasBeenAliveFor time.Time +} + +type PortTranslation struct { + UDPAddr *net.UDPAddr + WriteFrom func(ip string, port uint16, data []byte) + + newConnectionLock sync.Mutex + connections map[string]map[uint16]*connectionData +} + +func (translation *PortTranslation) CleanupPorts() { + if translation.connections == nil { + translation.connections = map[string]map[uint16]*connectionData{} + return + } + + for connectionIPIndex, connectionPorts := range translation.connections { + anyAreAlive := false + + for connectionPortIndex, connectionData := range connectionPorts { + if time.Now().Before(connectionData.hasBeenAliveFor.Add(3 * time.Minute)) { + anyAreAlive = true + continue + } + + connectionData.udpConn.Close() + delete(connectionPorts, connectionPortIndex) + } + + if !anyAreAlive { + delete(translation.connections, connectionIPIndex) + } + } +} + +func (translation *PortTranslation) StopAllPorts() { + if translation.connections == nil { + return + } + + for connectionIPIndex, connectionPorts := range translation.connections { + for connectionPortIndex, connectionData := range connectionPorts { + connectionData.udpConn.Close() + delete(connectionPorts, connectionPortIndex) + } + + delete(translation.connections, connectionIPIndex) + } + + translation.connections = nil +} + +func (translation *PortTranslation) WriteTo(ip string, port uint16, data []byte) (int, error) { + if translation.connections == nil { + translation.connections = map[string]map[uint16]*connectionData{} + } + + connectionPortData, ok := translation.connections[ip] + + if !ok { + translation.connections[ip] = map[uint16]*connectionData{} + connectionPortData = translation.connections[ip] + } + + connectionStruct, ok := connectionPortData[port] + + if !ok { + connectionPortData[port] = &connectionData{} + connectionStruct = connectionPortData[port] + + udpConn, err := net.DialUDP("udp", nil, translation.UDPAddr) + + if err != nil { + return 0, fmt.Errorf("failed to initialize UDP socket: %s", err.Error()) + } + + connectionStruct.udpConn = udpConn + connectionStruct.buf = make([]byte, 65535) + + go func() { + for { + n, err := udpConn.Read(connectionStruct.buf) + + if err != nil { + udpConn.Close() + delete(connectionPortData, port) + + return + } + + connectionStruct.hasBeenAliveFor = time.Now() + translation.WriteFrom(ip, port, connectionStruct.buf[:n]) + } + }() + } + + connectionStruct.hasBeenAliveFor = time.Now() + return connectionStruct.udpConn.Write(data) +} From 34b605c1b14b9fa1223760924391a0c094e3bc78 Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 20 Feb 2025 09:11:28 -0500 Subject: [PATCH 34/45] feature: Adds API manifest definitions, and implement GetAllClientConnections() --- Dockerfile | 1 + backend/backends.dev.json | 4 +++ backend/backends.prod.json | 4 +++ backend/sshappbackend/local-code/main.go | 41 ++++++++++++++++++++++-- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 81710a7..ae9c525 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,5 @@ WORKDIR /app COPY --from=build /build/backend/backends.prod.json /app/backends.json COPY --from=build /build/backend/api/api /app/hermes COPY --from=build /build/backend/sshbackend/sshbackend /app/sshbackend +COPY --from=build /build/backend/sshappbackend/local-code/sshappbackend /app/sshappbackend ENTRYPOINT ["/app/hermes", "--backends-path", "/app/backends.json"] diff --git a/backend/backends.dev.json b/backend/backends.dev.json index 9ff52ca..f314c69 100644 --- a/backend/backends.dev.json +++ b/backend/backends.dev.json @@ -3,6 +3,10 @@ "name": "ssh", "path": "./sshbackend/sshbackend" }, + { + "name": "sshapp", + "path": "./sshappbackend/local-code/sshappbackend" + }, { "name": "dummy", "path": "./dummybackend/dummybackend" diff --git a/backend/backends.prod.json b/backend/backends.prod.json index 9a9a09e..0ccfedc 100644 --- a/backend/backends.prod.json +++ b/backend/backends.prod.json @@ -2,5 +2,9 @@ { "name": "ssh", "path": "./sshbackend" + }, + { + "name": "sshapp", + "path": "./sshappbackend" } ] diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go index abb0081..cb0c13d 100644 --- a/backend/sshappbackend/local-code/main.go +++ b/backend/sshappbackend/local-code/main.go @@ -533,9 +533,46 @@ func (backend *SSHAppBackend) StopProxy(command *commonbackend.RemoveProxy) (boo return false, fmt.Errorf("could not find the proxy") } -// TODO: implement! func (backend *SSHAppBackend) GetAllClientConnections() []*commonbackend.ProxyClientConnection { - return []*commonbackend.ProxyClientConnection{} + connections := []*commonbackend.ProxyClientConnection{} + informationRequest := &datacommands.ProxyConnectionInformationRequest{} + + for proxyID, tcpProxy := range backend.tcpProxies { + informationRequest.ProxyID = proxyID + + for connectionID := range tcpProxy.connections { + informationRequest.ConnectionID = connectionID + + proxyStatusRaw, err := backend.SendNonCriticalMessage(informationRequest) + + if err != nil { + log.Warnf("Failed to get connection information for Proxy ID: %d, Connection ID: %d: %s", proxyID, connectionID, err.Error()) + return connections + } + + connectionStatus, ok := proxyStatusRaw.(*datacommands.ProxyConnectionInformationResponse) + + if !ok { + log.Warn("Failed to get connection response: typecast failed") + return connections + } + + if !connectionStatus.Exists { + log.Warnf("Connection with proxy ID: %d, Connection ID: %d is reported to not exist!", proxyID, connectionID) + tcpProxy.connections[connectionID].Close() + } + + connections = append(connections, &commonbackend.ProxyClientConnection{ + SourceIP: tcpProxy.proxyInformation.SourceIP, + SourcePort: tcpProxy.proxyInformation.SourcePort, + DestPort: tcpProxy.proxyInformation.DestPort, + ClientIP: connectionStatus.ClientIP, + ClientPort: connectionStatus.ClientPort, + }) + } + } + + return connections } // We don't have any parameter limitations, so we should be good. From 959718163ebf8af98ec9ddffdef79a41c760dcfa Mon Sep 17 00:00:00 2001 From: imterah Date: Sun, 16 Mar 2025 21:34:20 -0400 Subject: [PATCH 35/45] fix: Fix disconnect handler not working in production --- backend/sshbackend/main.go | 110 +++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index baf1994..ffdf546 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "os" + "slices" "strconv" "strings" "sync" @@ -18,6 +19,32 @@ import ( "golang.org/x/crypto/ssh" ) +type ConnWithTimeout struct { + net.Conn + ReadTimeout time.Duration + WriteTimeout time.Duration +} + +func (c *ConnWithTimeout) Read(b []byte) (int, error) { + err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)) + + if err != nil { + return 0, err + } + + return c.Conn.Read(b) +} + +func (c *ConnWithTimeout) Write(b []byte) (int, error) { + err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) + + if err != nil { + return 0, err + } + + return c.Conn.Write(b) +} + type SSHListener struct { SourceIP string SourcePort uint16 @@ -76,16 +103,34 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { }, } - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config) + addr := fmt.Sprintf("%s:%d", backendData.IP, backendData.Port) + timeout := time.Duration(10 * time.Second) + + rawTCPConn, err := net.DialTimeout("tcp", addr, timeout) if err != nil { return false, err } - backend.conn = conn + connWithTimeout := &ConnWithTimeout{ + Conn: rawTCPConn, + ReadTimeout: timeout, + WriteTimeout: timeout, + } + + c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config) + + if err != nil { + return false, err + } + + client := ssh.NewClient(c, chans, reqs) + backend.conn = client + + go backend.backendDisconnectHandler() + go backend.backendKeepaliveHandler() log.Info("SSHBackend has initialized successfully.") - go backend.backendDisconnectHandler() return true, nil } @@ -203,8 +248,7 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er // Splice out the clientInstance by clientIndex // TODO: change approach. It works but it's a bit wonky imho - // I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively - backend.clients = append(backend.clients[:clientIndex], backend.clients[clientIndex+1:]...) + backend.clients = slices.Delete(backend.clients, clientIndex, clientIndex+1) return } } @@ -288,10 +332,8 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool, } // Splice out the proxy instance by proxyIndex - // TODO: change approach. It works but it's a bit wonky imho - // I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively - backend.proxies = append(backend.proxies[:proxyIndex], backend.proxies[proxyIndex+1:]...) + backend.proxies = slices.Delete(backend.proxies, proxyIndex, proxyIndex+1) return true, nil } } @@ -341,17 +383,31 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba } } -func (backend *SSHBackend) backendDisconnectHandler() { +func (backend *SSHBackend) backendKeepaliveHandler() { for { if backend.conn != nil { - err := backend.conn.Wait() + _, _, err := backend.conn.SendRequest("keepalive@openssh.com", true, nil) - if err == nil || err.Error() != "EOF" { - continue + if err != nil { + log.Warn("Keepalive message failed!") + return } } - log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...") + time.Sleep(5 * time.Second) + } +} + +func (backend *SSHBackend) backendDisconnectHandler() { + for { + if backend.conn != nil { + backend.conn.Wait() + backend.conn.Close() + + log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...") + } else { + log.Info("Retrying reconnection in 5 seconds...") + } time.Sleep(5 * time.Second) @@ -376,14 +432,34 @@ func (backend *SSHBackend) backendDisconnectHandler() { }, } - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port), config) + addr := fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port) + timeout := time.Duration(10 * time.Second) + + rawTCPConn, err := net.DialTimeout("tcp", addr, timeout) if err != nil { - log.Errorf("Failed to connect to the server: %s", err.Error()) - return + log.Errorf("Failed to establish connection to the server: %s", err.Error()) + continue } - backend.conn = conn + connWithTimeout := &ConnWithTimeout{ + Conn: rawTCPConn, + ReadTimeout: timeout, + WriteTimeout: timeout, + } + + c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config) + + if err != nil { + log.Errorf("Failed to create SSH client connection: %s", err.Error()) + rawTCPConn.Close() + continue + } + + client := ssh.NewClient(c, chans, reqs) + backend.conn = client + + go backend.backendKeepaliveHandler() log.Info("SSHBackend has reconnected successfully. Attempting to set up proxies again...") From 5c503f04218c6ca4be6beb778946aee57228d056 Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Mar 2025 20:27:40 -0400 Subject: [PATCH 36/45] fix: Make logging options more clear for the backend runtime's backend logs --- .prettierrc | 16 ---------------- backend/api/backendruntime/core.go | 4 ++-- backend/api/backendruntime/struct.go | 2 +- 3 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 57562cf..0000000 --- a/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "arrowParens": "avoid", - "bracketSpacing": true, - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxSingleQuote": false, - "printWidth": 80, - "proseWrap": "always", - "quoteProps": "as-needed", - "requirePragma": false, - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "all", - "useTabs": false -} \ No newline at end of file diff --git a/backend/api/backendruntime/core.go b/backend/api/backendruntime/core.go index e06a2a6..eac5934 100644 --- a/backend/api/backendruntime/core.go +++ b/backend/api/backendruntime/core.go @@ -6,10 +6,10 @@ var ( AvailableBackends []*Backend RunningBackends map[uint]*Runtime TempDir string - isDevelopmentMode bool + shouldLog bool ) func init() { RunningBackends = make(map[uint]*Runtime) - isDevelopmentMode = os.Getenv("HERMES_DEVELOPMENT_MODE") != "" + shouldLog = os.Getenv("HERMES_DEVELOPMENT_MODE") != "" || os.Getenv("HERMES_BACKEND_LOGGING_ENABLED") != "" || os.Getenv("HERMES_LOG_LEVEL") == "debug" } diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index 9d057ad..cd30182 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -42,7 +42,7 @@ type writeLogger struct { func (writer writeLogger) Write(p []byte) (n int, err error) { logSplit := strings.Split(string(p), "\n") - if isDevelopmentMode { + if shouldLog { for _, logLine := range logSplit { if logLine == "" { continue From 17b10c9b19b0c39de7b5249b819b8ff3fd65a232 Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Mar 2025 20:31:28 -0400 Subject: [PATCH 37/45] fix: Add system to detect duplicate running remote processes and kill them accordingly --- backend/sshbackend/main.go | 106 ++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 8 deletions(-) diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index ffdf546..a554fa3 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -53,24 +53,35 @@ type SSHListener struct { Listeners []net.Listener } +type SSHBackendData struct { + IP string `json:"ip" validate:"required"` + Port uint16 `json:"port" validate:"required"` + Username string `json:"username" validate:"required"` + PrivateKey string `json:"privateKey" validate:"required"` + DisablePIDCheck bool `json:"disablePIDCheck"` + ListenOnIPs []string `json:"listenOnIPs"` +} + type SSHBackend struct { config *SSHBackendData conn *ssh.Client clients []*commonbackend.ProxyClientConnection proxies []*SSHListener arrayPropMutex sync.Mutex -} - -type SSHBackendData struct { - IP string `json:"ip" validate:"required"` - Port uint16 `json:"port" validate:"required"` - Username string `json:"username" validate:"required"` - PrivateKey string `json:"privateKey" validate:"required"` - ListenOnIPs []string `json:"listenOnIPs"` + pid int + isReady bool + inReinitLoop bool } func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { log.Info("SSHBackend is initializing...") + + if backend.inReinitLoop { + for !backend.isReady { + time.Sleep(100 * time.Millisecond) + } + } + var backendData SSHBackendData if err := json.Unmarshal(bytes, &backendData); err != nil { @@ -127,6 +138,42 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { client := ssh.NewClient(c, chans, reqs) backend.conn = client + if !backendData.DisablePIDCheck { + if backend.pid != 0 { + session, err := client.NewSession() + + if err != nil { + return false, err + } + + err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid)) + + if err != nil { + log.Warnf("Failed to kill process: %s", err.Error()) + } + } + + session, err := client.NewSession() + + if err != nil { + return false, err + } + + // Get the parent PID of the shell so we can kill it if we disconnect + output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'") + + if err != nil { + return false, err + } + + // Strip the new line and convert to int + backend.pid, err = strconv.Atoi(string(output)[:len(output)-1]) + + if err != nil { + return false, err + } + } + go backend.backendDisconnectHandler() go backend.backendKeepaliveHandler() @@ -404,6 +451,9 @@ func (backend *SSHBackend) backendDisconnectHandler() { backend.conn.Wait() backend.conn.Close() + backend.isReady = false + backend.inReinitLoop = true + log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...") } else { log.Info("Retrying reconnection in 5 seconds...") @@ -459,6 +509,46 @@ func (backend *SSHBackend) backendDisconnectHandler() { client := ssh.NewClient(c, chans, reqs) backend.conn = client + if !backend.config.DisablePIDCheck { + if backend.pid != 0 { + session, err := client.NewSession() + + if err != nil { + log.Warnf("Failed to create SSH command session: %s", err.Error()) + return + } + + err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid)) + + if err != nil { + log.Warnf("Failed to kill process: %s", err.Error()) + } + } + + session, err := client.NewSession() + + if err != nil { + log.Warnf("Failed to create SSH command session: %s", err.Error()) + return + } + + // Get the parent PID of the shell so we can kill it if we disconnect + output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'") + + if err != nil { + log.Warnf("Failed to execute command to fetch PID: %s", err.Error()) + return + } + + // Strip the new line and convert to int + backend.pid, err = strconv.Atoi(string(output)[:len(output)-1]) + + if err != nil { + log.Warnf("Failed to parse PID: %s", err.Error()) + return + } + } + go backend.backendKeepaliveHandler() log.Info("SSHBackend has reconnected successfully. Attempting to set up proxies again...") From 7dee159d5f46f0e4baf2b915b45f3167882ecbab Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Mar 2025 20:33:59 -0400 Subject: [PATCH 38/45] chore: Change license name --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8914588..a085e23 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Greyson +Copyright (c) 2024, Tera Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: From 83f80af4055d242c033b1b8c87db1d6751873676 Mon Sep 17 00:00:00 2001 From: imterah Date: Wed, 19 Mar 2025 00:34:36 +0000 Subject: [PATCH 39/45] chore: Delete unmaintained CHANGELOG --- CHANGELOG.md | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index c1fa49f..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,49 +0,0 @@ -# Changelog - -## [v1.1.2](https://github.com/imterah/nextnet/tree/v1.1.2) (2024-09-29) - -## [v1.1.1](https://github.com/imterah/nextnet/tree/v1.1.1) (2024-09-29) - -## [v1.1.0](https://github.com/imterah/nextnet/tree/v1.1.0) (2024-09-22) - -**Fixed bugs:** - -- Desktop app fails to build on macOS w/ `nix-shell` [\#1](https://github.com/imterah/nextnet/issues/1) - -**Merged pull requests:** - -- chore\(deps\): bump find-my-way from 8.1.0 to 8.2.2 in /api [\#17](https://github.com/imterah/nextnet/pull/17) -- chore\(deps\): bump axios from 1.6.8 to 1.7.4 in /lom [\#16](https://github.com/imterah/nextnet/pull/16) -- chore\(deps\): bump micromatch from 4.0.5 to 4.0.8 in /lom [\#15](https://github.com/imterah/nextnet/pull/15) -- chore\(deps\): bump braces from 3.0.2 to 3.0.3 in /lom [\#13](https://github.com/imterah/nextnet/pull/13) -- chore\(deps-dev\): bump braces from 3.0.2 to 3.0.3 in /api [\#11](https://github.com/imterah/nextnet/pull/11) -- chore\(deps\): bump ws from 8.17.0 to 8.17.1 in /api [\#10](https://github.com/imterah/nextnet/pull/10) - -## [v1.0.1](https://github.com/imterah/nextnet/tree/v1.0.1) (2024-05-18) - -**Merged pull requests:** - -- Adds public key authentication [\#6](https://github.com/imterah/nextnet/pull/6) -- Add support for eslint [\#5](https://github.com/imterah/nextnet/pull/5) - -## [v1.0.0](https://github.com/imterah/nextnet/tree/v1.0.0) (2024-05-10) - -## [v0.1.1](https://github.com/imterah/nextnet/tree/v0.1.1) (2024-05-05) - -## [v0.1.0](https://github.com/imterah/nextnet/tree/v0.1.0) (2024-05-05) - -**Implemented enhancements:** - -- \(potentially\) Migrate nix shell to nix flake [\#2](https://github.com/imterah/nextnet/issues/2) - -**Closed issues:** - -- add precommit hooks [\#3](https://github.com/imterah/nextnet/issues/3) - -**Merged pull requests:** - -- Reimplements PassyFire as a possible backend [\#4](https://github.com/imterah/nextnet/pull/4) - - - -\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 1cefe64f8837509f891c8960279a5dfd6a4848f4 Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Mar 2025 20:36:00 -0400 Subject: [PATCH 40/45] chore: Remove Node.JS from the nix shell --- shell.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/shell.nix b/shell.nix index 17c5989..ea1aa8e 100644 --- a/shell.nix +++ b/shell.nix @@ -3,7 +3,6 @@ }: pkgs.mkShell { buildInputs = with pkgs; [ # api/ - nodejs go gopls ]; From 71d53990de34fb59eb18356e884e06556bb02d82 Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Mar 2025 20:51:01 -0400 Subject: [PATCH 41/45] chore: Fix sample code to remove the deprecated LOM, and add JWT secrets --- README.md | 2 +- docker-compose.yml | 18 +----------------- prod-docker.env | 3 +-- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 01d5885..323855d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ 1. Copy and change the default password (or username & db name too) from the template file `prod-docker.env`: ```bash - sed "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" prod-docker.env > .env + sed -e "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" -e "s/JWT_SECRET=hermes/JWT_SECRET=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" prod-docker.env > .env ``` 2. Build the docker stack: `docker compose --env-file .env up -d` diff --git a/docker-compose.yml b/docker-compose.yml index 7ac3334..a549035 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,27 +6,12 @@ services: environment: DATABASE_URL: postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB}?schema=nextnet HERMES_POSTGRES_DSN: postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB} + HERMES_JWT_SECRET: ${JWT_SECRET} HERMES_DATABASE_BACKEND: postgresql depends_on: - db ports: - 3000:3000 - - # WARN: The LOM is deprecated and likely broken currently. - # - # NOTE: For this to work correctly, the nextnet-api must be version > 0.1.1 - # or have a version with backported username support, incl. logins - lom: - image: ghcr.io/imterah/hermes-lom:latest - container_name: hermes-lom - restart: always - ports: - - 2222:2222 - depends_on: - - api - volumes: - - ssh_key_data:/app/keys - db: image: postgres:17.2 container_name: nextnet-postgres @@ -37,7 +22,6 @@ services: POSTGRES_USER: ${POSTGRES_USERNAME} volumes: - postgres_data:/var/lib/postgresql/data - volumes: postgres_data: ssh_key_data: diff --git a/prod-docker.env b/prod-docker.env index fe5da79..954c20f 100644 --- a/prod-docker.env +++ b/prod-docker.env @@ -1,5 +1,4 @@ -# These are default values, please change these! - POSTGRES_USERNAME=hermes POSTGRES_PASSWORD=hermes POSTGRES_DB=hermes +JWT_SECRET=hermes From d56a8eb7bf69af685a00113d030239e496d155bb Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 21 Mar 2025 12:59:51 -0400 Subject: [PATCH 42/45] feature: Change state management from global variables to object passing This restructures dbcore (now the db package) and jwtcore (now the jwt package) to use a single struct. There is now a state package, which contains a struct with the full application state. After this, instead of initializing the API routes directly in the main function, the state object gets passed, and the API routes get initialized with their accompanying code. One fix done to reduce memory usage and increase speed is that the validator object is now persistent across requests, instead of recreating it each time. This should speed things up slightly, and improve memory usage. One additional chore done is that the database models have been moved to be a seperate file from the DB initialization itself. --- backend/api/backup.go | 298 --------------- backend/api/controllers/v1/backends/create.go | 357 +++++++++--------- backend/api/controllers/v1/backends/lookup.go | 187 ++++----- backend/api/controllers/v1/backends/remove.go | 175 ++++----- .../api/controllers/v1/proxies/connections.go | 226 +++++------ backend/api/controllers/v1/proxies/create.go | 268 ++++++------- backend/api/controllers/v1/proxies/lookup.go | 249 ++++++------ backend/api/controllers/v1/proxies/remove.go | 218 ++++++----- backend/api/controllers/v1/proxies/start.go | 208 +++++----- backend/api/controllers/v1/proxies/stop.go | 194 +++++----- backend/api/controllers/v1/users/create.go | 251 ++++++------ backend/api/controllers/v1/users/login.go | 267 ++++++------- backend/api/controllers/v1/users/lookup.go | 169 ++++----- backend/api/controllers/v1/users/refresh.go | 185 ++++----- backend/api/controllers/v1/users/remove.go | 159 ++++---- backend/api/db/db.go | 77 ++++ backend/api/db/models.go | 66 ++++ backend/api/dbcore/db.go | 142 ------- backend/api/jwt/jwt.go | 107 ++++++ backend/api/jwtcore/jwt.go | 117 ------ backend/api/main.go | 114 +++--- backend/api/permissions/permission_nodes.go | 4 +- backend/api/state/state.go | 24 ++ 23 files changed, 1901 insertions(+), 2161 deletions(-) delete mode 100644 backend/api/backup.go create mode 100644 backend/api/db/db.go create mode 100644 backend/api/db/models.go delete mode 100644 backend/api/dbcore/db.go create mode 100644 backend/api/jwt/jwt.go delete mode 100644 backend/api/jwtcore/jwt.go create mode 100644 backend/api/state/state.go diff --git a/backend/api/backup.go b/backend/api/backup.go deleted file mode 100644 index d5a9b86..0000000 --- a/backend/api/backup.go +++ /dev/null @@ -1,298 +0,0 @@ -package main - -import ( - "compress/gzip" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "os" - "strings" - - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "github.com/charmbracelet/log" - "github.com/go-playground/validator/v10" - "github.com/urfave/cli/v2" - "gorm.io/gorm" -) - -// Data structures -type BackupBackend struct { - ID uint `json:"id" validate:"required"` - - Name string `json:"name" validate:"required"` - Description *string `json:"description"` - Backend string `json:"backend" validate:"required"` - BackendParameters string `json:"connectionDetails" validate:"required"` -} - -type BackupProxy struct { - ID uint `json:"id" validate:"required"` - BackendID uint `json:"destProviderID" validate:"required"` - - Name string `json:"name" validate:"required"` - Description *string `json:"description"` - Protocol string `json:"protocol" validate:"required"` - SourceIP string `json:"sourceIP" validate:"required"` - SourcePort uint16 `json:"sourcePort" validate:"required"` - DestinationPort uint16 `json:"destPort" validate:"required"` - AutoStart bool `json:"enabled" validate:"required"` -} - -type BackupPermission struct { - ID uint `json:"id" validate:"required"` - - PermissionNode string `json:"permission" validate:"required"` - HasPermission bool `json:"has" validate:"required"` - UserID uint `json:"userID" validate:"required"` -} - -type BackupUser struct { - ID uint `json:"id" validate:"required"` - - Email string `json:"email" validate:"required"` - Username *string `json:"username"` - Name string `json:"name" validate:"required"` - Password string `json:"password" validate:"required"` - IsBot *bool `json:"isRootServiceAccount"` - - Token *string `json:"rootToken" validate:"required"` -} - -type BackupData struct { - Backends []*BackupBackend `json:"destinationProviders" validate:"required"` - Proxies []*BackupProxy `json:"forwardRules" validate:"required"` - Permissions []*BackupPermission `json:"allPermissions" validate:"required"` - Users []*BackupUser `json:"users" validate:"required"` -} - -// From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text -// Strips all alphanumeric characters from a string -func stripAllAlphanumeric(s string) string { - var result strings.Builder - - for i := 0; i < len(s); i++ { - b := s[i] - if ('a' <= b && b <= 'z') || - ('A' <= b && b <= 'Z') || - ('0' <= b && b <= '9') { - result.WriteByte(b) - } else { - result.WriteByte('_') - } - } - - return result.String() -} - -func backupRestoreEntrypoint(cCtx *cli.Context) error { - log.Info("Decompressing backup...") - - backupFile, err := os.Open(cCtx.String("backup-path")) - - if err != nil { - return fmt.Errorf("failed to open backup: %s", err.Error()) - } - - reader, err := gzip.NewReader(backupFile) - - if err != nil { - return fmt.Errorf("failed to initialize Gzip (compression) reader: %s", err.Error()) - } - - backupDataBytes, err := io.ReadAll(reader) - - if err != nil { - return fmt.Errorf("failed to read backup contents: %s", err.Error()) - } - - log.Info("Decompressed backup. Cleaning up...") - - err = reader.Close() - - if err != nil { - return fmt.Errorf("failed to close Gzip reader: %s", err.Error()) - } - - err = backupFile.Close() - - if err != nil { - return fmt.Errorf("failed to close backup: %s", err.Error()) - } - - log.Info("Parsing backup into internal structures...") - - backupData := &BackupData{} - - err = json.Unmarshal(backupDataBytes, backupData) - - if err != nil { - return fmt.Errorf("failed to parse backup: %s", err.Error()) - } - - if err := validator.New().Struct(backupData); err != nil { - return fmt.Errorf("failed to validate backup: %s", err.Error()) - } - - log.Info("Initializing database and opening it...") - - err = dbcore.InitializeDatabase(&gorm.Config{}) - - if err != nil { - log.Fatalf("Failed to initialize database: %s", err) - } - - log.Info("Running database migrations...") - - if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil { - return fmt.Errorf("Failed to run database migrations: %s", err) - } - - log.Info("Restoring database...") - bestEffortOwnerUIDFromBackup := -1 - - log.Info("Attempting to find user to use as owner of resources...") - - for _, user := range backupData.Users { - foundUser := false - failedAdministrationCheck := false - - for _, permission := range backupData.Permissions { - if permission.UserID != user.ID { - continue - } - - foundUser = true - - if !strings.HasPrefix(permission.PermissionNode, "routes.") && permission.PermissionNode != "permissions.see" && !permission.HasPermission { - log.Infof("User with email '%s' and ID of '%d' failed administration check (lacks all permissions required). Attempting to find better user", user.Email, user.ID) - failedAdministrationCheck = true - - break - } - } - - if !foundUser { - log.Warnf("User with email '%s' and ID of '%d' lacks any permissions!", user.Email, user.ID) - continue - } - - if failedAdministrationCheck { - continue - } - - log.Infof("Using user with email '%s', and ID of '%d'", user.Email, user.ID) - bestEffortOwnerUIDFromBackup = int(user.ID) - - break - } - - if bestEffortOwnerUIDFromBackup == -1 { - log.Warnf("Could not find Administrative level user to use as the owner of resources. Using user with email '%s', and ID of '%d'", backupData.Users[0].Email, backupData.Users[0].ID) - bestEffortOwnerUIDFromBackup = int(backupData.Users[0].ID) - } - - var bestEffortOwnerUID uint - - for _, user := range backupData.Users { - log.Infof("Migrating user with email '%s' and ID of '%d'", user.Email, user.ID) - tokens := make([]dbcore.Token, 0) - permissions := make([]dbcore.Permission, 0) - - if user.Token != nil { - tokens = append(tokens, dbcore.Token{ - Token: *user.Token, - DisableExpiry: true, - CreationIPAddr: "127.0.0.1", // We don't know the creation IP address... - }) - } - - for _, permission := range backupData.Permissions { - if permission.UserID != user.ID { - continue - } - - permissions = append(permissions, dbcore.Permission{ - PermissionNode: permission.PermissionNode, - HasPermission: permission.HasPermission, - }) - } - - username := "" - - if user.Username == nil { - username = strings.ToLower(stripAllAlphanumeric(user.Email)) - log.Warnf("User with ID of '%d' doesn't have a username. Derived username from email is '%s' (email is '%s')", user.ID, username, user.Email) - } else { - username = *user.Username - } - - userDatabase := &dbcore.User{ - Email: user.Email, - Username: username, - Name: user.Name, - Password: base64.StdEncoding.EncodeToString([]byte(user.Password)), - IsBot: user.IsBot, - - Tokens: tokens, - Permissions: permissions, - } - - if err := dbcore.DB.Create(userDatabase).Error; err != nil { - log.Errorf("Failed to create user: %s", err.Error()) - continue - } - - if uint(bestEffortOwnerUIDFromBackup) == user.ID { - bestEffortOwnerUID = userDatabase.ID - } - } - - for _, backend := range backupData.Backends { - log.Infof("Migrating backend ID '%d' with name '%s'", backend.ID, backend.Name) - - backendDatabase := &dbcore.Backend{ - UserID: bestEffortOwnerUID, - Name: backend.Name, - Description: backend.Description, - Backend: backend.Backend, - BackendParameters: base64.StdEncoding.EncodeToString([]byte(backend.BackendParameters)), - } - - if err := dbcore.DB.Create(backendDatabase).Error; err != nil { - log.Errorf("Failed to create backend: %s", err.Error()) - continue - } - - log.Infof("Migrating proxies for backend ID '%d'", backend.ID) - - for _, proxy := range backupData.Proxies { - if proxy.BackendID != backend.ID { - continue - } - - log.Infof("Migrating proxy ID '%d' with name '%s'", proxy.ID, proxy.Name) - - proxyDatabase := &dbcore.Proxy{ - BackendID: backendDatabase.ID, - UserID: bestEffortOwnerUID, - - Name: proxy.Name, - Description: proxy.Description, - Protocol: proxy.Protocol, - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestinationPort: proxy.DestinationPort, - AutoStart: proxy.AutoStart, - } - - if err := dbcore.DB.Create(proxyDatabase).Error; err != nil { - log.Errorf("Failed to create proxy: %s", err.Error()) - } - } - } - - log.Info("Successfully upgraded to Hermes from NextNet.") - - return nil -} diff --git a/backend/api/controllers/v1/backends/create.go b/backend/api/controllers/v1/backends/create.go index 287f314..314dc3e 100644 --- a/backend/api/controllers/v1/backends/create.go +++ b/backend/api/controllers/v1/backends/create.go @@ -7,13 +7,12 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type BackendCreationRequest struct { @@ -24,131 +23,114 @@ type BackendCreationRequest struct { BackendParameters interface{} `json:"connectionDetails" validate:"required"` } -func CreateBackend(c *gin.Context) { - var req BackendCreationRequest +func SetupCreateBackend(state *state.State) { + state.Engine.POST("/api/v1/backends/create", func(c *gin.Context) { + var req BackendCreationRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := jwtcore.GetUserFromJWT(req.Token) + user, err := state.JWT.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "backends.add") { c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + "error": "Missing permissions", }) return } - } - if !permissions.UserHasPermission(user, "backends.add") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + var backendParameters []byte - return - } + switch parameters := req.BackendParameters.(type) { + case string: + backendParameters = []byte(parameters) + case map[string]interface{}: + backendParameters, err = json.Marshal(parameters) - var backendParameters []byte + if err != nil { + log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error()) - switch parameters := req.BackendParameters.(type) { - case string: - backendParameters = []byte(parameters) - case map[string]interface{}: - backendParameters, err = json.Marshal(parameters) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to prepare parameters", + }) + + return + } + default: + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Invalid type for connectionDetails (recieved %T)", parameters), + }) + + return + } + + var backendRuntimeFilePath string + + for _, runtime := range backendruntime.AvailableBackends { + if runtime.Name == req.Backend { + backendRuntimeFilePath = runtime.Path + } + } + + if backendRuntimeFilePath == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Unsupported backend recieved", + }) + + return + } + + backend := backendruntime.NewBackend(backendRuntimeFilePath) + err = backend.Start() if err != nil { - log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error()) + log.Warnf("Failed to start backend: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to prepare parameters", + "error": "Failed to start backend", }) return } - default: - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Invalid type for connectionDetails (recieved %T)", parameters), + + backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{ + Arguments: backendParameters, }) - return - } - - var backendRuntimeFilePath string - - for _, runtime := range backendruntime.AvailableBackends { - if runtime.Name == req.Backend { - backendRuntimeFilePath = runtime.Path - } - } - - if backendRuntimeFilePath == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Unsupported backend recieved", - }) - - return - } - - backend := backendruntime.NewBackend(backendRuntimeFilePath) - err = backend.Start() - - if err != nil { - log.Warnf("Failed to start backend: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to start backend", - }) - - return - } - - backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{ - Arguments: backendParameters, - }) - - if err != nil { - log.Warnf("Failed to get response for backend: %s", err.Error()) - - err = backend.Stop() - if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) - } - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get status response from backend", - }) - - return - } - - switch responseMessage := backendParamCheckResponse.(type) { - case *commonbackend.CheckParametersResponse: - if responseMessage.InResponseTo != "checkServerParameters" { - log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo) + log.Warnf("Failed to get response for backend: %s", err.Error()) err = backend.Stop() @@ -163,107 +145,126 @@ func CreateBackend(c *gin.Context) { return } - if !responseMessage.IsValid { + switch responseMessage := backendParamCheckResponse.(type) { + case *commonbackend.CheckParametersResponse: + if responseMessage.InResponseTo != "checkServerParameters" { + log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo) + + err = backend.Stop() + + if err != nil { + log.Warnf("Failed to stop backend: %s", err.Error()) + } + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get status response from backend", + }) + + return + } + + if !responseMessage.IsValid { + err = backend.Stop() + + if err != nil { + log.Warnf("Failed to stop backend: %s", err.Error()) + } + + var errorMessage string + + if responseMessage.Message == "" { + errorMessage = "Unkown error while trying to parse connectionDetails" + } else { + errorMessage = fmt.Sprintf("Invalid backend parameters: %s", responseMessage.Message) + } + + c.JSON(http.StatusBadRequest, gin.H{ + "error": errorMessage, + }) + + return + } + default: + log.Warnf("Got illegal response type for backend: %T", responseMessage) + } + + log.Info("Passed backend checks successfully") + + backendInDatabase := &db.Backend{ + UserID: user.ID, + Name: req.Name, + Description: req.Description, + Backend: req.Backend, + BackendParameters: base64.StdEncoding.EncodeToString(backendParameters), + } + + if result := state.DB.DB.Create(&backendInDatabase); result.Error != nil { + log.Warnf("Failed to create backend: %s", result.Error.Error()) + err = backend.Stop() if err != nil { log.Warnf("Failed to stop backend: %s", err.Error()) } - var errorMessage string - - if responseMessage.Message == "" { - errorMessage = "Unkown error while trying to parse connectionDetails" - } else { - errorMessage = fmt.Sprintf("Invalid backend parameters: %s", responseMessage.Message) - } - - c.JSON(http.StatusBadRequest, gin.H{ - "error": errorMessage, + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add backend into database", }) return } - default: - log.Warnf("Got illegal response type for backend: %T", responseMessage) - } - log.Info("Passed backend checks successfully") - - backendInDatabase := &dbcore.Backend{ - UserID: user.ID, - Name: req.Name, - Description: req.Description, - Backend: req.Backend, - BackendParameters: base64.StdEncoding.EncodeToString(backendParameters), - } - - if result := dbcore.DB.Create(&backendInDatabase); result.Error != nil { - log.Warnf("Failed to create backend: %s", result.Error.Error()) - - err = backend.Stop() - - if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) - } - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add backend into database", + backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{ + Arguments: backendParameters, }) - return - } - - backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{ - Arguments: backendParameters, - }) - - if err != nil { - log.Warnf("Failed to get response for backend: %s", err.Error()) - - err = backend.Stop() - if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) - } + log.Warnf("Failed to get response for backend: %s", err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get status response from backend", - }) - - return - } - - switch responseMessage := backendStartResponse.(type) { - case *commonbackend.BackendStatusResponse: - if !responseMessage.IsRunning { err = backend.Stop() if err != nil { - log.Warnf("Failed to start backend: %s", err.Error()) + log.Warnf("Failed to stop backend: %s", err.Error()) } - var errorMessage string - - if responseMessage.Message == "" { - errorMessage = "Unkown error while trying to start the backend" - } else { - errorMessage = fmt.Sprintf("Failed to start backend: %s", responseMessage.Message) - } - - c.JSON(http.StatusBadRequest, gin.H{ - "error": errorMessage, + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get status response from backend", }) return } - default: - log.Warnf("Got illegal response type for backend: %T", responseMessage) - } - backendruntime.RunningBackends[backendInDatabase.ID] = backend + switch responseMessage := backendStartResponse.(type) { + case *commonbackend.BackendStatusResponse: + if !responseMessage.IsRunning { + err = backend.Stop() - c.JSON(http.StatusOK, gin.H{ - "success": true, + if err != nil { + log.Warnf("Failed to start backend: %s", err.Error()) + } + + var errorMessage string + + if responseMessage.Message == "" { + errorMessage = "Unkown error while trying to start the backend" + } else { + errorMessage = fmt.Sprintf("Failed to start backend: %s", responseMessage.Message) + } + + c.JSON(http.StatusBadRequest, gin.H{ + "error": errorMessage, + }) + + return + } + default: + log.Warnf("Got illegal response type for backend: %T", responseMessage) + } + + backendruntime.RunningBackends[backendInDatabase.ID] = backend + + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) }) } diff --git a/backend/api/controllers/v1/backends/lookup.go b/backend/api/controllers/v1/backends/lookup.go index 9819df7..6cbb386 100644 --- a/backend/api/controllers/v1/backends/lookup.go +++ b/backend/api/controllers/v1/backends/lookup.go @@ -7,12 +7,11 @@ import ( "strings" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type BackendLookupRequest struct { @@ -38,95 +37,80 @@ type LookupResponse struct { Data []*SanitizedBackend `json:"data"` } -func LookupBackend(c *gin.Context) { - var req BackendLookupRequest +func SetupLookupBackend(state *state.State) { + state.Engine.POST("/api/v1/backends/lookup", func(c *gin.Context) { + var req BackendLookupRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return } - } - if !permissions.UserHasPermission(user, "backends.visible") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - backends := []dbcore.Backend{} - queryString := []string{} - queryParameters := []interface{}{} + user, err := state.JWT.GetUserFromJWT(req.Token) - if req.BackendID != nil { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, req.BackendID) - } + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) - if req.Name != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - if req.Description != nil { - queryString = append(queryString, "description = ?") - queryParameters = append(queryParameters, req.Description) - } + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) - if req.Backend != nil { - queryString = append(queryString, "is_bot = ?") - queryParameters = append(queryParameters, req.Backend) - } + return + } + } - if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil { - log.Warnf("Failed to get backends: %s", err.Error()) + if !permissions.UserHasPermission(user, "backends.visible") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get backends", - }) + return + } - return - } + backends := []db.Backend{} + queryString := []string{} + queryParameters := []interface{}{} - sanitizedBackends := make([]*SanitizedBackend, len(backends)) - hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis") + if req.BackendID != nil { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, req.BackendID) + } - for backendIndex, backend := range backends { - foundBackend, ok := backendruntime.RunningBackends[backend.ID] + if req.Name != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } - if !ok { - log.Warnf("Failed to get backend #%d controller", backend.ID) + if req.Description != nil { + queryString = append(queryString, "description = ?") + queryParameters = append(queryParameters, req.Description) + } + + if req.Backend != nil { + queryString = append(queryString, "is_bot = ?") + queryParameters = append(queryParameters, req.Backend) + } + + if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil { + log.Warnf("Failed to get backends: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get backends", @@ -135,29 +119,46 @@ func LookupBackend(c *gin.Context) { return } - sanitizedBackends[backendIndex] = &SanitizedBackend{ - BackendID: backend.ID, - OwnerID: backend.UserID, - Name: backend.Name, - Description: backend.Description, - Backend: backend.Backend, - Logs: foundBackend.Logs, - } + sanitizedBackends := make([]*SanitizedBackend, len(backends)) + hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis") - if backend.UserID == user.ID || hasSecretVisibility { - backendParametersBytes, err := base64.StdEncoding.DecodeString(backend.BackendParameters) + for backendIndex, backend := range backends { + foundBackend, ok := backendruntime.RunningBackends[backend.ID] - if err != nil { - log.Warnf("Failed to decode base64 backend parameters: %s", err.Error()) + if !ok { + log.Warnf("Failed to get backend #%d controller", backend.ID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get backends", + }) + + return } - backendParameters := string(backendParametersBytes) - sanitizedBackends[backendIndex].BackendParameters = &backendParameters - } - } + sanitizedBackends[backendIndex] = &SanitizedBackend{ + BackendID: backend.ID, + OwnerID: backend.UserID, + Name: backend.Name, + Description: backend.Description, + Backend: backend.Backend, + Logs: foundBackend.Logs, + } - c.JSON(http.StatusOK, &LookupResponse{ - Success: true, - Data: sanitizedBackends, + if backend.UserID == user.ID || hasSecretVisibility { + backendParametersBytes, err := base64.StdEncoding.DecodeString(backend.BackendParameters) + + if err != nil { + log.Warnf("Failed to decode base64 backend parameters: %s", err.Error()) + } + + backendParameters := string(backendParametersBytes) + sanitizedBackends[backendIndex].BackendParameters = &backendParameters + } + } + + c.JSON(http.StatusOK, &LookupResponse{ + Success: true, + Data: sanitizedBackends, + }) }) } diff --git a/backend/api/controllers/v1/backends/remove.go b/backend/api/controllers/v1/backends/remove.go index f9dbd8c..338ccbd 100644 --- a/backend/api/controllers/v1/backends/remove.go +++ b/backend/api/controllers/v1/backends/remove.go @@ -5,12 +5,11 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type BackendRemovalRequest struct { @@ -18,106 +17,108 @@ type BackendRemovalRequest struct { BackendID uint `json:"id" validate:"required"` } -func RemoveBackend(c *gin.Context) { - var req BackendRemovalRequest +func SetupRemoveBackend(state *state.State) { + state.Engine.POST("/api/v1/backends/remove", func(c *gin.Context) { + var req BackendRemovalRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return } - } - if !permissions.UserHasPermission(user, "backends.remove") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - var backend *dbcore.Backend - backendRequest := dbcore.DB.Where("id = ?", req.BackendID).Find(&backend) - - if backendRequest.Error != nil { - log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if backend exists", - }) - - return - } - - backendExists := backendRequest.RowsAffected > 0 - - if !backendExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Backend doesn't exist", - }) - - return - } - - if err := dbcore.DB.Delete(backend).Error; err != nil { - log.Warnf("failed to delete backend: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to delete backend", - }) - - return - } - - backendInstance, ok := backendruntime.RunningBackends[req.BackendID] - - if ok { - err = backendInstance.Stop() + user, err := state.JWT.GetUserFromJWT(req.Token) if err != nil { - log.Warnf("Failed to stop backend: %s", err.Error()) + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Backend deleted, but failed to stop", + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "backends.remove") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", }) - delete(backendruntime.RunningBackends, req.BackendID) return } - delete(backendruntime.RunningBackends, req.BackendID) - } + var backend *db.Backend + backendRequest := state.DB.DB.Where("id = ?", req.BackendID).Find(&backend) - c.JSON(http.StatusOK, gin.H{ - "success": true, + if backendRequest.Error != nil { + log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if backend exists", + }) + + return + } + + backendExists := backendRequest.RowsAffected > 0 + + if !backendExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Backend doesn't exist", + }) + + return + } + + if err := state.DB.DB.Delete(backend).Error; err != nil { + log.Warnf("failed to delete backend: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete backend", + }) + + return + } + + backendInstance, ok := backendruntime.RunningBackends[req.BackendID] + + if ok { + err = backendInstance.Stop() + + if err != nil { + log.Warnf("Failed to stop backend: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Backend deleted, but failed to stop", + }) + + delete(backendruntime.RunningBackends, req.BackendID) + return + } + + delete(backendruntime.RunningBackends, req.BackendID) + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) }) } diff --git a/backend/api/controllers/v1/proxies/connections.go b/backend/api/controllers/v1/proxies/connections.go index 5eb3db9..7ea42fb 100644 --- a/backend/api/controllers/v1/proxies/connections.go +++ b/backend/api/controllers/v1/proxies/connections.go @@ -5,13 +5,12 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type ConnectionsRequest struct { @@ -37,127 +36,130 @@ type ConnectionsResponse struct { Data []*SanitizedConnection `json:"data"` } -func GetConnections(c *gin.Context) { - var req ConnectionsRequest +func SetupGetConnections(state *state.State) { + state.Engine.POST("/api/v1/forward/connections", func(c *gin.Context) { + var req ConnectionsRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return } - } - if !permissions.UserHasPermission(user, "routes.visibleConn") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - var proxy dbcore.Proxy - proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy) + user, err := state.JWT.GetUserFromJWT(req.Token) - if proxyRequest.Error != nil { - log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find forward entry", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "No forward entry found", - }) - - return - } - - backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{}) - - if err != nil { - log.Warnf("Failed to get response for backend: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get status response from backend", - }) - - return - } - - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyConnectionsResponse: - sanitizedConnections := []*SanitizedConnection{} - - for _, connection := range responseMessage.Connections { - if connection.SourceIP == proxy.SourceIP && connection.SourcePort == proxy.SourcePort && proxy.DestinationPort == proxy.DestinationPort { - sanitizedConnections = append(sanitizedConnections, &SanitizedConnection{ - ClientIP: connection.ClientIP, - Port: connection.ClientPort, - - ConnectionDetails: &ConnectionDetailsForConnection{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - }, + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return } } - c.JSON(http.StatusOK, &ConnectionsResponse{ - Success: true, - Data: sanitizedConnections, - }) - default: - log.Warnf("Got illegal response type for backend: %T", responseMessage) + if !permissions.UserHasPermission(user, "routes.visibleConn") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got illegal response type", - }) - } + return + } + + var proxy db.Proxy + proxyRequest := state.DB.DB.Where("id = ?", req.Id).First(&proxy) + + if proxyRequest.Error != nil { + log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find forward entry", + }) + + return + } + + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "No forward entry found", + }) + + return + } + + backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{}) + + if err != nil { + log.Warnf("Failed to get response for backend: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get status response from backend", + }) + + return + } + + switch responseMessage := backendResponse.(type) { + case *commonbackend.ProxyConnectionsResponse: + sanitizedConnections := []*SanitizedConnection{} + + for _, connection := range responseMessage.Connections { + if connection.SourceIP == proxy.SourceIP && connection.SourcePort == proxy.SourcePort && proxy.DestinationPort == proxy.DestinationPort { + sanitizedConnections = append(sanitizedConnections, &SanitizedConnection{ + ClientIP: connection.ClientIP, + Port: connection.ClientPort, + + ConnectionDetails: &ConnectionDetailsForConnection{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + }, + }) + } + } + + c.JSON(http.StatusOK, &ConnectionsResponse{ + Success: true, + Data: sanitizedConnections, + }) + default: + log.Warnf("Got illegal response type for backend: %T", responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got illegal response type", + }) + } + }) } diff --git a/backend/api/controllers/v1/proxies/create.go b/backend/api/controllers/v1/proxies/create.go index 28e8c95..d790c49 100644 --- a/backend/api/controllers/v1/proxies/create.go +++ b/backend/api/controllers/v1/proxies/create.go @@ -5,13 +5,12 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type ProxyCreationRequest struct { @@ -26,150 +25,153 @@ type ProxyCreationRequest struct { AutoStart *bool `json:"autoStart"` } -func CreateProxy(c *gin.Context) { - var req ProxyCreationRequest +func SetupCreateProxy(state *state.State) { + state.Engine.POST("/api/v1/forward/create", func(c *gin.Context) { + var req ProxyCreationRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.add") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - if req.Protocol != "tcp" && req.Protocol != "udp" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Protocol must be either 'tcp' or 'udp'", - }) - - return - } - - var backend dbcore.Backend - backendRequest := dbcore.DB.Where("id = ?", req.ProviderID).First(&backend) - - if backendRequest.Error != nil { - log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if backend exists", - }) - } - - backendExists := backendRequest.RowsAffected > 0 - - if !backendExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Could not find backend", - }) - } - - autoStart := false - - if req.AutoStart != nil { - autoStart = *req.AutoStart - } - - proxy := &dbcore.Proxy{ - UserID: user.ID, - BackendID: req.ProviderID, - Name: req.Name, - Description: req.Description, - Protocol: req.Protocol, - SourceIP: req.SourceIP, - SourcePort: req.SourcePort, - DestinationPort: req.DestinationPort, - AutoStart: autoStart, - } - - if result := dbcore.DB.Create(proxy); result.Error != nil { - log.Warnf("failed to create proxy: %s", result.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add forward rule to database", - }) - - return - } - - if autoStart { - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "id": proxy.ID, + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return } - backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := state.JWT.GetUserFromJWT(req.Token) if err != nil { - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to get response from backend", + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "routes.add") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", }) return } - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyStatusResponse: - if !responseMessage.IsActive { - log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID) - } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) - } - } + if req.Protocol != "tcp" && req.Protocol != "udp" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Protocol must be either 'tcp' or 'udp'", + }) - c.JSON(http.StatusOK, gin.H{ - "success": true, - "id": proxy.ID, + return + } + + var backend db.Backend + backendRequest := state.DB.DB.Where("id = ?", req.ProviderID).First(&backend) + + if backendRequest.Error != nil { + log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if backend exists", + }) + } + + backendExists := backendRequest.RowsAffected > 0 + + if !backendExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Could not find backend", + }) + } + + autoStart := false + + if req.AutoStart != nil { + autoStart = *req.AutoStart + } + + proxy := &db.Proxy{ + UserID: user.ID, + BackendID: req.ProviderID, + Name: req.Name, + Description: req.Description, + Protocol: req.Protocol, + SourceIP: req.SourceIP, + SourcePort: req.SourcePort, + DestinationPort: req.DestinationPort, + AutoStart: autoStart, + } + + if result := state.DB.DB.Create(proxy); result.Error != nil { + log.Warnf("failed to create proxy: %s", result.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add forward rule to database", + }) + + return + } + + if autoStart { + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "id": proxy.ID, + }) + + return + } + + backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, + }) + + if err != nil { + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to get response from backend", + }) + + return + } + + switch responseMessage := backendResponse.(type) { + case *commonbackend.ProxyStatusResponse: + if !responseMessage.IsActive { + log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID) + } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + } + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "id": proxy.ID, + }) }) } diff --git a/backend/api/controllers/v1/proxies/lookup.go b/backend/api/controllers/v1/proxies/lookup.go index 3b0d4c3..bf2c3ea 100644 --- a/backend/api/controllers/v1/proxies/lookup.go +++ b/backend/api/controllers/v1/proxies/lookup.go @@ -5,12 +5,11 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type ProxyLookupRequest struct { @@ -43,141 +42,143 @@ type ProxyLookupResponse struct { Data []*SanitizedProxy `json:"data"` } -func LookupProxy(c *gin.Context) { - var req ProxyLookupRequest +func SetupLookupProxy(state *state.State) { + state.Engine.POST("/api/v1/forward/lookup", func(c *gin.Context) { + var req ProxyLookupRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + } + + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := state.JWT.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "routes.visible") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + + if req.Protocol != nil { + if *req.Protocol != "tcp" && *req.Protocol != "udp" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Protocol specified in body must either be 'tcp' or 'udp'", + }) + + return + } + } + + proxies := []db.Proxy{} + + queryString := []string{} + queryParameters := []interface{}{} + + if req.Id != nil { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, req.Id) + } + + if req.Name != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } + + if req.Description != nil { + queryString = append(queryString, "description = ?") + queryParameters = append(queryParameters, req.Description) + } + + if req.SourceIP != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } + + if req.SourcePort != nil { + queryString = append(queryString, "source_port = ?") + queryParameters = append(queryParameters, req.SourcePort) + } + + if req.DestinationPort != nil { + queryString = append(queryString, "destination_port = ?") + queryParameters = append(queryParameters, req.DestinationPort) + } + + if req.ProviderID != nil { + queryString = append(queryString, "backend_id = ?") + queryParameters = append(queryParameters, req.ProviderID) + } + + if req.AutoStart != nil { + queryString = append(queryString, "auto_start = ?") + queryParameters = append(queryParameters, req.AutoStart) + } + + if req.Protocol != nil { + queryString = append(queryString, "protocol = ?") + queryParameters = append(queryParameters, req.Protocol) + } + + if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil { + log.Warnf("failed to get proxies: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + "error": "Failed to get proxies", }) return } - } - if !permissions.UserHasPermission(user, "routes.visible") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + sanitizedProxies := make([]*SanitizedProxy, len(proxies)) - return - } - - if req.Protocol != nil { - if *req.Protocol != "tcp" && *req.Protocol != "udp" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Protocol specified in body must either be 'tcp' or 'udp'", - }) - - return + for proxyIndex, proxy := range proxies { + sanitizedProxies[proxyIndex] = &SanitizedProxy{ + Id: proxy.ID, + Name: proxy.Name, + Description: proxy.Description, + Protcol: proxy.Protocol, + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestinationPort: proxy.DestinationPort, + ProviderID: proxy.BackendID, + AutoStart: proxy.AutoStart, + } } - } - proxies := []dbcore.Proxy{} - - queryString := []string{} - queryParameters := []interface{}{} - - if req.Id != nil { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, req.Id) - } - - if req.Name != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } - - if req.Description != nil { - queryString = append(queryString, "description = ?") - queryParameters = append(queryParameters, req.Description) - } - - if req.SourceIP != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } - - if req.SourcePort != nil { - queryString = append(queryString, "source_port = ?") - queryParameters = append(queryParameters, req.SourcePort) - } - - if req.DestinationPort != nil { - queryString = append(queryString, "destination_port = ?") - queryParameters = append(queryParameters, req.DestinationPort) - } - - if req.ProviderID != nil { - queryString = append(queryString, "backend_id = ?") - queryParameters = append(queryParameters, req.ProviderID) - } - - if req.AutoStart != nil { - queryString = append(queryString, "auto_start = ?") - queryParameters = append(queryParameters, req.AutoStart) - } - - if req.Protocol != nil { - queryString = append(queryString, "protocol = ?") - queryParameters = append(queryParameters, req.Protocol) - } - - if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil { - log.Warnf("failed to get proxies: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get proxies", + c.JSON(http.StatusOK, &ProxyLookupResponse{ + Success: true, + Data: sanitizedProxies, }) - - return - } - - sanitizedProxies := make([]*SanitizedProxy, len(proxies)) - - for proxyIndex, proxy := range proxies { - sanitizedProxies[proxyIndex] = &SanitizedProxy{ - Id: proxy.ID, - Name: proxy.Name, - Description: proxy.Description, - Protcol: proxy.Protocol, - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestinationPort: proxy.DestinationPort, - ProviderID: proxy.BackendID, - AutoStart: proxy.AutoStart, - } - } - - c.JSON(http.StatusOK, &ProxyLookupResponse{ - Success: true, - Data: sanitizedProxies, }) } diff --git a/backend/api/controllers/v1/proxies/remove.go b/backend/api/controllers/v1/proxies/remove.go index e41fa84..304c5c7 100644 --- a/backend/api/controllers/v1/proxies/remove.go +++ b/backend/api/controllers/v1/proxies/remove.go @@ -5,13 +5,12 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type ProxyRemovalRequest struct { @@ -19,134 +18,133 @@ type ProxyRemovalRequest struct { ID uint `validate:"required" json:"id"` } -func RemoveProxy(c *gin.Context) { - var req ProxyRemovalRequest +func SetupRemoveProxy(state *state.State) { + state.Engine.POST("/api/v1/forward/remove", func(c *gin.Context) { + var req ProxyRemovalRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { + user, err := state.JWT.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "routes.remove") { c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + "error": "Missing permissions", }) return } - } - if !permissions.UserHasPermission(user, "routes.remove") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + var proxy *db.Proxy + proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) - return - } + if proxyRequest.Error != nil { + log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - var proxy *dbcore.Proxy - proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if forward rule exists", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Forward rule doesn't exist", - }) - - return - } - - if err := dbcore.DB.Delete(proxy).Error; err != nil { - log.Warnf("failed to delete proxy: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to delete forward rule", - }) - - return - } - - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, - }) - - if err != nil { - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get response from backend. Proxy was still successfully deleted", - }) - - return - } - - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyStatusResponse: - if responseMessage.IsActive { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to stop proxy. Proxy was still successfully deleted", + "error": "Failed to find if forward rule exists", }) return } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got invalid response from backend. Proxy was still successfully deleted", + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Forward rule doesn't exist", + }) + + return + } + + if err := state.DB.DB.Delete(proxy).Error; err != nil { + log.Warnf("failed to delete proxy: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to delete forward rule", + }) + + return + } + + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, }) - return - } + if err != nil { + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) - c.JSON(http.StatusOK, gin.H{ - "success": true, + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to get response from backend. Proxy was still successfully deleted", + }) + + return + } + + switch responseMessage := backendResponse.(type) { + case *commonbackend.ProxyStatusResponse: + if responseMessage.IsActive { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to stop proxy. Proxy was still successfully deleted", + }) + } else { + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) + } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got invalid response from backend. Proxy was still successfully deleted", + }) + } }) } diff --git a/backend/api/controllers/v1/proxies/start.go b/backend/api/controllers/v1/proxies/start.go index 1573382..1680ddf 100644 --- a/backend/api/controllers/v1/proxies/start.go +++ b/backend/api/controllers/v1/proxies/start.go @@ -5,13 +5,12 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type ProxyStartRequest struct { @@ -19,124 +18,119 @@ type ProxyStartRequest struct { ID uint `validate:"required" json:"id"` } -func StartProxy(c *gin.Context) { - var req ProxyStartRequest +func SetupStartProxy(state *state.State) { + state.Engine.POST("/api/v1/forward/start", func(c *gin.Context) { + var req ProxyStartRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { + user, err := state.JWT.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "routes.start") { c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - if !permissions.UserHasPermission(user, "routes.start") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - - var proxy *dbcore.Proxy - proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if forward rule exists", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Forward rule doesn't exist", - }) - - return - } - - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, - }) - - switch responseMessage := backendResponse.(type) { - case error: - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to get response from backend", - }) - - return - case *commonbackend.ProxyStatusResponse: - if !responseMessage.IsActive { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to start proxy", + "error": "Missing permissions", }) return } - break - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + var proxy *db.Proxy + proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got invalid response from backend. Proxy was still successfully deleted", + if proxyRequest.Error != nil { + log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if forward rule exists", + }) + + return + } + + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Forward rule doesn't exist", + }) + + return + } + + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, }) - return - } + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) - c.JSON(http.StatusOK, gin.H{ - "success": true, + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to get response from backend", + }) + case *commonbackend.ProxyStatusResponse: + if !responseMessage.IsActive { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to start proxy", + }) + } else { + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) + } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got invalid response from backend. Proxy was likely still successfully started", + }) + } }) } diff --git a/backend/api/controllers/v1/proxies/stop.go b/backend/api/controllers/v1/proxies/stop.go index 820cec4..27d63ce 100644 --- a/backend/api/controllers/v1/proxies/stop.go +++ b/backend/api/controllers/v1/proxies/stop.go @@ -5,13 +5,12 @@ import ( "net/http" "git.terah.dev/imterah/hermes/backend/api/backendruntime" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type ProxyStopRequest struct { @@ -19,124 +18,119 @@ type ProxyStopRequest struct { ID uint `validate:"required" json:"id"` } -func StopProxy(c *gin.Context) { - var req ProxyStopRequest +func SetupStopProxy(state *state.State) { + state.Engine.POST("/api/v1/forward/stop", func(c *gin.Context) { + var req ProxyStartRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } + return + } - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user, err := jwtcore.GetUserFromJWT(req.Token) - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { + user, err := state.JWT.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + if !permissions.UserHasPermission(user, "routes.stop") { c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + "error": "Missing permissions", }) return } - } - if !permissions.UserHasPermission(user, "routes.stop") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) + var proxy *db.Proxy + proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy) - return - } + if proxyRequest.Error != nil { + log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - var proxy *dbcore.Proxy - proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy) - - if proxyRequest.Error != nil { - log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if forward rule exists", - }) - - return - } - - proxyExists := proxyRequest.RowsAffected > 0 - - if !proxyExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Forward rule doesn't exist", - }) - - return - } - - backend, ok := backendruntime.RunningBackends[proxy.BackendID] - - if !ok { - log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Couldn't fetch backend runtime", - }) - - return - } - - backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ - SourceIP: proxy.SourceIP, - SourcePort: proxy.SourcePort, - DestPort: proxy.DestinationPort, - Protocol: proxy.Protocol, - }) - - if err != nil { - log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to get response from backend", - }) - - return - } - - switch responseMessage := backendResponse.(type) { - case *commonbackend.ProxyStatusResponse: - if responseMessage.IsActive { c.JSON(http.StatusInternalServerError, gin.H{ - "error": "failed to stop proxy", + "error": "Failed to find if forward rule exists", }) return } - default: - log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Got invalid response from backend. Proxy was still successfully deleted", + proxyExists := proxyRequest.RowsAffected > 0 + + if !proxyExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Forward rule doesn't exist", + }) + + return + } + + backend, ok := backendruntime.RunningBackends[proxy.BackendID] + + if !ok { + log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Couldn't fetch backend runtime", + }) + + return + } + + backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{ + SourceIP: proxy.SourceIP, + SourcePort: proxy.SourcePort, + DestPort: proxy.DestinationPort, + Protocol: proxy.Protocol, }) - return - } + switch responseMessage := backendResponse.(type) { + case error: + log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error()) - c.JSON(http.StatusOK, gin.H{ - "success": true, + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to get response from backend", + }) + case *commonbackend.ProxyStatusResponse: + if responseMessage.IsActive { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to stop proxy", + }) + } else { + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) + } + default: + log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Got invalid response from backend. Proxy was likely still successfully stopped", + }) + } }) } diff --git a/backend/api/controllers/v1/users/create.go b/backend/api/controllers/v1/users/create.go index e92858a..f39aa95 100644 --- a/backend/api/controllers/v1/users/create.go +++ b/backend/api/controllers/v1/users/create.go @@ -7,11 +7,9 @@ import ( "net/http" "strings" - "github.com/go-playground/validator/v10" - - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" @@ -22,142 +20,141 @@ type UserCreationRequest struct { Email string `validate:"required"` Password string `validate:"required"` Username string `validate:"required"` - - // TODO: implement support - ExistingUserToken string `json:"token"` - IsBot bool + IsBot bool } -func CreateUser(c *gin.Context) { - if !signupEnabled && !unsafeSignup { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Signing up is not enabled at this time.", - }) +func SetupCreateUser(state *state.State) { + state.Engine.POST("/api/v1/users/create", func(c *gin.Context) { + if !signupEnabled && !unsafeSignup { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Signing up is not enabled at this time.", + }) - return - } - - var req UserCreationRequest - - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - var user *dbcore.User - userRequest := dbcore.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user) - - if userRequest.Error != nil { - log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if user exists", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if userExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "User already exists", - }) - - return - } - - passwordHashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - - if err != nil { - log.Warnf("Failed to generate password for client upon signup: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate password hash", - }) - - return - } - - permissions := []dbcore.Permission{} - - for _, permission := range permissionHelper.DefaultPermissionNodes { - permissionEnabledState := false - - if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" { - permissionEnabledState = true + return } - permissions = append(permissions, dbcore.Permission{ - PermissionNode: permission, - HasPermission: permissionEnabledState, - }) - } + var req UserCreationRequest - tokenRandomData := make([]byte, 80) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - if _, err := rand.Read(tokenRandomData); err != nil { - log.Warnf("Failed to read random data to use as token: %s", err.Error()) + return + } - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - return - } + return + } - user = &dbcore.User{ - Email: req.Email, - Username: req.Username, - Name: req.Name, - IsBot: &req.IsBot, - Password: base64.StdEncoding.EncodeToString(passwordHashed), - Permissions: permissions, - Tokens: []dbcore.Token{ - { - Token: base64.StdEncoding.EncodeToString(tokenRandomData), - DisableExpiry: forceNoExpiryTokens, - CreationIPAddr: c.ClientIP(), + var user *db.User + userRequest := state.DB.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User already exists", + }) + + return + } + + passwordHashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + + if err != nil { + log.Warnf("Failed to generate password for client upon signup: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate password hash", + }) + + return + } + + permissions := []db.Permission{} + + for _, permission := range permissionHelper.DefaultPermissionNodes { + permissionEnabledState := false + + if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" { + permissionEnabledState = true + } + + permissions = append(permissions, db.Permission{ + PermissionNode: permission, + HasPermission: permissionEnabledState, + }) + } + + tokenRandomData := make([]byte, 80) + + if _, err := rand.Read(tokenRandomData); err != nil { + log.Warnf("Failed to read random data to use as token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + user = &db.User{ + Email: req.Email, + Username: req.Username, + Name: req.Name, + IsBot: &req.IsBot, + Password: base64.StdEncoding.EncodeToString(passwordHashed), + Permissions: permissions, + Tokens: []db.Token{ + { + Token: base64.StdEncoding.EncodeToString(tokenRandomData), + DisableExpiry: forceNoExpiryTokens, + CreationIPAddr: c.ClientIP(), + }, }, - }, - } + } - if result := dbcore.DB.Create(&user); result.Error != nil { - log.Warnf("Failed to create user: %s", result.Error.Error()) + if result := state.DB.DB.Create(&user); result.Error != nil { + log.Warnf("Failed to create user: %s", result.Error.Error()) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add user into database", + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add user into database", + }) + + return + } + + jwt, err := state.JWT.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, + "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), }) - - return - } - - jwt, err := jwtcore.Generate(user.ID) - - if err != nil { - log.Warnf("Failed to generate JWT: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "token": jwt, - "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), }) } diff --git a/backend/api/controllers/v1/users/login.go b/backend/api/controllers/v1/users/login.go index b6fd586..ea4f2f3 100644 --- a/backend/api/controllers/v1/users/login.go +++ b/backend/api/controllers/v1/users/login.go @@ -6,11 +6,10 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" "golang.org/x/crypto/bcrypt" ) @@ -21,137 +20,139 @@ type UserLoginRequest struct { Password string `validate:"required"` } -func LoginUser(c *gin.Context) { - var req UserLoginRequest +func SetupLoginUser(state *state.State) { + state.Engine.POST("/api/v1/users/login", func(c *gin.Context) { + var req UserLoginRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) + + return + } + + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + if req.Email == nil && req.Username == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing both email and username in body", + }) + + return + } + + userFindRequestArguments := make([]interface{}, 1) + userFindRequest := "" + + if req.Email != nil { + userFindRequestArguments[0] = &req.Email + userFindRequest += "email = ?" + } + + if req.Username != nil { + userFindRequestArguments[0] = &req.Username + userFindRequest += "username = ?" + } + + var user *db.User + userRequest := state.DB.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User not found", + }) + + return + } + + decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password))) + _, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password)) + + if err != nil { + log.Warnf("failed to decode password in database: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse database result for password", + }) + + return + } + + err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password)) + + if err != nil { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Invalid password", + }) + + return + } + + tokenRandomData := make([]byte, 80) + + if _, err := rand.Read(tokenRandomData); err != nil { + log.Warnf("Failed to read random data to use as token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + token := &db.Token{ + UserID: user.ID, + + Token: base64.StdEncoding.EncodeToString(tokenRandomData), + DisableExpiry: forceNoExpiryTokens, + CreationIPAddr: c.ClientIP(), + } + + if result := state.DB.DB.Create(&token); result.Error != nil { + log.Warnf("Failed to create user: %s", result.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to add refresh token into database", + }) + + return + } + + jwt, err := state.JWT.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, + "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - if req.Email == nil && req.Username == nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Missing both email and username in body", - }) - - return - } - - userFindRequestArguments := make([]interface{}, 1) - userFindRequest := "" - - if req.Email != nil { - userFindRequestArguments[0] = &req.Email - userFindRequest += "email = ?" - } - - if req.Username != nil { - userFindRequestArguments[0] = &req.Username - userFindRequest += "username = ?" - } - - var user *dbcore.User - userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user) - - if userRequest.Error != nil { - log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if user exists", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "User not found", - }) - - return - } - - decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password))) - _, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password)) - - if err != nil { - log.Warnf("failed to decode password in database: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse database result for password", - }) - - return - } - - err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password)) - - if err != nil { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Invalid password", - }) - - return - } - - tokenRandomData := make([]byte, 80) - - if _, err := rand.Read(tokenRandomData); err != nil { - log.Warnf("Failed to read random data to use as token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - token := &dbcore.Token{ - UserID: user.ID, - - Token: base64.StdEncoding.EncodeToString(tokenRandomData), - DisableExpiry: forceNoExpiryTokens, - CreationIPAddr: c.ClientIP(), - } - - if result := dbcore.DB.Create(&token); result.Error != nil { - log.Warnf("Failed to create user: %s", result.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to add refresh token into database", - }) - - return - } - - jwt, err := jwtcore.Generate(user.ID) - - if err != nil { - log.Warnf("Failed to generate JWT: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "token": jwt, - "refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData), }) } diff --git a/backend/api/controllers/v1/users/lookup.go b/backend/api/controllers/v1/users/lookup.go index 04782e5..f5c14fc 100644 --- a/backend/api/controllers/v1/users/lookup.go +++ b/backend/api/controllers/v1/users/lookup.go @@ -5,12 +5,11 @@ import ( "net/http" "strings" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type UserLookupRequest struct { @@ -35,102 +34,104 @@ type LookupResponse struct { Data []*SanitizedUsers `json:"data"` } -func LookupUser(c *gin.Context) { - var req UserLookupRequest +func SetupLookupUser(state *state.State) { + state.Engine.POST("/api/v1/users/lookup", func(c *gin.Context) { + var req UserLookupRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + } + + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) + + return + } + + user, err := state.JWT.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + users := []db.User{} + queryString := []string{} + queryParameters := []interface{}{} + + if !permissions.UserHasPermission(user, "users.lookup") { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, user.ID) + } else if permissions.UserHasPermission(user, "users.lookup") && req.UID != nil { + queryString = append(queryString, "id = ?") + queryParameters = append(queryParameters, req.UID) + } + + if req.Name != nil { + queryString = append(queryString, "name = ?") + queryParameters = append(queryParameters, req.Name) + } + + if req.Email != nil { + queryString = append(queryString, "email = ?") + queryParameters = append(queryParameters, req.Email) + } + + if req.IsBot != nil { + queryString = append(queryString, "is_bot = ?") + queryParameters = append(queryParameters, req.IsBot) + } + + if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil { + log.Warnf("Failed to get users: %s", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", + "error": "Failed to get users", }) return } - } - users := []dbcore.User{} - queryString := []string{} - queryParameters := []interface{}{} + sanitizedUsers := make([]*SanitizedUsers, len(users)) - if !permissions.UserHasPermission(user, "users.lookup") { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, user.ID) - } else if permissions.UserHasPermission(user, "users.lookup") && req.UID != nil { - queryString = append(queryString, "id = ?") - queryParameters = append(queryParameters, req.UID) - } + for userIndex, user := range users { + isBot := false - if req.Name != nil { - queryString = append(queryString, "name = ?") - queryParameters = append(queryParameters, req.Name) - } + if user.IsBot != nil { + isBot = *user.IsBot + } - if req.Email != nil { - queryString = append(queryString, "email = ?") - queryParameters = append(queryParameters, req.Email) - } + sanitizedUsers[userIndex] = &SanitizedUsers{ + UID: user.ID, + Name: user.Name, + Email: user.Email, + Username: user.Username, + IsBot: isBot, + } + } - if req.IsBot != nil { - queryString = append(queryString, "is_bot = ?") - queryParameters = append(queryParameters, req.IsBot) - } - - if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil { - log.Warnf("Failed to get users: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to get users", + c.JSON(http.StatusOK, &LookupResponse{ + Success: true, + Data: sanitizedUsers, }) - - return - } - - sanitizedUsers := make([]*SanitizedUsers, len(users)) - - for userIndex, user := range users { - isBot := false - - if user.IsBot != nil { - isBot = *user.IsBot - } - - sanitizedUsers[userIndex] = &SanitizedUsers{ - UID: user.ID, - Name: user.Name, - Email: user.Email, - Username: user.Username, - IsBot: isBot, - } - } - - c.JSON(http.StatusOK, &LookupResponse{ - Success: true, - Data: sanitizedUsers, }) } diff --git a/backend/api/controllers/v1/users/refresh.go b/backend/api/controllers/v1/users/refresh.go index 59fcf80..ce9fbaf 100644 --- a/backend/api/controllers/v1/users/refresh.go +++ b/backend/api/controllers/v1/users/refresh.go @@ -5,113 +5,114 @@ import ( "net/http" "time" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type UserRefreshRequest struct { Token string `validate:"required"` } -func RefreshUserToken(c *gin.Context) { - var req UserRefreshRequest +func SetupRefreshUserToken(state *state.State) { + state.Engine.POST("/api/v1/users/refresh", func(c *gin.Context) { + var req UserRefreshRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), + }) - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - var tokenInDatabase *dbcore.Token - tokenRequest := dbcore.DB.Where("token = ?", req.Token).Find(&tokenInDatabase) - - if tokenRequest.Error != nil { - log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if token exists", - }) - - return - } - - tokenExists := tokenRequest.RowsAffected > 0 - - if !tokenExists { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Token not found", - }) - - return - } - - // First, we check to make sure that the key expiry is disabled before checking if the key is expired. - // Then, we check if the IP addresses differ, or if it has been 7 days since the token has been created. - if !tokenInDatabase.DisableExpiry && (c.ClientIP() != tokenInDatabase.CreationIPAddr || time.Now().Before(tokenInDatabase.CreatedAt.Add((24*7)*time.Hour))) { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Token has expired", - }) - - tx := dbcore.DB.Delete(tokenInDatabase) - - if tx.Error != nil { - log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error()) + return } - return - } + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - // Get the user to check if the user exists before doing anything - var user *dbcore.User - userRequest := dbcore.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user) + return + } - if tokenRequest.Error != nil { - log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error()) + var tokenInDatabase *db.Token + tokenRequest := state.DB.DB.Where("token = ?", req.Token).Find(&tokenInDatabase) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find user", + if tokenRequest.Error != nil { + log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if token exists", + }) + + return + } + + tokenExists := tokenRequest.RowsAffected > 0 + + if !tokenExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Token not found", + }) + + return + } + + // First, we check to make sure that the key expiry is disabled before checking if the key is expired. + // Then, we check if the IP addresses differ, or if it has been 7 days since the token has been created. + if !tokenInDatabase.DisableExpiry && (c.ClientIP() != tokenInDatabase.CreationIPAddr || time.Now().Before(tokenInDatabase.CreatedAt.Add((24*7)*time.Hour))) { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Token has expired", + }) + + tx := state.DB.DB.Delete(tokenInDatabase) + + if tx.Error != nil { + log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error()) + } + + return + } + + // Get the user to check if the user exists before doing anything + var user *db.User + userRequest := state.DB.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user) + + if tokenRequest.Error != nil { + log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find user", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "User not found", + }) + + return + } + + jwt, err := state.JWT.Generate(user.ID) + + if err != nil { + log.Warnf("Failed to generate JWT: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to generate refresh token", + }) + + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "token": jwt, }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "User not found", - }) - - return - } - - jwt, err := jwtcore.Generate(user.ID) - - if err != nil { - log.Warnf("Failed to generate JWT: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to generate refresh token", - }) - - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "token": jwt, }) } diff --git a/backend/api/controllers/v1/users/remove.go b/backend/api/controllers/v1/users/remove.go index 5446755..59e460c 100644 --- a/backend/api/controllers/v1/users/remove.go +++ b/backend/api/controllers/v1/users/remove.go @@ -4,12 +4,11 @@ import ( "fmt" "net/http" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" "git.terah.dev/imterah/hermes/backend/api/permissions" + "git.terah.dev/imterah/hermes/backend/api/state" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" ) type UserRemovalRequest struct { @@ -17,89 +16,91 @@ type UserRemovalRequest struct { UID *uint `json:"uid"` } -func RemoveUser(c *gin.Context) { - var req UserRemovalRequest +func SetupRemoveUser(state *state.State) { + state.Engine.POST("/api/v1/users/remove", func(c *gin.Context) { + var req UserRemovalRequest - if err := c.BindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), - }) - - return - } - - if err := validator.New().Struct(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), - }) - - return - } - - user, err := jwtcore.GetUserFromJWT(req.Token) - - if err != nil { - if err.Error() == "token is expired" || err.Error() == "user does not exist" { - c.JSON(http.StatusForbidden, gin.H{ - "error": err.Error(), - }) - - return - } else { - log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to parse token", - }) - - return - } - } - - uid := user.ID - - if req.UID != nil { - uid = *req.UID - - if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") { - c.JSON(http.StatusForbidden, gin.H{ - "error": "Missing permissions", - }) - - return - } - } - - // Make sure the user exists first if we have a custom UserID - - if uid != user.ID { - var customUser *dbcore.User - userRequest := dbcore.DB.Where("id = ?", uid).Find(customUser) - - if userRequest.Error != nil { - log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) - - c.JSON(http.StatusInternalServerError, gin.H{ - "error": "Failed to find if user exists", - }) - - return - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { + if err := c.BindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "error": "User doesn't exist", + "error": fmt.Sprintf("Failed to parse body: %s", err.Error()), }) return } - } - dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user) + if err := state.Validator.Struct(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": fmt.Sprintf("Failed to validate body: %s", err.Error()), + }) - c.JSON(http.StatusOK, gin.H{ - "success": true, + return + } + + user, err := state.JWT.GetUserFromJWT(req.Token) + + if err != nil { + if err.Error() == "token is expired" || err.Error() == "user does not exist" { + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + + return + } else { + log.Warnf("Failed to get user from the provided JWT token: %s", err.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to parse token", + }) + + return + } + } + + uid := user.ID + + if req.UID != nil { + uid = *req.UID + + if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") { + c.JSON(http.StatusForbidden, gin.H{ + "error": "Missing permissions", + }) + + return + } + } + + // Make sure the user exists first if we have a custom UserID + + if uid != user.ID { + var customUser *db.User + userRequest := state.DB.DB.Where("id = ?", uid).Find(customUser) + + if userRequest.Error != nil { + log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error()) + + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to find if user exists", + }) + + return + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "User doesn't exist", + }) + + return + } + } + + state.DB.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + }) }) } diff --git a/backend/api/db/db.go b/backend/api/db/db.go new file mode 100644 index 0000000..295bff8 --- /dev/null +++ b/backend/api/db/db.go @@ -0,0 +1,77 @@ +package db + +import ( + "fmt" + "os" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type DB struct { + DB *gorm.DB +} + +func New(backend, params string) (*DB, error) { + var err error + + dialector, err := initDialector(backend, params) + + if err != nil { + return nil, fmt.Errorf("failed to initialize physical database: %s", err) + } + + database, err := gorm.Open(dialector) + + if err != nil { + return nil, fmt.Errorf("failed to open database: %s", err) + } + + return &DB{DB: database}, nil +} + +func (db *DB) DoMigrations() error { + if err := db.DB.AutoMigrate(&Proxy{}); err != nil { + return err + } + + if err := db.DB.AutoMigrate(&Backend{}); err != nil { + return err + } + + if err := db.DB.AutoMigrate(&Permission{}); err != nil { + return err + } + + if err := db.DB.AutoMigrate(&Token{}); err != nil { + return err + } + + if err := db.DB.AutoMigrate(&User{}); err != nil { + return err + } + + return nil +} + +func initDialector(backend, params string) (gorm.Dialector, error) { + switch backend { + case "sqlite": + if params == "" { + return nil, fmt.Errorf("sqlite database file not specified") + } + + return sqlite.Open(params), nil + case "postgresql": + if params == "" { + return nil, fmt.Errorf("postgres DSN not specified") + } + + return postgres.Open(params), nil + case "": + return nil, fmt.Errorf("no database backend specified in environment variables") + default: + return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(backend)) + } +} diff --git a/backend/api/db/models.go b/backend/api/db/models.go new file mode 100644 index 0000000..290cd6e --- /dev/null +++ b/backend/api/db/models.go @@ -0,0 +1,66 @@ +package db + +import ( + "gorm.io/gorm" +) + +type Backend struct { + gorm.Model + + UserID uint + + Name string + Description *string + Backend string + BackendParameters string + + Proxies []Proxy +} + +type Proxy struct { + gorm.Model + + BackendID uint + UserID uint + + Name string + Description *string + Protocol string + SourceIP string + SourcePort uint16 + DestinationPort uint16 + AutoStart bool +} + +type Permission struct { + gorm.Model + + PermissionNode string + HasPermission bool + UserID uint +} + +type Token struct { + gorm.Model + + UserID uint + + Token string + DisableExpiry bool + CreationIPAddr string +} + +type User struct { + gorm.Model + + Email string `gorm:"unique"` + Username string `gorm:"unique"` + Name string + Password string + IsBot *bool + + Permissions []Permission + OwnedProxies []Proxy + OwnedBackends []Backend + Tokens []Token +} diff --git a/backend/api/dbcore/db.go b/backend/api/dbcore/db.go deleted file mode 100644 index b5e0676..0000000 --- a/backend/api/dbcore/db.go +++ /dev/null @@ -1,142 +0,0 @@ -package dbcore - -import ( - "fmt" - "os" - - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -type Backend struct { - gorm.Model - - UserID uint - - Name string - Description *string - Backend string - BackendParameters string - - Proxies []Proxy -} - -type Proxy struct { - gorm.Model - - BackendID uint - UserID uint - - Name string - Description *string - Protocol string - SourceIP string - SourcePort uint16 - DestinationPort uint16 - AutoStart bool -} - -type Permission struct { - gorm.Model - - PermissionNode string - HasPermission bool - UserID uint -} - -type Token struct { - gorm.Model - - UserID uint - - Token string - DisableExpiry bool - CreationIPAddr string -} - -type User struct { - gorm.Model - - Email string `gorm:"unique"` - Username string `gorm:"unique"` - Name string - Password string - IsBot *bool - - Permissions []Permission - OwnedProxies []Proxy - OwnedBackends []Backend - Tokens []Token -} - -var DB *gorm.DB - -func InitializeDatabaseDialector() (gorm.Dialector, error) { - databaseBackend := os.Getenv("HERMES_DATABASE_BACKEND") - - switch databaseBackend { - case "sqlite": - filePath := os.Getenv("HERMES_SQLITE_FILEPATH") - - if filePath == "" { - return nil, fmt.Errorf("sqlite database file not specified (missing HERMES_SQLITE_FILEPATH)") - } - - return sqlite.Open(filePath), nil - case "postgresql": - postgresDSN := os.Getenv("HERMES_POSTGRES_DSN") - - if postgresDSN == "" { - return nil, fmt.Errorf("postgres DSN not specified (missing HERMES_POSTGRES_DSN)") - } - - return postgres.Open(postgresDSN), nil - case "": - return nil, fmt.Errorf("no database backend specified in environment variables (missing HERMES_DATABASE_BACKEND)") - default: - return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(databaseBackend)) - } -} - -func InitializeDatabase(config *gorm.Config) error { - var err error - - dialector, err := InitializeDatabaseDialector() - - if err != nil { - return fmt.Errorf("failed to initialize physical database: %s", err) - } - - DB, err = gorm.Open(dialector, config) - - if err != nil { - return fmt.Errorf("failed to open database: %s", err) - } - - return nil -} - -func DoDatabaseMigrations(db *gorm.DB) error { - if err := db.AutoMigrate(&Proxy{}); err != nil { - return err - } - - if err := db.AutoMigrate(&Backend{}); err != nil { - return err - } - - if err := db.AutoMigrate(&Permission{}); err != nil { - return err - } - - if err := db.AutoMigrate(&Token{}); err != nil { - return err - } - - if err := db.AutoMigrate(&User{}); err != nil { - return err - } - - return nil -} diff --git a/backend/api/jwt/jwt.go b/backend/api/jwt/jwt.go new file mode 100644 index 0000000..40e011b --- /dev/null +++ b/backend/api/jwt/jwt.go @@ -0,0 +1,107 @@ +package jwt + +import ( + "errors" + "fmt" + "strconv" + "time" + + "git.terah.dev/imterah/hermes/backend/api/db" + "github.com/golang-jwt/jwt/v5" +) + +var ( + DevelopmentModeTimings = time.Duration(60*24) * time.Minute + NormalModeTimings = time.Duration(3) * time.Minute +) + +type JWTCore struct { + Key []byte + Database *db.DB + TimeMultiplier time.Duration +} + +func New(key []byte, database *db.DB, timeMultiplier time.Duration) *JWTCore { + jwtCore := &JWTCore{ + Key: key, + Database: database, + TimeMultiplier: timeMultiplier, + } + + return jwtCore +} + +func (jwtCore *JWTCore) Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) { + return jwt.Parse(tokenString, jwtCore.jwtKeyCallback, options...) +} + +func (jwtCore *JWTCore) GetUserFromJWT(token string) (*db.User, error) { + if jwtCore.Database == nil { + return nil, fmt.Errorf("database is not initialized") + } + + parsedJWT, err := jwtCore.Parse(token) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, fmt.Errorf("token is expired") + } else { + return nil, err + } + } + + audience, err := parsedJWT.Claims.GetAudience() + + if err != nil { + return nil, err + } + + if len(audience) < 1 { + return nil, fmt.Errorf("audience is too small") + } + + uid, err := strconv.Atoi(audience[0]) + + if err != nil { + return nil, err + } + + user := &db.User{} + userRequest := jwtCore.Database.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user) + + if userRequest.Error != nil { + return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error()) + } + + userExists := userRequest.RowsAffected > 0 + + if !userExists { + return user, fmt.Errorf("user does not exist") + } + + return user, nil +} + +func (jwtCore *JWTCore) Generate(uid uint) (string, error) { + currentJWTTime := jwt.NewNumericDate(time.Now()) + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtCore.TimeMultiplier)), + IssuedAt: currentJWTTime, + NotBefore: currentJWTTime, + // Convert the user ID to a string, and then set it as the audience parameters only value (there's only 1 user per key) + Audience: []string{strconv.Itoa(int(uid))}, + }) + + signedToken, err := token.SignedString(jwtCore.Key) + + if err != nil { + return "", err + } + + return signedToken, nil +} + +func (jwtCore *JWTCore) jwtKeyCallback(*jwt.Token) (any, error) { + return jwtCore.Key, nil +} diff --git a/backend/api/jwtcore/jwt.go b/backend/api/jwtcore/jwt.go deleted file mode 100644 index ed2ec56..0000000 --- a/backend/api/jwtcore/jwt.go +++ /dev/null @@ -1,117 +0,0 @@ -package jwtcore - -import ( - "encoding/base64" - "errors" - "fmt" - "os" - "strconv" - "time" - - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "github.com/golang-jwt/jwt/v5" -) - -var ( - JWTKey []byte - developmentMode bool -) - -func SetupJWT() error { - var err error - jwtDataString := os.Getenv("HERMES_JWT_SECRET") - - if jwtDataString == "" { - return fmt.Errorf("JWT secret isn't set (missing HERMES_JWT_SECRET)") - } - - if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" { - JWTKey, err = base64.StdEncoding.DecodeString(jwtDataString) - - if err != nil { - return fmt.Errorf("failed to decode base64 JWT: %s", err.Error()) - } - } else { - JWTKey = []byte(jwtDataString) - } - - if os.Getenv("HERMES_DEVELOPMENT_MODE") != "" { - developmentMode = true - } - - return nil -} - -func Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) { - return jwt.Parse(tokenString, JWTKeyCallback, options...) -} - -func GetUserFromJWT(token string) (*dbcore.User, error) { - parsedJWT, err := Parse(token) - - if err != nil { - if errors.Is(err, jwt.ErrTokenExpired) { - return nil, fmt.Errorf("token is expired") - } else { - return nil, err - } - } - - audience, err := parsedJWT.Claims.GetAudience() - - if err != nil { - return nil, err - } - - if len(audience) < 1 { - return nil, fmt.Errorf("audience is too small") - } - - uid, err := strconv.Atoi(audience[0]) - - if err != nil { - return nil, err - } - - user := &dbcore.User{} - userRequest := dbcore.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user) - - if userRequest.Error != nil { - return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error()) - } - - userExists := userRequest.RowsAffected > 0 - - if !userExists { - return user, fmt.Errorf("user does not exist") - } - - return user, nil -} - -func Generate(uid uint) (string, error) { - timeMultiplier := 3 - - if developmentMode { - timeMultiplier = 60 * 24 - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeMultiplier) * time.Minute)), - IssuedAt: jwt.NewNumericDate(time.Now()), - NotBefore: jwt.NewNumericDate(time.Now()), - Audience: []string{strconv.Itoa(int(uid))}, - }) - - signedToken, err := token.SignedString(JWTKey) - - if err != nil { - return "", err - } - - return signedToken, nil -} - -func JWTKeyCallback(*jwt.Token) (interface{}, error) { - return JWTKey, nil -} diff --git a/backend/api/main.go b/backend/api/main.go index c88f1ae..f7ba678 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -9,18 +9,19 @@ import ( "path" "path/filepath" "strings" + "time" "git.terah.dev/imterah/hermes/backend/api/backendruntime" "git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends" "git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies" "git.terah.dev/imterah/hermes/backend/api/controllers/v1/users" - "git.terah.dev/imterah/hermes/backend/api/dbcore" - "git.terah.dev/imterah/hermes/backend/api/jwtcore" + "git.terah.dev/imterah/hermes/backend/api/db" + "git.terah.dev/imterah/hermes/backend/api/jwt" + "git.terah.dev/imterah/hermes/backend/api/state" "git.terah.dev/imterah/hermes/backend/commonbackend" "github.com/charmbracelet/log" "github.com/gin-gonic/gin" "github.com/urfave/cli/v2" - "gorm.io/gorm" ) func apiEntrypoint(cCtx *cli.Context) error { @@ -34,7 +35,26 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Info("Hermes is initializing...") log.Debug("Initializing database and opening it...") - err := dbcore.InitializeDatabase(&gorm.Config{}) + databaseBackendName := os.Getenv("HERMES_DATABASE_BACKEND") + var databaseBackendParams string + + if databaseBackendName == "sqlite" { + databaseBackendParams = os.Getenv("HERMES_SQLITE_FILEPATH") + + if databaseBackendParams == "" { + log.Fatal("HERMES_SQLITE_FILEPATH is not set") + } + } + + if databaseBackendName == "postgres" { + databaseBackendParams = os.Getenv("HERMES_POSTGRES_DSN") + + if databaseBackendParams == "" { + log.Fatal("HERMES_POSTGRES_DSN is not set") + } + } + + dbInstance, err := db.New(databaseBackendName, databaseBackendParams) if err != nil { log.Fatalf("Failed to initialize database: %s", err) @@ -42,16 +62,38 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Debug("Running database migrations...") - if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil { + if err := dbInstance.DoMigrations(); err != nil { return fmt.Errorf("Failed to run database migrations: %s", err) } log.Debug("Initializing the JWT subsystem...") - if err := jwtcore.SetupJWT(); err != nil { - return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error()) + jwtDataString := os.Getenv("HERMES_JWT_SECRET") + var jwtKey []byte + var jwtValidityTimeDuration time.Duration + + if jwtDataString == "" { + log.Fatalf("HERMES_JWT_SECRET is not set") } + if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" { + jwtKey, err = base64.StdEncoding.DecodeString(jwtDataString) + + if err != nil { + log.Fatalf("Failed to decode base64 JWT: %s", err.Error()) + } + } else { + jwtKey = []byte(jwtDataString) + } + + if developmentMode { + jwtValidityTimeDuration = jwt.DevelopmentModeTimings + } else { + jwtValidityTimeDuration = jwt.NormalModeTimings + } + + jwtInstance := jwt.New(jwtKey, dbInstance, jwtValidityTimeDuration) + log.Debug("Initializing the backend subsystem...") backendMetadataPath := cCtx.String("backends-path") @@ -76,9 +118,9 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Debug("Enumerating backends...") - backendList := []dbcore.Backend{} + backendList := []db.Backend{} - if err := dbcore.DB.Find(&backendList).Error; err != nil { + if err := dbInstance.DB.Find(&backendList).Error; err != nil { return fmt.Errorf("Failed to enumerate backends: %s", err.Error()) } @@ -141,9 +183,9 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID) - autoStartProxies := []dbcore.Proxy{} + autoStartProxies := []db.Proxy{} - if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { + if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { log.Errorf("Failed to query proxies to autostart: %s", err.Error()) return } @@ -243,9 +285,9 @@ func apiEntrypoint(cCtx *cli.Context) error { log.Infof("Successfully initialized backend #%d", backend.ID) - autoStartProxies := []dbcore.Proxy{} + autoStartProxies := []db.Proxy{} - if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { + if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil { log.Errorf("Failed to query proxies to autostart: %s", err.Error()) continue } @@ -309,23 +351,25 @@ func apiEntrypoint(cCtx *cli.Context) error { engine.SetTrustedProxies(nil) } + state := state.New(dbInstance, jwtInstance, engine) + // Initialize routes - engine.POST("/api/v1/users/create", users.CreateUser) - engine.POST("/api/v1/users/login", users.LoginUser) - engine.POST("/api/v1/users/refresh", users.RefreshUserToken) - engine.POST("/api/v1/users/remove", users.RemoveUser) - engine.POST("/api/v1/users/lookup", users.LookupUser) + users.SetupCreateUser(state) + users.SetupLoginUser(state) + users.SetupRefreshUserToken(state) + users.SetupRemoveUser(state) + users.SetupLookupUser(state) - engine.POST("/api/v1/backends/create", backends.CreateBackend) - engine.POST("/api/v1/backends/remove", backends.RemoveBackend) - engine.POST("/api/v1/backends/lookup", backends.LookupBackend) + backends.SetupCreateBackend(state) + backends.SetupRemoveBackend(state) + backends.SetupLookupBackend(state) - engine.POST("/api/v1/forward/create", proxies.CreateProxy) - engine.POST("/api/v1/forward/lookup", proxies.LookupProxy) - engine.POST("/api/v1/forward/remove", proxies.RemoveProxy) - engine.POST("/api/v1/forward/start", proxies.StartProxy) - engine.POST("/api/v1/forward/stop", proxies.StopProxy) - engine.POST("/api/v1/forward/connections", proxies.GetConnections) + proxies.SetupCreateProxy(state) + proxies.SetupRemoveProxy(state) + proxies.SetupLookupProxy(state) + proxies.SetupStartProxy(state) + proxies.SetupStopProxy(state) + proxies.SetupGetConnections(state) log.Infof("Listening on '%s'", listeningAddress) err = engine.Run(listeningAddress) @@ -362,22 +406,6 @@ func main() { app := &cli.App{ Name: "hermes", Usage: "port forwarding across boundaries", - Commands: []*cli.Command{ - { - Name: "import", - Usage: "imports from legacy NextNet/Hermes source", - Aliases: []string{"i"}, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "backup-path", - Aliases: []string{"bp"}, - Usage: "path to the backup file", - Required: true, - }, - }, - Action: backupRestoreEntrypoint, - }, - }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "backends-path", diff --git a/backend/api/permissions/permission_nodes.go b/backend/api/permissions/permission_nodes.go index d682d69..f13e80a 100644 --- a/backend/api/permissions/permission_nodes.go +++ b/backend/api/permissions/permission_nodes.go @@ -1,6 +1,6 @@ package permissions -import "git.terah.dev/imterah/hermes/backend/api/dbcore" +import "git.terah.dev/imterah/hermes/backend/api/db" var DefaultPermissionNodes []string = []string{ "routes.add", @@ -27,7 +27,7 @@ var DefaultPermissionNodes []string = []string{ "users.edit", } -func UserHasPermission(user *dbcore.User, node string) bool { +func UserHasPermission(user *db.User, node string) bool { for _, permission := range user.Permissions { if permission.PermissionNode == node && permission.HasPermission { return true diff --git a/backend/api/state/state.go b/backend/api/state/state.go new file mode 100644 index 0000000..754ff56 --- /dev/null +++ b/backend/api/state/state.go @@ -0,0 +1,24 @@ +package state + +import ( + "git.terah.dev/imterah/hermes/backend/api/db" + "git.terah.dev/imterah/hermes/backend/api/jwt" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +type State struct { + DB *db.DB + JWT *jwt.JWTCore + Engine *gin.Engine + Validator *validator.Validate +} + +func New(db *db.DB, jwt *jwt.JWTCore, engine *gin.Engine) *State { + return &State{ + DB: db, + JWT: jwt, + Engine: engine, + Validator: validator.New(), + } +} From b93bf456b55ee3b52c28a6681edec01b3f26f541 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 21 Mar 2025 13:17:08 -0400 Subject: [PATCH 43/45] fix: Fixes 100% CPU usage in the backend runtime This makes the backend runtime not constantly search for messages to be processed. Instead, it only wakes up when it needs to be woken up via goroutines. --- backend/api/backendruntime/runtime.go | 13 +++++++++++++ backend/api/backendruntime/struct.go | 13 ++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/api/backendruntime/runtime.go b/backend/api/backendruntime/runtime.go index 52e280d..8d5ca7a 100644 --- a/backend/api/backendruntime/runtime.go +++ b/backend/api/backendruntime/runtime.go @@ -15,6 +15,9 @@ import ( "github.com/charmbracelet/log" ) +// TODO TODO TODO(imterah): +// This code is a mess. This NEEDS to be rearchitected and refactored to work better. Or at the very least, this code needs to be documented heavily. + func handleCommand(command interface{}, sock net.Conn, rtcChan chan interface{}) error { bytes, err := commonbackend.Marshal(command) @@ -160,6 +163,9 @@ func (runtime *Runtime) goRoutineHandler() error { OuterLoop: for { + _ = <-runtime.startProcessingNotification + runtime.isRuntimeCurrentlyProcessing = true + for chanIndex, messageData := range runtime.messageBuffer { if messageData == nil { continue @@ -177,6 +183,8 @@ func (runtime *Runtime) goRoutineHandler() error { runtime.messageBuffer[chanIndex] = nil } + + runtime.isRuntimeCurrentlyProcessing = false } sock.Close() @@ -235,6 +243,7 @@ func (runtime *Runtime) Start() error { runtime.messageBuffer = make([]*messageForBuf, 10) runtime.messageBufferLock = sync.Mutex{} + runtime.startProcessingNotification = make(chan bool) runtime.processRestartNotification = make(chan bool, 1) runtime.logger = &writeLogger{ @@ -322,6 +331,10 @@ SchedulingLoop: schedulingAttempts++ } + if !runtime.isRuntimeCurrentlyProcessing { + runtime.startProcessingNotification <- true + } + // Fetch response and close Channel response, ok := <-commandChannel diff --git a/backend/api/backendruntime/struct.go b/backend/api/backendruntime/struct.go index cd30182..cd4b3b8 100644 --- a/backend/api/backendruntime/struct.go +++ b/backend/api/backendruntime/struct.go @@ -16,15 +16,18 @@ type Backend struct { type messageForBuf struct { Channel chan interface{} + // TODO(imterah): could this be refactored to just be a []byte instead? Look into this Message interface{} } type Runtime struct { - isRuntimeRunning bool - logger *writeLogger - currentProcess *exec.Cmd - currentListener net.Listener - processRestartNotification chan bool + isRuntimeRunning bool + isRuntimeCurrentlyProcessing bool + startProcessingNotification chan bool + logger *writeLogger + currentProcess *exec.Cmd + currentListener net.Listener + processRestartNotification chan bool messageBufferLock sync.Mutex messageBuffer []*messageForBuf From 75b12f205318f79939340b1d1f4269efd21ae409 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 21 Mar 2025 13:24:59 -0400 Subject: [PATCH 44/45] fix: Avoid recreating validator in SSHBackend and SSHAppBackend --- backend/sshappbackend/local-code/main.go | 15 +++++++++++++-- backend/sshbackend/main.go | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/backend/sshappbackend/local-code/main.go b/backend/sshappbackend/local-code/main.go index cb0c13d..34a85d0 100644 --- a/backend/sshappbackend/local-code/main.go +++ b/backend/sshappbackend/local-code/main.go @@ -26,6 +26,8 @@ import ( "golang.org/x/crypto/ssh" ) +var validatorInstance *validator.Validate + type TCPProxy struct { proxyInformation *commonbackend.AddProxy connections map[uint16]net.Conn @@ -62,6 +64,11 @@ type SSHAppBackend struct { func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { log.Info("SSHAppBackend is initializing...") + + if validatorInstance == nil { + validatorInstance = validator.New() + } + backend.globalNonCriticalMessageChan = make(chan interface{}) backend.tcpProxies = map[uint16]*TCPProxy{} backend.udpProxies = map[uint16]*UDPProxy{} @@ -72,7 +79,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) { return false, err } - if err := validator.New().Struct(&backendData); err != nil { + if err := validatorInstance.Struct(&backendData); err != nil { return false, err } @@ -585,6 +592,10 @@ func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *co func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { var backendData SSHAppBackendData + if validatorInstance == nil { + validatorInstance = validator.New() + } + if err := json.Unmarshal(arguments, &backendData); err != nil { return &commonbackend.CheckParametersResponse{ IsValid: false, @@ -592,7 +603,7 @@ func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commo } } - if err := validator.New().Struct(&backendData); err != nil { + if err := validatorInstance.Struct(&backendData); err != nil { return &commonbackend.CheckParametersResponse{ IsValid: false, Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()), diff --git a/backend/sshbackend/main.go b/backend/sshbackend/main.go index a554fa3..d46b330 100644 --- a/backend/sshbackend/main.go +++ b/backend/sshbackend/main.go @@ -19,6 +19,8 @@ import ( "golang.org/x/crypto/ssh" ) +var validatorInstance *validator.Validate + type ConnWithTimeout struct { net.Conn ReadTimeout time.Duration @@ -76,6 +78,10 @@ type SSHBackend struct { func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { log.Info("SSHBackend is initializing...") + if validatorInstance == nil { + validatorInstance = validator.New() + } + if backend.inReinitLoop { for !backend.isReady { time.Sleep(100 * time.Millisecond) @@ -88,7 +94,7 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) { return false, err } - if err := validator.New().Struct(&backendData); err != nil { + if err := validatorInstance.Struct(&backendData); err != nil { return false, err } @@ -411,6 +417,10 @@ func (backend *SSHBackend) CheckParametersForConnections(clientParameters *commo func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse { var backendData SSHBackendData + if validatorInstance == nil { + validatorInstance = validator.New() + } + if err := json.Unmarshal(arguments, &backendData); err != nil { return &commonbackend.CheckParametersResponse{ IsValid: false, @@ -418,7 +428,7 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba } } - if err := validator.New().Struct(&backendData); err != nil { + if err := validatorInstance.Struct(&backendData); err != nil { return &commonbackend.CheckParametersResponse{ IsValid: false, Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()), From 8e9c7f120fd6732e719fb86f4ed4637909fc99eb Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 21 Mar 2025 13:39:51 -0400 Subject: [PATCH 45/45] fix: Fix regression where Postgres DSN wouldn't be detected There was a typo where databaseBackendName == "postgresql" was "postgres" instead on accident. --- backend/api/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/api/main.go b/backend/api/main.go index f7ba678..1438af8 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -44,14 +44,14 @@ func apiEntrypoint(cCtx *cli.Context) error { if databaseBackendParams == "" { log.Fatal("HERMES_SQLITE_FILEPATH is not set") } - } - - if databaseBackendName == "postgres" { + } else if databaseBackendName == "postgresql" { databaseBackendParams = os.Getenv("HERMES_POSTGRES_DSN") if databaseBackendParams == "" { log.Fatal("HERMES_POSTGRES_DSN is not set") } + } else { + log.Fatalf("Unsupported database backend: %s", databaseBackendName) } dbInstance, err := db.New(databaseBackendName, databaseBackendParams)