From 7dee159d5f46f0e4baf2b915b45f3167882ecbab Mon Sep 17 00:00:00 2001 From: imterah Date: Tue, 18 Mar 2025 20:33:59 -0400 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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)