Compare commits

..

8 commits
v2.2.2 ... dev

Author SHA1 Message Date
8e9c7f120f
fix: Fix regression where Postgres DSN wouldn't be detected
All checks were successful
Release code / build (push) Successful in 5m47s
There was a typo where databaseBackendName == "postgresql" was
"postgres" instead on accident.
2025-03-21 13:39:51 -04:00
75b12f2053
fix: Avoid recreating validator in SSHBackend and SSHAppBackend
All checks were successful
Release code / build (push) Successful in 5m46s
2025-03-21 13:24:59 -04:00
b93bf456b5
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.
2025-03-21 13:17:08 -04:00
d56a8eb7bf
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.
2025-03-21 12:59:51 -04:00
71d53990de
chore: Fix sample code to remove the deprecated LOM, and add JWT secrets 2025-03-18 20:51:01 -04:00
1cefe64f88
chore: Remove Node.JS from the nix shell 2025-03-18 20:36:20 -04:00
83f80af405 chore: Delete unmaintained CHANGELOG 2025-03-19 00:34:36 +00:00
7dee159d5f
chore: Change license name 2025-03-18 20:33:59 -04:00
33 changed files with 1951 additions and 2241 deletions

View file

@ -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)*

View file

@ -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:

View file

@ -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`

View file

@ -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

View file

@ -16,11 +16,14 @@ 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
isRuntimeCurrentlyProcessing bool
startProcessingNotification chan bool
logger *writeLogger
currentProcess *exec.Cmd
currentListener net.Listener

View file

@ -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
}

View file

@ -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,7 +23,8 @@ type BackendCreationRequest struct {
BackendParameters interface{} `json:"connectionDetails" validate:"required"`
}
func CreateBackend(c *gin.Context) {
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 {
@ -35,7 +35,7 @@ func CreateBackend(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -43,7 +43,7 @@ func CreateBackend(c *gin.Context) {
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" {
@ -190,7 +190,7 @@ func CreateBackend(c *gin.Context) {
log.Info("Passed backend checks successfully")
backendInDatabase := &dbcore.Backend{
backendInDatabase := &db.Backend{
UserID: user.ID,
Name: req.Name,
Description: req.Description,
@ -198,7 +198,7 @@ func CreateBackend(c *gin.Context) {
BackendParameters: base64.StdEncoding.EncodeToString(backendParameters),
}
if result := dbcore.DB.Create(&backendInDatabase); result.Error != nil {
if result := state.DB.DB.Create(&backendInDatabase); result.Error != nil {
log.Warnf("Failed to create backend: %s", result.Error.Error())
err = backend.Stop()
@ -266,4 +266,5 @@ func CreateBackend(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
}

View file

@ -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,7 +37,8 @@ type LookupResponse struct {
Data []*SanitizedBackend `json:"data"`
}
func LookupBackend(c *gin.Context) {
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 {
@ -49,7 +49,7 @@ func LookupBackend(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -57,7 +57,7 @@ func LookupBackend(c *gin.Context) {
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" {
@ -85,7 +85,7 @@ func LookupBackend(c *gin.Context) {
return
}
backends := []dbcore.Backend{}
backends := []db.Backend{}
queryString := []string{}
queryParameters := []interface{}{}
@ -109,7 +109,7 @@ func LookupBackend(c *gin.Context) {
queryParameters = append(queryParameters, req.Backend)
}
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil {
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{
@ -160,4 +160,5 @@ func LookupBackend(c *gin.Context) {
Success: true,
Data: sanitizedBackends,
})
})
}

View file

@ -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,7 +17,8 @@ type BackendRemovalRequest struct {
BackendID uint `json:"id" validate:"required"`
}
func RemoveBackend(c *gin.Context) {
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 {
@ -29,7 +29,7 @@ func RemoveBackend(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -37,7 +37,7 @@ func RemoveBackend(c *gin.Context) {
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" {
@ -65,8 +65,8 @@ func RemoveBackend(c *gin.Context) {
return
}
var backend *dbcore.Backend
backendRequest := dbcore.DB.Where("id = ?", req.BackendID).Find(&backend)
var backend *db.Backend
backendRequest := state.DB.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())
@ -88,7 +88,7 @@ func RemoveBackend(c *gin.Context) {
return
}
if err := dbcore.DB.Delete(backend).Error; err != nil {
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{
@ -120,4 +120,5 @@ func RemoveBackend(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
}

View file

@ -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,7 +36,8 @@ type ConnectionsResponse struct {
Data []*SanitizedConnection `json:"data"`
}
func GetConnections(c *gin.Context) {
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 {
@ -48,7 +48,7 @@ func GetConnections(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -56,7 +56,8 @@ func GetConnections(c *gin.Context) {
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" {
c.JSON(http.StatusForbidden, gin.H{
@ -83,8 +84,8 @@ func GetConnections(c *gin.Context) {
return
}
var proxy dbcore.Proxy
proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy)
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())
@ -160,4 +161,5 @@ func GetConnections(c *gin.Context) {
"error": "Got illegal response type",
})
}
})
}

View file

@ -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,7 +25,8 @@ type ProxyCreationRequest struct {
AutoStart *bool `json:"autoStart"`
}
func CreateProxy(c *gin.Context) {
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 {
@ -37,7 +37,7 @@ func CreateProxy(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -45,7 +45,8 @@ func CreateProxy(c *gin.Context) {
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" {
c.JSON(http.StatusForbidden, gin.H{
@ -80,8 +81,8 @@ func CreateProxy(c *gin.Context) {
return
}
var backend dbcore.Backend
backendRequest := dbcore.DB.Where("id = ?", req.ProviderID).First(&backend)
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())
@ -105,7 +106,7 @@ func CreateProxy(c *gin.Context) {
autoStart = *req.AutoStart
}
proxy := &dbcore.Proxy{
proxy := &db.Proxy{
UserID: user.ID,
BackendID: req.ProviderID,
Name: req.Name,
@ -117,7 +118,7 @@ func CreateProxy(c *gin.Context) {
AutoStart: autoStart,
}
if result := dbcore.DB.Create(proxy); result.Error != nil {
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{
@ -172,4 +173,5 @@ func CreateProxy(c *gin.Context) {
"success": true,
"id": proxy.ID,
})
})
}

View file

@ -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,7 +42,8 @@ type ProxyLookupResponse struct {
Data []*SanitizedProxy `json:"data"`
}
func LookupProxy(c *gin.Context) {
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 {
@ -54,7 +54,7 @@ func LookupProxy(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -62,7 +62,7 @@ func LookupProxy(c *gin.Context) {
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" {
@ -100,7 +100,7 @@ func LookupProxy(c *gin.Context) {
}
}
proxies := []dbcore.Proxy{}
proxies := []db.Proxy{}
queryString := []string{}
queryParameters := []interface{}{}
@ -150,7 +150,7 @@ func LookupProxy(c *gin.Context) {
queryParameters = append(queryParameters, req.Protocol)
}
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil {
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{
@ -180,4 +180,5 @@ func LookupProxy(c *gin.Context) {
Success: true,
Data: sanitizedProxies,
})
})
}

View file

@ -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,7 +18,8 @@ type ProxyRemovalRequest struct {
ID uint `validate:"required" json:"id"`
}
func RemoveProxy(c *gin.Context) {
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 {
@ -30,7 +30,7 @@ func RemoveProxy(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -38,7 +38,8 @@ func RemoveProxy(c *gin.Context) {
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" {
c.JSON(http.StatusForbidden, gin.H{
@ -65,8 +66,8 @@ func RemoveProxy(c *gin.Context) {
return
}
var proxy *dbcore.Proxy
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
var proxy *db.Proxy
proxyRequest := state.DB.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())
@ -88,7 +89,7 @@ func RemoveProxy(c *gin.Context) {
return
}
if err := dbcore.DB.Delete(proxy).Error; err != nil {
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{
@ -133,8 +134,10 @@ func RemoveProxy(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to stop proxy. Proxy was still successfully deleted",
})
return
} else {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}
default:
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
@ -142,11 +145,6 @@ func RemoveProxy(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Got invalid response from backend. Proxy was still successfully deleted",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}

View file

@ -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,7 +18,8 @@ type ProxyStartRequest struct {
ID uint `validate:"required" json:"id"`
}
func StartProxy(c *gin.Context) {
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 {
@ -30,7 +30,7 @@ func StartProxy(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -38,7 +38,8 @@ func StartProxy(c *gin.Context) {
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" {
c.JSON(http.StatusForbidden, gin.H{
@ -65,8 +66,8 @@ func StartProxy(c *gin.Context) {
return
}
var proxy *dbcore.Proxy
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
var proxy *db.Proxy
proxyRequest := state.DB.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())
@ -114,29 +115,22 @@ func StartProxy(c *gin.Context) {
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",
})
return
} else {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}
break
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",
"error": "Got invalid response from backend. Proxy was likely still successfully started",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}

View file

@ -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,8 +18,9 @@ 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{
@ -30,7 +30,7 @@ func StopProxy(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -38,7 +38,8 @@ func StopProxy(c *gin.Context) {
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" {
c.JSON(http.StatusForbidden, gin.H{
@ -65,8 +66,8 @@ func StopProxy(c *gin.Context) {
return
}
var proxy *dbcore.Proxy
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
var proxy *db.Proxy
proxyRequest := state.DB.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())
@ -107,36 +108,29 @@ func StopProxy(c *gin.Context) {
Protocol: proxy.Protocol,
})
if err != nil {
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
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
}
switch responseMessage := backendResponse.(type) {
case *commonbackend.ProxyStatusResponse:
if responseMessage.IsActive {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to stop proxy",
})
return
} 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",
"error": "Got invalid response from backend. Proxy was likely still successfully stopped",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
})
}

View file

@ -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,13 +20,11 @@ type UserCreationRequest struct {
Email string `validate:"required"`
Password string `validate:"required"`
Username string `validate:"required"`
// TODO: implement support
ExistingUserToken string `json:"token"`
IsBot bool
}
func CreateUser(c *gin.Context) {
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.",
@ -47,7 +43,7 @@ func CreateUser(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -55,8 +51,8 @@ func CreateUser(c *gin.Context) {
return
}
var user *dbcore.User
userRequest := dbcore.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user)
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())
@ -90,7 +86,7 @@ func CreateUser(c *gin.Context) {
return
}
permissions := []dbcore.Permission{}
permissions := []db.Permission{}
for _, permission := range permissionHelper.DefaultPermissionNodes {
permissionEnabledState := false
@ -99,7 +95,7 @@ func CreateUser(c *gin.Context) {
permissionEnabledState = true
}
permissions = append(permissions, dbcore.Permission{
permissions = append(permissions, db.Permission{
PermissionNode: permission,
HasPermission: permissionEnabledState,
})
@ -117,14 +113,14 @@ func CreateUser(c *gin.Context) {
return
}
user = &dbcore.User{
user = &db.User{
Email: req.Email,
Username: req.Username,
Name: req.Name,
IsBot: &req.IsBot,
Password: base64.StdEncoding.EncodeToString(passwordHashed),
Permissions: permissions,
Tokens: []dbcore.Token{
Tokens: []db.Token{
{
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
DisableExpiry: forceNoExpiryTokens,
@ -133,7 +129,7 @@ func CreateUser(c *gin.Context) {
},
}
if result := dbcore.DB.Create(&user); result.Error != nil {
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{
@ -143,7 +139,7 @@ func CreateUser(c *gin.Context) {
return
}
jwt, err := jwtcore.Generate(user.ID)
jwt, err := state.JWT.Generate(user.ID)
if err != nil {
log.Warnf("Failed to generate JWT: %s", err.Error())
@ -160,4 +156,5 @@ func CreateUser(c *gin.Context) {
"token": jwt,
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
})
})
}

View file

@ -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,7 +20,8 @@ type UserLoginRequest struct {
Password string `validate:"required"`
}
func LoginUser(c *gin.Context) {
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 {
@ -32,7 +32,7 @@ func LoginUser(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -61,8 +61,8 @@ func LoginUser(c *gin.Context) {
userFindRequest += "username = ?"
}
var user *dbcore.User
userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user)
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())
@ -119,7 +119,7 @@ func LoginUser(c *gin.Context) {
return
}
token := &dbcore.Token{
token := &db.Token{
UserID: user.ID,
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
@ -127,7 +127,7 @@ func LoginUser(c *gin.Context) {
CreationIPAddr: c.ClientIP(),
}
if result := dbcore.DB.Create(&token); result.Error != nil {
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{
@ -137,7 +137,7 @@ func LoginUser(c *gin.Context) {
return
}
jwt, err := jwtcore.Generate(user.ID)
jwt, err := state.JWT.Generate(user.ID)
if err != nil {
log.Warnf("Failed to generate JWT: %s", err.Error())
@ -154,4 +154,5 @@ func LoginUser(c *gin.Context) {
"token": jwt,
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
})
})
}

View file

@ -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,7 +34,8 @@ type LookupResponse struct {
Data []*SanitizedUsers `json:"data"`
}
func LookupUser(c *gin.Context) {
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 {
@ -46,7 +46,7 @@ func LookupUser(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -54,7 +54,7 @@ func LookupUser(c *gin.Context) {
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" {
@ -74,7 +74,7 @@ func LookupUser(c *gin.Context) {
}
}
users := []dbcore.User{}
users := []db.User{}
queryString := []string{}
queryParameters := []interface{}{}
@ -101,7 +101,7 @@ func LookupUser(c *gin.Context) {
queryParameters = append(queryParameters, req.IsBot)
}
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil {
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{
@ -133,4 +133,5 @@ func LookupUser(c *gin.Context) {
Success: true,
Data: sanitizedUsers,
})
})
}

View file

@ -5,18 +5,18 @@ 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) {
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 {
@ -27,7 +27,7 @@ func RefreshUserToken(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -35,8 +35,8 @@ func RefreshUserToken(c *gin.Context) {
return
}
var tokenInDatabase *dbcore.Token
tokenRequest := dbcore.DB.Where("token = ?", req.Token).Find(&tokenInDatabase)
var tokenInDatabase *db.Token
tokenRequest := state.DB.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())
@ -65,7 +65,7 @@ func RefreshUserToken(c *gin.Context) {
"error": "Token has expired",
})
tx := dbcore.DB.Delete(tokenInDatabase)
tx := state.DB.DB.Delete(tokenInDatabase)
if tx.Error != nil {
log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error())
@ -75,8 +75,8 @@ func RefreshUserToken(c *gin.Context) {
}
// 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)
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())
@ -98,7 +98,7 @@ func RefreshUserToken(c *gin.Context) {
return
}
jwt, err := jwtcore.Generate(user.ID)
jwt, err := state.JWT.Generate(user.ID)
if err != nil {
log.Warnf("Failed to generate JWT: %s", err.Error())
@ -114,4 +114,5 @@ func RefreshUserToken(c *gin.Context) {
"success": true,
"token": jwt,
})
})
}

View file

@ -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,7 +16,8 @@ type UserRemovalRequest struct {
UID *uint `json:"uid"`
}
func RemoveUser(c *gin.Context) {
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 {
@ -28,7 +28,7 @@ func RemoveUser(c *gin.Context) {
return
}
if err := validator.New().Struct(&req); err != nil {
if err := state.Validator.Struct(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
})
@ -36,7 +36,7 @@ func RemoveUser(c *gin.Context) {
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" {
@ -73,8 +73,8 @@ func RemoveUser(c *gin.Context) {
// 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)
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())
@ -97,9 +97,10 @@ func RemoveUser(c *gin.Context) {
}
}
dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
state.DB.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
}

77
backend/api/db/db.go Normal file
View file

@ -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))
}
}

66
backend/api/db/models.go Normal file
View file

@ -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
}

View file

@ -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
}

107
backend/api/jwt/jwt.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}
} 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)
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",

View file

@ -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

View file

@ -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(),
}
}

View file

@ -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()),

View file

@ -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()),

View file

@ -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:

View file

@ -1,5 +1,4 @@
# These are default values, please change these!
POSTGRES_USERNAME=hermes
POSTGRES_PASSWORD=hermes
POSTGRES_DB=hermes
JWT_SECRET=hermes

View file

@ -3,7 +3,6 @@
}: pkgs.mkShell {
buildInputs = with pkgs; [
# api/
nodejs
go
gopls
];