Compare commits
No commits in common. "dev" and "sshappbackend" have entirely different histories.
dev
...
sshappback
35 changed files with 2326 additions and 2186 deletions
16
.prettierrc
Normal file
16
.prettierrc
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "always",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
49
CHANGELOG.md
Normal file
49
CHANGELOG.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
# 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)*
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Tera
|
||||
Copyright (c) 2024, Greyson
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
|
|
@ -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 -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
|
||||
sed "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(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`
|
||||
|
|
|
@ -6,10 +6,10 @@ var (
|
|||
AvailableBackends []*Backend
|
||||
RunningBackends map[uint]*Runtime
|
||||
TempDir string
|
||||
shouldLog bool
|
||||
isDevelopmentMode bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
RunningBackends = make(map[uint]*Runtime)
|
||||
shouldLog = os.Getenv("HERMES_DEVELOPMENT_MODE") != "" || os.Getenv("HERMES_BACKEND_LOGGING_ENABLED") != "" || os.Getenv("HERMES_LOG_LEVEL") == "debug"
|
||||
isDevelopmentMode = os.Getenv("HERMES_DEVELOPMENT_MODE") != ""
|
||||
}
|
||||
|
|
|
@ -15,9 +15,6 @@ 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)
|
||||
|
||||
|
@ -163,9 +160,6 @@ func (runtime *Runtime) goRoutineHandler() error {
|
|||
|
||||
OuterLoop:
|
||||
for {
|
||||
_ = <-runtime.startProcessingNotification
|
||||
runtime.isRuntimeCurrentlyProcessing = true
|
||||
|
||||
for chanIndex, messageData := range runtime.messageBuffer {
|
||||
if messageData == nil {
|
||||
continue
|
||||
|
@ -183,8 +177,6 @@ func (runtime *Runtime) goRoutineHandler() error {
|
|||
|
||||
runtime.messageBuffer[chanIndex] = nil
|
||||
}
|
||||
|
||||
runtime.isRuntimeCurrentlyProcessing = false
|
||||
}
|
||||
|
||||
sock.Close()
|
||||
|
@ -243,7 +235,6 @@ 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{
|
||||
|
@ -331,10 +322,6 @@ SchedulingLoop:
|
|||
schedulingAttempts++
|
||||
}
|
||||
|
||||
if !runtime.isRuntimeCurrentlyProcessing {
|
||||
runtime.startProcessingNotification <- true
|
||||
}
|
||||
|
||||
// Fetch response and close Channel
|
||||
response, ok := <-commandChannel
|
||||
|
||||
|
|
|
@ -16,18 +16,15 @@ 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
|
||||
processRestartNotification chan bool
|
||||
isRuntimeRunning bool
|
||||
logger *writeLogger
|
||||
currentProcess *exec.Cmd
|
||||
currentListener net.Listener
|
||||
processRestartNotification chan bool
|
||||
|
||||
messageBufferLock sync.Mutex
|
||||
messageBuffer []*messageForBuf
|
||||
|
@ -45,7 +42,7 @@ type writeLogger struct {
|
|||
func (writer writeLogger) Write(p []byte) (n int, err error) {
|
||||
logSplit := strings.Split(string(p), "\n")
|
||||
|
||||
if shouldLog {
|
||||
if isDevelopmentMode {
|
||||
for _, logLine := range logSplit {
|
||||
if logLine == "" {
|
||||
continue
|
||||
|
|
298
backend/api/backup.go
Normal file
298
backend/api/backup.go
Normal file
|
@ -0,0 +1,298 @@
|
|||
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
|
||||
}
|
|
@ -7,12 +7,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/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 {
|
||||
|
@ -23,114 +24,131 @@ type BackendCreationRequest struct {
|
|||
BackendParameters interface{} `json:"connectionDetails" validate:"required"`
|
||||
}
|
||||
|
||||
func SetupCreateBackend(state *state.State) {
|
||||
state.Engine.POST("/api/v1/backends/create", func(c *gin.Context) {
|
||||
var req BackendCreationRequest
|
||||
func CreateBackend(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 := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
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, "backends.add") {
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var backendParameters []byte
|
||||
|
||||
switch parameters := req.BackendParameters.(type) {
|
||||
case string:
|
||||
backendParameters = []byte(parameters)
|
||||
case map[string]interface{}:
|
||||
backendParameters, err = json.Marshal(parameters)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error())
|
||||
|
||||
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 start backend: %s", err.Error())
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to start backend",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{
|
||||
Arguments: backendParameters,
|
||||
if !permissions.UserHasPermission(user, "backends.add") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var backendParameters []byte
|
||||
|
||||
switch parameters := req.BackendParameters.(type) {
|
||||
case string:
|
||||
backendParameters = []byte(parameters)
|
||||
case map[string]interface{}:
|
||||
backendParameters, err = json.Marshal(parameters)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error())
|
||||
|
||||
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 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)
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
|
@ -145,126 +163,107 @@ func SetupCreateBackend(state *state.State) {
|
|||
return
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
if !responseMessage.IsValid {
|
||||
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",
|
||||
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)
|
||||
}
|
||||
|
||||
backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{
|
||||
Arguments: backendParameters,
|
||||
})
|
||||
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 get response for backend: %s", err.Error())
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add backend into database",
|
||||
})
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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 stop backend: %s", err.Error())
|
||||
log.Warnf("Failed to start backend: %s", err.Error())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
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)
|
||||
}
|
||||
|
||||
switch responseMessage := backendStartResponse.(type) {
|
||||
case *commonbackend.BackendStatusResponse:
|
||||
if !responseMessage.IsRunning {
|
||||
err = backend.Stop()
|
||||
backendruntime.RunningBackends[backendInDatabase.ID] = backend
|
||||
|
||||
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,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,11 +7,12 @@ import (
|
|||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type BackendLookupRequest struct {
|
||||
|
@ -37,80 +38,95 @@ type LookupResponse struct {
|
|||
Data []*SanitizedBackend `json:"data"`
|
||||
}
|
||||
|
||||
func SetupLookupBackend(state *state.State) {
|
||||
state.Engine.POST("/api/v1/backends/lookup", func(c *gin.Context) {
|
||||
var req BackendLookupRequest
|
||||
func LookupBackend(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()),
|
||||
})
|
||||
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 := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
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, "backends.visible") {
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
backends := []db.Backend{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
if !permissions.UserHasPermission(user, "backends.visible") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
if req.BackendID != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.BackendID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
backends := []dbcore.Backend{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
|
||||
if req.Description != nil {
|
||||
queryString = append(queryString, "description = ?")
|
||||
queryParameters = append(queryParameters, req.Description)
|
||||
}
|
||||
if req.BackendID != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.BackendID)
|
||||
}
|
||||
|
||||
if req.Backend != nil {
|
||||
queryString = append(queryString, "is_bot = ?")
|
||||
queryParameters = append(queryParameters, req.Backend)
|
||||
}
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
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())
|
||||
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 := dbcore.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",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedBackends := make([]*SanitizedBackend, len(backends))
|
||||
hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis")
|
||||
|
||||
for backendIndex, backend := range backends {
|
||||
foundBackend, ok := backendruntime.RunningBackends[backend.ID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Failed to get backend #%d controller", backend.ID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get backends",
|
||||
|
@ -119,46 +135,29 @@ func SetupLookupBackend(state *state.State) {
|
|||
return
|
||||
}
|
||||
|
||||
sanitizedBackends := make([]*SanitizedBackend, len(backends))
|
||||
hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis")
|
||||
|
||||
for backendIndex, backend := range backends {
|
||||
foundBackend, ok := backendruntime.RunningBackends[backend.ID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Failed to get backend #%d controller", backend.ID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get backends",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedBackends[backendIndex] = &SanitizedBackend{
|
||||
BackendID: backend.ID,
|
||||
OwnerID: backend.UserID,
|
||||
Name: backend.Name,
|
||||
Description: backend.Description,
|
||||
Backend: backend.Backend,
|
||||
Logs: foundBackend.Logs,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type BackendRemovalRequest struct {
|
||||
|
@ -17,108 +18,106 @@ type BackendRemovalRequest struct {
|
|||
BackendID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
func SetupRemoveBackend(state *state.State) {
|
||||
state.Engine.POST("/api/v1/backends/remove", func(c *gin.Context) {
|
||||
var req BackendRemovalRequest
|
||||
func RemoveBackend(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()),
|
||||
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 err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
if !permissions.UserHasPermission(user, "backends.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
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()
|
||||
|
||||
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.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if backend exists",
|
||||
"error": "Backend deleted, but failed to stop",
|
||||
})
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
delete(backendruntime.RunningBackends, req.BackendID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/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 {
|
||||
|
@ -36,130 +37,127 @@ type ConnectionsResponse struct {
|
|||
Data []*SanitizedConnection `json:"data"`
|
||||
}
|
||||
|
||||
func SetupGetConnections(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/connections", func(c *gin.Context) {
|
||||
var req ConnectionsRequest
|
||||
func GetConnections(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()),
|
||||
})
|
||||
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 := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
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
|
||||
}
|
||||
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.visibleConn") {
|
||||
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": "Missing permissions",
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
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())
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find forward entry",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
if !permissions.UserHasPermission(user, "routes.visibleConn") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "No forward entry found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
var proxy dbcore.Proxy
|
||||
proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy)
|
||||
|
||||
backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error())
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find forward entry",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{})
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "No forward entry found",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyConnectionsResponse:
|
||||
sanitizedConnections := []*SanitizedConnection{}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
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,
|
||||
return
|
||||
}
|
||||
|
||||
ConnectionDetails: &ConnectionDetailsForConnection{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
},
|
||||
})
|
||||
}
|
||||
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",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/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 {
|
||||
|
@ -25,153 +26,150 @@ type ProxyCreationRequest struct {
|
|||
AutoStart *bool `json:"autoStart"`
|
||||
}
|
||||
|
||||
func SetupCreateProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/create", func(c *gin.Context) {
|
||||
var req ProxyCreationRequest
|
||||
func CreateProxy(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()),
|
||||
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,
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
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 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())
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.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",
|
||||
"error": "failed to get response from backend",
|
||||
})
|
||||
|
||||
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)
|
||||
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,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"id": proxy.ID,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ProxyLookupRequest struct {
|
||||
|
@ -42,143 +43,141 @@ type ProxyLookupResponse struct {
|
|||
Data []*SanitizedProxy `json:"data"`
|
||||
}
|
||||
|
||||
func SetupLookupProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/lookup", func(c *gin.Context) {
|
||||
var req ProxyLookupRequest
|
||||
func LookupProxy(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()),
|
||||
})
|
||||
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 := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
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.visible") {
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
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())
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get proxies",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
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,
|
||||
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 := []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",
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/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 {
|
||||
|
@ -18,133 +19,134 @@ type ProxyRemovalRequest struct {
|
|||
ID uint `validate:"required" json:"id"`
|
||||
}
|
||||
|
||||
func SetupRemoveProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/remove", func(c *gin.Context) {
|
||||
var req ProxyRemovalRequest
|
||||
func RemoveProxy(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()),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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 := 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,
|
||||
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 != nil {
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, 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 get response from backend. Proxy was still successfully deleted",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
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)
|
||||
if !permissions.UserHasPermission(user, "routes.remove") {
|
||||
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
|
||||
}
|
||||
|
||||
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": "Got invalid response from backend. Proxy was still successfully deleted",
|
||||
"error": "Failed to stop proxy. Proxy was still successfully deleted",
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/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 {
|
||||
|
@ -18,119 +19,124 @@ type ProxyStartRequest struct {
|
|||
ID uint `validate:"required" json:"id"`
|
||||
}
|
||||
|
||||
func SetupStartProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/start", func(c *gin.Context) {
|
||||
var req ProxyStartRequest
|
||||
func StartProxy(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()),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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,
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case error:
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.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 get response from backend",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
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",
|
||||
})
|
||||
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",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/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 {
|
||||
|
@ -18,119 +19,124 @@ type ProxyStopRequest struct {
|
|||
ID uint `validate:"required" json:"id"`
|
||||
}
|
||||
|
||||
func SetupStopProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/stop", func(c *gin.Context) {
|
||||
var req ProxyStartRequest
|
||||
func StopProxy(c *gin.Context) {
|
||||
var req ProxyStopRequest
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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 := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case error:
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.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 get response from backend",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
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",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.stop") {
|
||||
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.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",
|
||||
})
|
||||
|
||||
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",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -20,141 +22,142 @@ type UserCreationRequest struct {
|
|||
Email string `validate:"required"`
|
||||
Password string `validate:"required"`
|
||||
Username string `validate:"required"`
|
||||
IsBot bool
|
||||
|
||||
// TODO: implement support
|
||||
ExistingUserToken string `json:"token"`
|
||||
IsBot bool
|
||||
}
|
||||
|
||||
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 := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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 := 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",
|
||||
})
|
||||
|
||||
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),
|
||||
func CreateUser(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
|
||||
}
|
||||
|
||||
permissions = append(permissions, dbcore.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 = &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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if result := dbcore.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",
|
||||
})
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,10 +6,11 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
|
@ -20,139 +21,137 @@ type UserLoginRequest struct {
|
|||
Password string `validate:"required"`
|
||||
}
|
||||
|
||||
func SetupLoginUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/login", func(c *gin.Context) {
|
||||
var req UserLoginRequest
|
||||
func LoginUser(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()),
|
||||
})
|
||||
|
||||
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),
|
||||
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
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type UserLookupRequest struct {
|
||||
|
@ -34,104 +35,102 @@ type LookupResponse struct {
|
|||
Data []*SanitizedUsers `json:"data"`
|
||||
}
|
||||
|
||||
func SetupLookupUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/lookup", func(c *gin.Context) {
|
||||
var req UserLookupRequest
|
||||
func LookupUser(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()),
|
||||
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
|
||||
}
|
||||
|
||||
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())
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get users",
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sanitizedUsers := make([]*SanitizedUsers, len(users))
|
||||
users := []dbcore.User{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
|
||||
for userIndex, user := range users {
|
||||
isBot := false
|
||||
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 user.IsBot != nil {
|
||||
isBot = *user.IsBot
|
||||
}
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
sanitizedUsers[userIndex] = &SanitizedUsers{
|
||||
UID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
IsBot: isBot,
|
||||
}
|
||||
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 := 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",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedUsers := make([]*SanitizedUsers, len(users))
|
||||
|
||||
for userIndex, user := range users {
|
||||
isBot := false
|
||||
|
||||
if user.IsBot != nil {
|
||||
isBot = *user.IsBot
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &LookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedUsers,
|
||||
})
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,114 +5,113 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type UserRefreshRequest struct {
|
||||
Token string `validate:"required"`
|
||||
}
|
||||
|
||||
func SetupRefreshUserToken(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/refresh", func(c *gin.Context) {
|
||||
var req UserRefreshRequest
|
||||
func RefreshUserToken(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()),
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
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 := 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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,11 +4,12 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type UserRemovalRequest struct {
|
||||
|
@ -16,91 +17,89 @@ type UserRemovalRequest struct {
|
|||
UID *uint `json:"uid"`
|
||||
}
|
||||
|
||||
func SetupRemoveUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/remove", func(c *gin.Context) {
|
||||
var req UserRemovalRequest
|
||||
func RemoveUser(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 := 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
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
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))
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
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
|
||||
}
|
142
backend/api/dbcore/db.go
Normal file
142
backend/api/dbcore/db.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
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
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
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
|
||||
}
|
117
backend/api/jwtcore/jwt.go
Normal file
117
backend/api/jwtcore/jwt.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
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
|
||||
}
|
|
@ -9,19 +9,18 @@ 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/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwt"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func apiEntrypoint(cCtx *cli.Context) error {
|
||||
|
@ -35,26 +34,7 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
log.Info("Hermes is initializing...")
|
||||
log.Debug("Initializing database and opening it...")
|
||||
|
||||
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)
|
||||
err := dbcore.InitializeDatabase(&gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %s", err)
|
||||
|
@ -62,38 +42,16 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Debug("Running database migrations...")
|
||||
|
||||
if err := dbInstance.DoMigrations(); err != nil {
|
||||
if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil {
|
||||
return fmt.Errorf("Failed to run database migrations: %s", err)
|
||||
}
|
||||
|
||||
log.Debug("Initializing the JWT subsystem...")
|
||||
|
||||
jwtDataString := os.Getenv("HERMES_JWT_SECRET")
|
||||
var jwtKey []byte
|
||||
var jwtValidityTimeDuration time.Duration
|
||||
|
||||
if jwtDataString == "" {
|
||||
log.Fatalf("HERMES_JWT_SECRET is not set")
|
||||
if err := jwtcore.SetupJWT(); err != nil {
|
||||
return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error())
|
||||
}
|
||||
|
||||
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")
|
||||
|
@ -118,9 +76,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Debug("Enumerating backends...")
|
||||
|
||||
backendList := []db.Backend{}
|
||||
backendList := []dbcore.Backend{}
|
||||
|
||||
if err := dbInstance.DB.Find(&backendList).Error; err != nil {
|
||||
if err := dbcore.DB.Find(&backendList).Error; err != nil {
|
||||
return fmt.Errorf("Failed to enumerate backends: %s", err.Error())
|
||||
}
|
||||
|
||||
|
@ -183,9 +141,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID)
|
||||
|
||||
autoStartProxies := []db.Proxy{}
|
||||
autoStartProxies := []dbcore.Proxy{}
|
||||
|
||||
if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -285,9 +243,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Infof("Successfully initialized backend #%d", backend.ID)
|
||||
|
||||
autoStartProxies := []db.Proxy{}
|
||||
autoStartProxies := []dbcore.Proxy{}
|
||||
|
||||
if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
@ -351,25 +309,23 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
engine.SetTrustedProxies(nil)
|
||||
}
|
||||
|
||||
state := state.New(dbInstance, jwtInstance, engine)
|
||||
|
||||
// Initialize routes
|
||||
users.SetupCreateUser(state)
|
||||
users.SetupLoginUser(state)
|
||||
users.SetupRefreshUserToken(state)
|
||||
users.SetupRemoveUser(state)
|
||||
users.SetupLookupUser(state)
|
||||
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)
|
||||
|
||||
backends.SetupCreateBackend(state)
|
||||
backends.SetupRemoveBackend(state)
|
||||
backends.SetupLookupBackend(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)
|
||||
|
||||
proxies.SetupCreateProxy(state)
|
||||
proxies.SetupRemoveProxy(state)
|
||||
proxies.SetupLookupProxy(state)
|
||||
proxies.SetupStartProxy(state)
|
||||
proxies.SetupStopProxy(state)
|
||||
proxies.SetupGetConnections(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)
|
||||
|
||||
log.Infof("Listening on '%s'", listeningAddress)
|
||||
err = engine.Run(listeningAddress)
|
||||
|
@ -406,6 +362,22 @@ 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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package permissions
|
||||
|
||||
import "git.terah.dev/imterah/hermes/backend/api/db"
|
||||
import "git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
|
||||
var DefaultPermissionNodes []string = []string{
|
||||
"routes.add",
|
||||
|
@ -27,7 +27,7 @@ var DefaultPermissionNodes []string = []string{
|
|||
"users.edit",
|
||||
}
|
||||
|
||||
func UserHasPermission(user *db.User, node string) bool {
|
||||
func UserHasPermission(user *dbcore.User, node string) bool {
|
||||
for _, permission := range user.Permissions {
|
||||
if permission.PermissionNode == node && permission.HasPermission {
|
||||
return true
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
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(),
|
||||
}
|
||||
}
|
|
@ -26,8 +26,6 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var validatorInstance *validator.Validate
|
||||
|
||||
type TCPProxy struct {
|
||||
proxyInformation *commonbackend.AddProxy
|
||||
connections map[uint16]net.Conn
|
||||
|
@ -64,11 +62,6 @@ 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{}
|
||||
|
@ -79,7 +72,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -592,10 +585,6 @@ 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,
|
||||
|
@ -603,7 +592,7 @@ func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commo
|
|||
}
|
||||
}
|
||||
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
return &commonbackend.CheckParametersResponse{
|
||||
IsValid: false,
|
||||
Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()),
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -19,34 +18,6 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var validatorInstance *validator.Validate
|
||||
|
||||
type ConnWithTimeout struct {
|
||||
net.Conn
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c *ConnWithTimeout) Read(b []byte) (int, error) {
|
||||
err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (c *ConnWithTimeout) Write(b []byte) (int, error) {
|
||||
err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
type SSHListener struct {
|
||||
SourceIP string
|
||||
SourcePort uint16
|
||||
|
@ -55,46 +26,31 @@ type SSHListener struct {
|
|||
Listeners []net.Listener
|
||||
}
|
||||
|
||||
type SSHBackendData struct {
|
||||
IP string `json:"ip" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
PrivateKey string `json:"privateKey" validate:"required"`
|
||||
DisablePIDCheck bool `json:"disablePIDCheck"`
|
||||
ListenOnIPs []string `json:"listenOnIPs"`
|
||||
}
|
||||
|
||||
type SSHBackend struct {
|
||||
config *SSHBackendData
|
||||
conn *ssh.Client
|
||||
clients []*commonbackend.ProxyClientConnection
|
||||
proxies []*SSHListener
|
||||
arrayPropMutex sync.Mutex
|
||||
pid int
|
||||
isReady bool
|
||||
inReinitLoop bool
|
||||
}
|
||||
|
||||
type SSHBackendData struct {
|
||||
IP string `json:"ip" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
PrivateKey string `json:"privateKey" validate:"required"`
|
||||
ListenOnIPs []string `json:"listenOnIPs"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
var backendData SSHBackendData
|
||||
|
||||
if err := json.Unmarshal(bytes, &backendData); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -120,70 +76,16 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) {
|
|||
},
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", backendData.IP, backendData.Port)
|
||||
timeout := time.Duration(10 * time.Second)
|
||||
|
||||
rawTCPConn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
connWithTimeout := &ConnWithTimeout{
|
||||
Conn: rawTCPConn,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
}
|
||||
|
||||
c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := ssh.NewClient(c, chans, reqs)
|
||||
backend.conn = client
|
||||
|
||||
if !backendData.DisablePIDCheck {
|
||||
if backend.pid != 0 {
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid))
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to kill process: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Get the parent PID of the shell so we can kill it if we disconnect
|
||||
output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'")
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Strip the new line and convert to int
|
||||
backend.pid, err = strconv.Atoi(string(output)[:len(output)-1])
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
go backend.backendDisconnectHandler()
|
||||
go backend.backendKeepaliveHandler()
|
||||
backend.conn = conn
|
||||
|
||||
log.Info("SSHBackend has initialized successfully.")
|
||||
go backend.backendDisconnectHandler()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
@ -301,7 +203,8 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er
|
|||
// Splice out the clientInstance by clientIndex
|
||||
|
||||
// TODO: change approach. It works but it's a bit wonky imho
|
||||
backend.clients = slices.Delete(backend.clients, clientIndex, clientIndex+1)
|
||||
// I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively
|
||||
backend.clients = append(backend.clients[:clientIndex], backend.clients[clientIndex+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -385,8 +288,10 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool,
|
|||
}
|
||||
|
||||
// Splice out the proxy instance by proxyIndex
|
||||
|
||||
// TODO: change approach. It works but it's a bit wonky imho
|
||||
backend.proxies = slices.Delete(backend.proxies, proxyIndex, proxyIndex+1)
|
||||
// I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively
|
||||
backend.proxies = append(backend.proxies[:proxyIndex], backend.proxies[proxyIndex+1:]...)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
@ -417,10 +322,6 @@ 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,
|
||||
|
@ -428,7 +329,7 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba
|
|||
}
|
||||
}
|
||||
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
return &commonbackend.CheckParametersResponse{
|
||||
IsValid: false,
|
||||
Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()),
|
||||
|
@ -440,35 +341,18 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba
|
|||
}
|
||||
}
|
||||
|
||||
func (backend *SSHBackend) backendKeepaliveHandler() {
|
||||
for {
|
||||
if backend.conn != nil {
|
||||
_, _, err := backend.conn.SendRequest("keepalive@openssh.com", true, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Warn("Keepalive message failed!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *SSHBackend) backendDisconnectHandler() {
|
||||
for {
|
||||
if backend.conn != nil {
|
||||
backend.conn.Wait()
|
||||
backend.conn.Close()
|
||||
err := backend.conn.Wait()
|
||||
|
||||
backend.isReady = false
|
||||
backend.inReinitLoop = true
|
||||
|
||||
log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...")
|
||||
} else {
|
||||
log.Info("Retrying reconnection in 5 seconds...")
|
||||
if err == nil || err.Error() != "EOF" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...")
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Make the connection nil to accurately report our status incase GetBackendStatus is called
|
||||
|
@ -492,74 +376,14 @@ func (backend *SSHBackend) backendDisconnectHandler() {
|
|||
},
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port)
|
||||
timeout := time.Duration(10 * time.Second)
|
||||
|
||||
rawTCPConn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port), config)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to establish connection to the server: %s", err.Error())
|
||||
continue
|
||||
log.Errorf("Failed to connect to the server: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
connWithTimeout := &ConnWithTimeout{
|
||||
Conn: rawTCPConn,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
}
|
||||
|
||||
c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create SSH client connection: %s", err.Error())
|
||||
rawTCPConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
client := ssh.NewClient(c, chans, reqs)
|
||||
backend.conn = client
|
||||
|
||||
if !backend.config.DisablePIDCheck {
|
||||
if backend.pid != 0 {
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to create SSH command session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid))
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to kill process: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to create SSH command session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get the parent PID of the shell so we can kill it if we disconnect
|
||||
output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'")
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to execute command to fetch PID: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Strip the new line and convert to int
|
||||
backend.pid, err = strconv.Atoi(string(output)[:len(output)-1])
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse PID: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go backend.backendKeepaliveHandler()
|
||||
backend.conn = conn
|
||||
|
||||
log.Info("SSHBackend has reconnected successfully. Attempting to set up proxies again...")
|
||||
|
||||
|
|
|
@ -6,12 +6,27 @@ 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
|
||||
|
@ -22,6 +37,7 @@ services:
|
|||
POSTGRES_USER: ${POSTGRES_USERNAME}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
ssh_key_data:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# These are default values, please change these!
|
||||
|
||||
POSTGRES_USERNAME=hermes
|
||||
POSTGRES_PASSWORD=hermes
|
||||
POSTGRES_DB=hermes
|
||||
JWT_SECRET=hermes
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
}: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# api/
|
||||
nodejs
|
||||
go
|
||||
gopls
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue