Compare commits
12 commits
sshappback
...
dev
Author | SHA1 | Date | |
---|---|---|---|
8e9c7f120f | |||
75b12f2053 | |||
b93bf456b5 | |||
d56a8eb7bf | |||
71d53990de | |||
1cefe64f88 | |||
83f80af405 | |||
7dee159d5f | |||
17b10c9b19 | |||
5c503f0421 | |||
959718163e | |||
24b165c9bb |
35 changed files with 2145 additions and 2285 deletions
16
.prettierrc
16
.prettierrc
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "always",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
49
CHANGELOG.md
49
CHANGELOG.md
|
@ -1,49 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
## [v1.1.2](https://github.com/imterah/nextnet/tree/v1.1.2) (2024-09-29)
|
||||
|
||||
## [v1.1.1](https://github.com/imterah/nextnet/tree/v1.1.1) (2024-09-29)
|
||||
|
||||
## [v1.1.0](https://github.com/imterah/nextnet/tree/v1.1.0) (2024-09-22)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Desktop app fails to build on macOS w/ `nix-shell` [\#1](https://github.com/imterah/nextnet/issues/1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- chore\(deps\): bump find-my-way from 8.1.0 to 8.2.2 in /api [\#17](https://github.com/imterah/nextnet/pull/17)
|
||||
- chore\(deps\): bump axios from 1.6.8 to 1.7.4 in /lom [\#16](https://github.com/imterah/nextnet/pull/16)
|
||||
- chore\(deps\): bump micromatch from 4.0.5 to 4.0.8 in /lom [\#15](https://github.com/imterah/nextnet/pull/15)
|
||||
- chore\(deps\): bump braces from 3.0.2 to 3.0.3 in /lom [\#13](https://github.com/imterah/nextnet/pull/13)
|
||||
- chore\(deps-dev\): bump braces from 3.0.2 to 3.0.3 in /api [\#11](https://github.com/imterah/nextnet/pull/11)
|
||||
- chore\(deps\): bump ws from 8.17.0 to 8.17.1 in /api [\#10](https://github.com/imterah/nextnet/pull/10)
|
||||
|
||||
## [v1.0.1](https://github.com/imterah/nextnet/tree/v1.0.1) (2024-05-18)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Adds public key authentication [\#6](https://github.com/imterah/nextnet/pull/6)
|
||||
- Add support for eslint [\#5](https://github.com/imterah/nextnet/pull/5)
|
||||
|
||||
## [v1.0.0](https://github.com/imterah/nextnet/tree/v1.0.0) (2024-05-10)
|
||||
|
||||
## [v0.1.1](https://github.com/imterah/nextnet/tree/v0.1.1) (2024-05-05)
|
||||
|
||||
## [v0.1.0](https://github.com/imterah/nextnet/tree/v0.1.0) (2024-05-05)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- \(potentially\) Migrate nix shell to nix flake [\#2](https://github.com/imterah/nextnet/issues/2)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- add precommit hooks [\#3](https://github.com/imterah/nextnet/issues/3)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Reimplements PassyFire as a possible backend [\#4](https://github.com/imterah/nextnet/pull/4)
|
||||
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Greyson
|
||||
Copyright (c) 2024, Tera
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
1. Copy and change the default password (or username & db name too) from the template file `prod-docker.env`:
|
||||
```bash
|
||||
sed "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" prod-docker.env > .env
|
||||
sed -e "s/POSTGRES_PASSWORD=hermes/POSTGRES_PASSWORD=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" -e "s/JWT_SECRET=hermes/JWT_SECRET=$(head -c 500 /dev/random | sha512sum | cut -d " " -f 1)/g" prod-docker.env > .env
|
||||
```
|
||||
|
||||
2. Build the docker stack: `docker compose --env-file .env up -d`
|
||||
|
|
|
@ -6,10 +6,10 @@ var (
|
|||
AvailableBackends []*Backend
|
||||
RunningBackends map[uint]*Runtime
|
||||
TempDir string
|
||||
isDevelopmentMode bool
|
||||
shouldLog bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
RunningBackends = make(map[uint]*Runtime)
|
||||
isDevelopmentMode = os.Getenv("HERMES_DEVELOPMENT_MODE") != ""
|
||||
shouldLog = os.Getenv("HERMES_DEVELOPMENT_MODE") != "" || os.Getenv("HERMES_BACKEND_LOGGING_ENABLED") != "" || os.Getenv("HERMES_LOG_LEVEL") == "debug"
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ import (
|
|||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// TODO TODO TODO(imterah):
|
||||
// This code is a mess. This NEEDS to be rearchitected and refactored to work better. Or at the very least, this code needs to be documented heavily.
|
||||
|
||||
func handleCommand(command interface{}, sock net.Conn, rtcChan chan interface{}) error {
|
||||
bytes, err := commonbackend.Marshal(command)
|
||||
|
||||
|
@ -160,6 +163,9 @@ func (runtime *Runtime) goRoutineHandler() error {
|
|||
|
||||
OuterLoop:
|
||||
for {
|
||||
_ = <-runtime.startProcessingNotification
|
||||
runtime.isRuntimeCurrentlyProcessing = true
|
||||
|
||||
for chanIndex, messageData := range runtime.messageBuffer {
|
||||
if messageData == nil {
|
||||
continue
|
||||
|
@ -177,6 +183,8 @@ func (runtime *Runtime) goRoutineHandler() error {
|
|||
|
||||
runtime.messageBuffer[chanIndex] = nil
|
||||
}
|
||||
|
||||
runtime.isRuntimeCurrentlyProcessing = false
|
||||
}
|
||||
|
||||
sock.Close()
|
||||
|
@ -235,6 +243,7 @@ func (runtime *Runtime) Start() error {
|
|||
runtime.messageBuffer = make([]*messageForBuf, 10)
|
||||
runtime.messageBufferLock = sync.Mutex{}
|
||||
|
||||
runtime.startProcessingNotification = make(chan bool)
|
||||
runtime.processRestartNotification = make(chan bool, 1)
|
||||
|
||||
runtime.logger = &writeLogger{
|
||||
|
@ -322,6 +331,10 @@ SchedulingLoop:
|
|||
schedulingAttempts++
|
||||
}
|
||||
|
||||
if !runtime.isRuntimeCurrentlyProcessing {
|
||||
runtime.startProcessingNotification <- true
|
||||
}
|
||||
|
||||
// Fetch response and close Channel
|
||||
response, ok := <-commandChannel
|
||||
|
||||
|
|
|
@ -16,15 +16,18 @@ type Backend struct {
|
|||
|
||||
type messageForBuf struct {
|
||||
Channel chan interface{}
|
||||
// TODO(imterah): could this be refactored to just be a []byte instead? Look into this
|
||||
Message interface{}
|
||||
}
|
||||
|
||||
type Runtime struct {
|
||||
isRuntimeRunning bool
|
||||
logger *writeLogger
|
||||
currentProcess *exec.Cmd
|
||||
currentListener net.Listener
|
||||
processRestartNotification chan bool
|
||||
isRuntimeRunning bool
|
||||
isRuntimeCurrentlyProcessing bool
|
||||
startProcessingNotification chan bool
|
||||
logger *writeLogger
|
||||
currentProcess *exec.Cmd
|
||||
currentListener net.Listener
|
||||
processRestartNotification chan bool
|
||||
|
||||
messageBufferLock sync.Mutex
|
||||
messageBuffer []*messageForBuf
|
||||
|
@ -42,7 +45,7 @@ type writeLogger struct {
|
|||
func (writer writeLogger) Write(p []byte) (n int, err error) {
|
||||
logSplit := strings.Split(string(p), "\n")
|
||||
|
||||
if isDevelopmentMode {
|
||||
if shouldLog {
|
||||
for _, logLine := range logSplit {
|
||||
if logLine == "" {
|
||||
continue
|
||||
|
|
|
@ -1,298 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Data structures
|
||||
type BackupBackend struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description *string `json:"description"`
|
||||
Backend string `json:"backend" validate:"required"`
|
||||
BackendParameters string `json:"connectionDetails" validate:"required"`
|
||||
}
|
||||
|
||||
type BackupProxy struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
BackendID uint `json:"destProviderID" validate:"required"`
|
||||
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description *string `json:"description"`
|
||||
Protocol string `json:"protocol" validate:"required"`
|
||||
SourceIP string `json:"sourceIP" validate:"required"`
|
||||
SourcePort uint16 `json:"sourcePort" validate:"required"`
|
||||
DestinationPort uint16 `json:"destPort" validate:"required"`
|
||||
AutoStart bool `json:"enabled" validate:"required"`
|
||||
}
|
||||
|
||||
type BackupPermission struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
|
||||
PermissionNode string `json:"permission" validate:"required"`
|
||||
HasPermission bool `json:"has" validate:"required"`
|
||||
UserID uint `json:"userID" validate:"required"`
|
||||
}
|
||||
|
||||
type BackupUser struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
|
||||
Email string `json:"email" validate:"required"`
|
||||
Username *string `json:"username"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
IsBot *bool `json:"isRootServiceAccount"`
|
||||
|
||||
Token *string `json:"rootToken" validate:"required"`
|
||||
}
|
||||
|
||||
type BackupData struct {
|
||||
Backends []*BackupBackend `json:"destinationProviders" validate:"required"`
|
||||
Proxies []*BackupProxy `json:"forwardRules" validate:"required"`
|
||||
Permissions []*BackupPermission `json:"allPermissions" validate:"required"`
|
||||
Users []*BackupUser `json:"users" validate:"required"`
|
||||
}
|
||||
|
||||
// From https://stackoverflow.com/questions/54461423/efficient-way-to-remove-all-non-alphanumeric-characters-from-large-text
|
||||
// Strips all alphanumeric characters from a string
|
||||
func stripAllAlphanumeric(s string) string {
|
||||
var result strings.Builder
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if ('a' <= b && b <= 'z') ||
|
||||
('A' <= b && b <= 'Z') ||
|
||||
('0' <= b && b <= '9') {
|
||||
result.WriteByte(b)
|
||||
} else {
|
||||
result.WriteByte('_')
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func backupRestoreEntrypoint(cCtx *cli.Context) error {
|
||||
log.Info("Decompressing backup...")
|
||||
|
||||
backupFile, err := os.Open(cCtx.String("backup-path"))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open backup: %s", err.Error())
|
||||
}
|
||||
|
||||
reader, err := gzip.NewReader(backupFile)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize Gzip (compression) reader: %s", err.Error())
|
||||
}
|
||||
|
||||
backupDataBytes, err := io.ReadAll(reader)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read backup contents: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Info("Decompressed backup. Cleaning up...")
|
||||
|
||||
err = reader.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close Gzip reader: %s", err.Error())
|
||||
}
|
||||
|
||||
err = backupFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close backup: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Info("Parsing backup into internal structures...")
|
||||
|
||||
backupData := &BackupData{}
|
||||
|
||||
err = json.Unmarshal(backupDataBytes, backupData)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse backup: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(backupData); err != nil {
|
||||
return fmt.Errorf("failed to validate backup: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Info("Initializing database and opening it...")
|
||||
|
||||
err = dbcore.InitializeDatabase(&gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %s", err)
|
||||
}
|
||||
|
||||
log.Info("Running database migrations...")
|
||||
|
||||
if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil {
|
||||
return fmt.Errorf("Failed to run database migrations: %s", err)
|
||||
}
|
||||
|
||||
log.Info("Restoring database...")
|
||||
bestEffortOwnerUIDFromBackup := -1
|
||||
|
||||
log.Info("Attempting to find user to use as owner of resources...")
|
||||
|
||||
for _, user := range backupData.Users {
|
||||
foundUser := false
|
||||
failedAdministrationCheck := false
|
||||
|
||||
for _, permission := range backupData.Permissions {
|
||||
if permission.UserID != user.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
foundUser = true
|
||||
|
||||
if !strings.HasPrefix(permission.PermissionNode, "routes.") && permission.PermissionNode != "permissions.see" && !permission.HasPermission {
|
||||
log.Infof("User with email '%s' and ID of '%d' failed administration check (lacks all permissions required). Attempting to find better user", user.Email, user.ID)
|
||||
failedAdministrationCheck = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundUser {
|
||||
log.Warnf("User with email '%s' and ID of '%d' lacks any permissions!", user.Email, user.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if failedAdministrationCheck {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Using user with email '%s', and ID of '%d'", user.Email, user.ID)
|
||||
bestEffortOwnerUIDFromBackup = int(user.ID)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if bestEffortOwnerUIDFromBackup == -1 {
|
||||
log.Warnf("Could not find Administrative level user to use as the owner of resources. Using user with email '%s', and ID of '%d'", backupData.Users[0].Email, backupData.Users[0].ID)
|
||||
bestEffortOwnerUIDFromBackup = int(backupData.Users[0].ID)
|
||||
}
|
||||
|
||||
var bestEffortOwnerUID uint
|
||||
|
||||
for _, user := range backupData.Users {
|
||||
log.Infof("Migrating user with email '%s' and ID of '%d'", user.Email, user.ID)
|
||||
tokens := make([]dbcore.Token, 0)
|
||||
permissions := make([]dbcore.Permission, 0)
|
||||
|
||||
if user.Token != nil {
|
||||
tokens = append(tokens, dbcore.Token{
|
||||
Token: *user.Token,
|
||||
DisableExpiry: true,
|
||||
CreationIPAddr: "127.0.0.1", // We don't know the creation IP address...
|
||||
})
|
||||
}
|
||||
|
||||
for _, permission := range backupData.Permissions {
|
||||
if permission.UserID != user.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
permissions = append(permissions, dbcore.Permission{
|
||||
PermissionNode: permission.PermissionNode,
|
||||
HasPermission: permission.HasPermission,
|
||||
})
|
||||
}
|
||||
|
||||
username := ""
|
||||
|
||||
if user.Username == nil {
|
||||
username = strings.ToLower(stripAllAlphanumeric(user.Email))
|
||||
log.Warnf("User with ID of '%d' doesn't have a username. Derived username from email is '%s' (email is '%s')", user.ID, username, user.Email)
|
||||
} else {
|
||||
username = *user.Username
|
||||
}
|
||||
|
||||
userDatabase := &dbcore.User{
|
||||
Email: user.Email,
|
||||
Username: username,
|
||||
Name: user.Name,
|
||||
Password: base64.StdEncoding.EncodeToString([]byte(user.Password)),
|
||||
IsBot: user.IsBot,
|
||||
|
||||
Tokens: tokens,
|
||||
Permissions: permissions,
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Create(userDatabase).Error; err != nil {
|
||||
log.Errorf("Failed to create user: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if uint(bestEffortOwnerUIDFromBackup) == user.ID {
|
||||
bestEffortOwnerUID = userDatabase.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, backend := range backupData.Backends {
|
||||
log.Infof("Migrating backend ID '%d' with name '%s'", backend.ID, backend.Name)
|
||||
|
||||
backendDatabase := &dbcore.Backend{
|
||||
UserID: bestEffortOwnerUID,
|
||||
Name: backend.Name,
|
||||
Description: backend.Description,
|
||||
Backend: backend.Backend,
|
||||
BackendParameters: base64.StdEncoding.EncodeToString([]byte(backend.BackendParameters)),
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Create(backendDatabase).Error; err != nil {
|
||||
log.Errorf("Failed to create backend: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Migrating proxies for backend ID '%d'", backend.ID)
|
||||
|
||||
for _, proxy := range backupData.Proxies {
|
||||
if proxy.BackendID != backend.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Migrating proxy ID '%d' with name '%s'", proxy.ID, proxy.Name)
|
||||
|
||||
proxyDatabase := &dbcore.Proxy{
|
||||
BackendID: backendDatabase.ID,
|
||||
UserID: bestEffortOwnerUID,
|
||||
|
||||
Name: proxy.Name,
|
||||
Description: proxy.Description,
|
||||
Protocol: proxy.Protocol,
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestinationPort: proxy.DestinationPort,
|
||||
AutoStart: proxy.AutoStart,
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Create(proxyDatabase).Error; err != nil {
|
||||
log.Errorf("Failed to create proxy: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Successfully upgraded to Hermes from NextNet.")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -7,13 +7,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type BackendCreationRequest struct {
|
||||
|
@ -24,131 +23,114 @@ type BackendCreationRequest struct {
|
|||
BackendParameters interface{} `json:"connectionDetails" validate:"required"`
|
||||
}
|
||||
|
||||
func CreateBackend(c *gin.Context) {
|
||||
var req BackendCreationRequest
|
||||
func SetupCreateBackend(state *state.State) {
|
||||
state.Engine.POST("/api/v1/backends/create", func(c *gin.Context) {
|
||||
var req BackendCreationRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "backends.add") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "backends.add") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
var backendParameters []byte
|
||||
|
||||
return
|
||||
}
|
||||
switch parameters := req.BackendParameters.(type) {
|
||||
case string:
|
||||
backendParameters = []byte(parameters)
|
||||
case map[string]interface{}:
|
||||
backendParameters, err = json.Marshal(parameters)
|
||||
|
||||
var backendParameters []byte
|
||||
if err != nil {
|
||||
log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error())
|
||||
|
||||
switch parameters := req.BackendParameters.(type) {
|
||||
case string:
|
||||
backendParameters = []byte(parameters)
|
||||
case map[string]interface{}:
|
||||
backendParameters, err = json.Marshal(parameters)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to prepare parameters",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Invalid type for connectionDetails (recieved %T)", parameters),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var backendRuntimeFilePath string
|
||||
|
||||
for _, runtime := range backendruntime.AvailableBackends {
|
||||
if runtime.Name == req.Backend {
|
||||
backendRuntimeFilePath = runtime.Path
|
||||
}
|
||||
}
|
||||
|
||||
if backendRuntimeFilePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Unsupported backend recieved",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend := backendruntime.NewBackend(backendRuntimeFilePath)
|
||||
err = backend.Start()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to marshal JSON recieved as BackendParameters: %s", err.Error())
|
||||
log.Warnf("Failed to start backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to prepare parameters",
|
||||
"error": "Failed to start backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Invalid type for connectionDetails (recieved %T)", parameters),
|
||||
|
||||
backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{
|
||||
Arguments: backendParameters,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var backendRuntimeFilePath string
|
||||
|
||||
for _, runtime := range backendruntime.AvailableBackends {
|
||||
if runtime.Name == req.Backend {
|
||||
backendRuntimeFilePath = runtime.Path
|
||||
}
|
||||
}
|
||||
|
||||
if backendRuntimeFilePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Unsupported backend recieved",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend := backendruntime.NewBackend(backendRuntimeFilePath)
|
||||
err = backend.Start()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to start backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendParamCheckResponse, err := backend.ProcessCommand(&commonbackend.CheckServerParameters{
|
||||
Arguments: backendParameters,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendParamCheckResponse.(type) {
|
||||
case *commonbackend.CheckParametersResponse:
|
||||
if responseMessage.InResponseTo != "checkServerParameters" {
|
||||
log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo)
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
|
@ -163,107 +145,126 @@ func CreateBackend(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if !responseMessage.IsValid {
|
||||
switch responseMessage := backendParamCheckResponse.(type) {
|
||||
case *commonbackend.CheckParametersResponse:
|
||||
if responseMessage.InResponseTo != "checkServerParameters" {
|
||||
log.Errorf("Got illegal response to CheckServerParameters: %s", responseMessage.InResponseTo)
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !responseMessage.IsValid {
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
|
||||
if responseMessage.Message == "" {
|
||||
errorMessage = "Unkown error while trying to parse connectionDetails"
|
||||
} else {
|
||||
errorMessage = fmt.Sprintf("Invalid backend parameters: %s", responseMessage.Message)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorMessage,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Warnf("Got illegal response type for backend: %T", responseMessage)
|
||||
}
|
||||
|
||||
log.Info("Passed backend checks successfully")
|
||||
|
||||
backendInDatabase := &db.Backend{
|
||||
UserID: user.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Backend: req.Backend,
|
||||
BackendParameters: base64.StdEncoding.EncodeToString(backendParameters),
|
||||
}
|
||||
|
||||
if result := state.DB.DB.Create(&backendInDatabase); result.Error != nil {
|
||||
log.Warnf("Failed to create backend: %s", result.Error.Error())
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
|
||||
if responseMessage.Message == "" {
|
||||
errorMessage = "Unkown error while trying to parse connectionDetails"
|
||||
} else {
|
||||
errorMessage = fmt.Sprintf("Invalid backend parameters: %s", responseMessage.Message)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorMessage,
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add backend into database",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Warnf("Got illegal response type for backend: %T", responseMessage)
|
||||
}
|
||||
|
||||
log.Info("Passed backend checks successfully")
|
||||
|
||||
backendInDatabase := &dbcore.Backend{
|
||||
UserID: user.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Backend: req.Backend,
|
||||
BackendParameters: base64.StdEncoding.EncodeToString(backendParameters),
|
||||
}
|
||||
|
||||
if result := dbcore.DB.Create(&backendInDatabase); result.Error != nil {
|
||||
log.Warnf("Failed to create backend: %s", result.Error.Error())
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add backend into database",
|
||||
backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{
|
||||
Arguments: backendParameters,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendStartResponse, err := backend.ProcessCommand(&commonbackend.Start{
|
||||
Arguments: backendParameters,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendStartResponse.(type) {
|
||||
case *commonbackend.BackendStatusResponse:
|
||||
if !responseMessage.IsRunning {
|
||||
err = backend.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start backend: %s", err.Error())
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
|
||||
if responseMessage.Message == "" {
|
||||
errorMessage = "Unkown error while trying to start the backend"
|
||||
} else {
|
||||
errorMessage = fmt.Sprintf("Failed to start backend: %s", responseMessage.Message)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorMessage,
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Warnf("Got illegal response type for backend: %T", responseMessage)
|
||||
}
|
||||
|
||||
backendruntime.RunningBackends[backendInDatabase.ID] = backend
|
||||
switch responseMessage := backendStartResponse.(type) {
|
||||
case *commonbackend.BackendStatusResponse:
|
||||
if !responseMessage.IsRunning {
|
||||
err = backend.Stop()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start backend: %s", err.Error())
|
||||
}
|
||||
|
||||
var errorMessage string
|
||||
|
||||
if responseMessage.Message == "" {
|
||||
errorMessage = "Unkown error while trying to start the backend"
|
||||
} else {
|
||||
errorMessage = fmt.Sprintf("Failed to start backend: %s", responseMessage.Message)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": errorMessage,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Warnf("Got illegal response type for backend: %T", responseMessage)
|
||||
}
|
||||
|
||||
backendruntime.RunningBackends[backendInDatabase.ID] = backend
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type BackendLookupRequest struct {
|
||||
|
@ -38,95 +37,80 @@ type LookupResponse struct {
|
|||
Data []*SanitizedBackend `json:"data"`
|
||||
}
|
||||
|
||||
func LookupBackend(c *gin.Context) {
|
||||
var req BackendLookupRequest
|
||||
func SetupLookupBackend(state *state.State) {
|
||||
state.Engine.POST("/api/v1/backends/lookup", func(c *gin.Context) {
|
||||
var req BackendLookupRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "backends.visible") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
backends := []dbcore.Backend{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if req.BackendID != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.BackendID)
|
||||
}
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
if req.Description != nil {
|
||||
queryString = append(queryString, "description = ?")
|
||||
queryParameters = append(queryParameters, req.Description)
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
if req.Backend != nil {
|
||||
queryString = append(queryString, "is_bot = ?")
|
||||
queryParameters = append(queryParameters, req.Backend)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil {
|
||||
log.Warnf("Failed to get backends: %s", err.Error())
|
||||
if !permissions.UserHasPermission(user, "backends.visible") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get backends",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
backends := []db.Backend{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
|
||||
sanitizedBackends := make([]*SanitizedBackend, len(backends))
|
||||
hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis")
|
||||
if req.BackendID != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.BackendID)
|
||||
}
|
||||
|
||||
for backendIndex, backend := range backends {
|
||||
foundBackend, ok := backendruntime.RunningBackends[backend.ID]
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Failed to get backend #%d controller", backend.ID)
|
||||
if req.Description != nil {
|
||||
queryString = append(queryString, "description = ?")
|
||||
queryParameters = append(queryParameters, req.Description)
|
||||
}
|
||||
|
||||
if req.Backend != nil {
|
||||
queryString = append(queryString, "is_bot = ?")
|
||||
queryParameters = append(queryParameters, req.Backend)
|
||||
}
|
||||
|
||||
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&backends).Error; err != nil {
|
||||
log.Warnf("Failed to get backends: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get backends",
|
||||
|
@ -135,29 +119,46 @@ func LookupBackend(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
sanitizedBackends[backendIndex] = &SanitizedBackend{
|
||||
BackendID: backend.ID,
|
||||
OwnerID: backend.UserID,
|
||||
Name: backend.Name,
|
||||
Description: backend.Description,
|
||||
Backend: backend.Backend,
|
||||
Logs: foundBackend.Logs,
|
||||
}
|
||||
sanitizedBackends := make([]*SanitizedBackend, len(backends))
|
||||
hasSecretVisibility := permissions.UserHasPermission(user, "backends.secretVis")
|
||||
|
||||
if backend.UserID == user.ID || hasSecretVisibility {
|
||||
backendParametersBytes, err := base64.StdEncoding.DecodeString(backend.BackendParameters)
|
||||
for backendIndex, backend := range backends {
|
||||
foundBackend, ok := backendruntime.RunningBackends[backend.ID]
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to decode base64 backend parameters: %s", err.Error())
|
||||
if !ok {
|
||||
log.Warnf("Failed to get backend #%d controller", backend.ID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get backends",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendParameters := string(backendParametersBytes)
|
||||
sanitizedBackends[backendIndex].BackendParameters = &backendParameters
|
||||
}
|
||||
}
|
||||
sanitizedBackends[backendIndex] = &SanitizedBackend{
|
||||
BackendID: backend.ID,
|
||||
OwnerID: backend.UserID,
|
||||
Name: backend.Name,
|
||||
Description: backend.Description,
|
||||
Backend: backend.Backend,
|
||||
Logs: foundBackend.Logs,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &LookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedBackends,
|
||||
if backend.UserID == user.ID || hasSecretVisibility {
|
||||
backendParametersBytes, err := base64.StdEncoding.DecodeString(backend.BackendParameters)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to decode base64 backend parameters: %s", err.Error())
|
||||
}
|
||||
|
||||
backendParameters := string(backendParametersBytes)
|
||||
sanitizedBackends[backendIndex].BackendParameters = &backendParameters
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &LookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedBackends,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type BackendRemovalRequest struct {
|
||||
|
@ -18,106 +17,108 @@ type BackendRemovalRequest struct {
|
|||
BackendID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
func RemoveBackend(c *gin.Context) {
|
||||
var req BackendRemovalRequest
|
||||
func SetupRemoveBackend(state *state.State) {
|
||||
state.Engine.POST("/api/v1/backends/remove", func(c *gin.Context) {
|
||||
var req BackendRemovalRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "backends.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var backend *dbcore.Backend
|
||||
backendRequest := dbcore.DB.Where("id = ?", req.BackendID).Find(&backend)
|
||||
|
||||
if backendRequest.Error != nil {
|
||||
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if backend exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendExists := backendRequest.RowsAffected > 0
|
||||
|
||||
if !backendExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Backend doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Delete(backend).Error; err != nil {
|
||||
log.Warnf("failed to delete backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to delete backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendInstance, ok := backendruntime.RunningBackends[req.BackendID]
|
||||
|
||||
if ok {
|
||||
err = backendInstance.Stop()
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Backend deleted, but failed to stop",
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "backends.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
delete(backendruntime.RunningBackends, req.BackendID)
|
||||
return
|
||||
}
|
||||
|
||||
delete(backendruntime.RunningBackends, req.BackendID)
|
||||
}
|
||||
var backend *db.Backend
|
||||
backendRequest := state.DB.DB.Where("id = ?", req.BackendID).Find(&backend)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
if backendRequest.Error != nil {
|
||||
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if backend exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendExists := backendRequest.RowsAffected > 0
|
||||
|
||||
if !backendExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Backend doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := state.DB.DB.Delete(backend).Error; err != nil {
|
||||
log.Warnf("failed to delete backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to delete backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendInstance, ok := backendruntime.RunningBackends[req.BackendID]
|
||||
|
||||
if ok {
|
||||
err = backendInstance.Stop()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to stop backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Backend deleted, but failed to stop",
|
||||
})
|
||||
|
||||
delete(backendruntime.RunningBackends, req.BackendID)
|
||||
return
|
||||
}
|
||||
|
||||
delete(backendruntime.RunningBackends, req.BackendID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ConnectionsRequest struct {
|
||||
|
@ -37,127 +36,130 @@ type ConnectionsResponse struct {
|
|||
Data []*SanitizedConnection `json:"data"`
|
||||
}
|
||||
|
||||
func GetConnections(c *gin.Context) {
|
||||
var req ConnectionsRequest
|
||||
func SetupGetConnections(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/connections", func(c *gin.Context) {
|
||||
var req ConnectionsRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.visibleConn") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var proxy dbcore.Proxy
|
||||
proxyRequest := dbcore.DB.Where("id = ?", req.Id).First(&proxy)
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find forward entry",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "No forward entry found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyConnectionsResponse:
|
||||
sanitizedConnections := []*SanitizedConnection{}
|
||||
|
||||
for _, connection := range responseMessage.Connections {
|
||||
if connection.SourceIP == proxy.SourceIP && connection.SourcePort == proxy.SourcePort && proxy.DestinationPort == proxy.DestinationPort {
|
||||
sanitizedConnections = append(sanitizedConnections, &SanitizedConnection{
|
||||
ClientIP: connection.ClientIP,
|
||||
Port: connection.ClientPort,
|
||||
|
||||
ConnectionDetails: &ConnectionDetailsForConnection{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
},
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &ConnectionsResponse{
|
||||
Success: true,
|
||||
Data: sanitizedConnections,
|
||||
})
|
||||
default:
|
||||
log.Warnf("Got illegal response type for backend: %T", responseMessage)
|
||||
if !permissions.UserHasPermission(user, "routes.visibleConn") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got illegal response type",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var proxy db.Proxy
|
||||
proxyRequest := state.DB.DB.Where("id = ?", req.Id).First(&proxy)
|
||||
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find proxy: %s", proxyRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find forward entry",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "No forward entry found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendRuntime, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backendRuntime.ProcessCommand(&commonbackend.ProxyConnectionsRequest{})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get status response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyConnectionsResponse:
|
||||
sanitizedConnections := []*SanitizedConnection{}
|
||||
|
||||
for _, connection := range responseMessage.Connections {
|
||||
if connection.SourceIP == proxy.SourceIP && connection.SourcePort == proxy.SourcePort && proxy.DestinationPort == proxy.DestinationPort {
|
||||
sanitizedConnections = append(sanitizedConnections, &SanitizedConnection{
|
||||
ClientIP: connection.ClientIP,
|
||||
Port: connection.ClientPort,
|
||||
|
||||
ConnectionDetails: &ConnectionDetailsForConnection{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &ConnectionsResponse{
|
||||
Success: true,
|
||||
Data: sanitizedConnections,
|
||||
})
|
||||
default:
|
||||
log.Warnf("Got illegal response type for backend: %T", responseMessage)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got illegal response type",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ProxyCreationRequest struct {
|
||||
|
@ -26,150 +25,153 @@ type ProxyCreationRequest struct {
|
|||
AutoStart *bool `json:"autoStart"`
|
||||
}
|
||||
|
||||
func CreateProxy(c *gin.Context) {
|
||||
var req ProxyCreationRequest
|
||||
func SetupCreateProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/create", func(c *gin.Context) {
|
||||
var req ProxyCreationRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.add") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if req.Protocol != "tcp" && req.Protocol != "udp" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Protocol must be either 'tcp' or 'udp'",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var backend dbcore.Backend
|
||||
backendRequest := dbcore.DB.Where("id = ?", req.ProviderID).First(&backend)
|
||||
|
||||
if backendRequest.Error != nil {
|
||||
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if backend exists",
|
||||
})
|
||||
}
|
||||
|
||||
backendExists := backendRequest.RowsAffected > 0
|
||||
|
||||
if !backendExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Could not find backend",
|
||||
})
|
||||
}
|
||||
|
||||
autoStart := false
|
||||
|
||||
if req.AutoStart != nil {
|
||||
autoStart = *req.AutoStart
|
||||
}
|
||||
|
||||
proxy := &dbcore.Proxy{
|
||||
UserID: user.ID,
|
||||
BackendID: req.ProviderID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Protocol: req.Protocol,
|
||||
SourceIP: req.SourceIP,
|
||||
SourcePort: req.SourcePort,
|
||||
DestinationPort: req.DestinationPort,
|
||||
AutoStart: autoStart,
|
||||
}
|
||||
|
||||
if result := dbcore.DB.Create(proxy); result.Error != nil {
|
||||
log.Warnf("failed to create proxy: %s", result.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add forward rule to database",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if autoStart {
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"id": proxy.ID,
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to get response from backend",
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.add") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if !responseMessage.IsActive {
|
||||
log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID)
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
}
|
||||
}
|
||||
if req.Protocol != "tcp" && req.Protocol != "udp" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Protocol must be either 'tcp' or 'udp'",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"id": proxy.ID,
|
||||
return
|
||||
}
|
||||
|
||||
var backend db.Backend
|
||||
backendRequest := state.DB.DB.Where("id = ?", req.ProviderID).First(&backend)
|
||||
|
||||
if backendRequest.Error != nil {
|
||||
log.Warnf("failed to find if backend exists or not: %s", backendRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if backend exists",
|
||||
})
|
||||
}
|
||||
|
||||
backendExists := backendRequest.RowsAffected > 0
|
||||
|
||||
if !backendExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Could not find backend",
|
||||
})
|
||||
}
|
||||
|
||||
autoStart := false
|
||||
|
||||
if req.AutoStart != nil {
|
||||
autoStart = *req.AutoStart
|
||||
}
|
||||
|
||||
proxy := &db.Proxy{
|
||||
UserID: user.ID,
|
||||
BackendID: req.ProviderID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Protocol: req.Protocol,
|
||||
SourceIP: req.SourceIP,
|
||||
SourcePort: req.SourcePort,
|
||||
DestinationPort: req.DestinationPort,
|
||||
AutoStart: autoStart,
|
||||
}
|
||||
|
||||
if result := state.DB.DB.Create(proxy); result.Error != nil {
|
||||
log.Warnf("failed to create proxy: %s", result.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add forward rule to database",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if autoStart {
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"id": proxy.ID,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to get response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if !responseMessage.IsActive {
|
||||
log.Warnf("Failed to start proxy for backend #%d", proxy.BackendID)
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"id": proxy.ID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ProxyLookupRequest struct {
|
||||
|
@ -43,141 +42,143 @@ type ProxyLookupResponse struct {
|
|||
Data []*SanitizedProxy `json:"data"`
|
||||
}
|
||||
|
||||
func LookupProxy(c *gin.Context) {
|
||||
var req ProxyLookupRequest
|
||||
func SetupLookupProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/lookup", func(c *gin.Context) {
|
||||
var req ProxyLookupRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.visible") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if req.Protocol != nil {
|
||||
if *req.Protocol != "tcp" && *req.Protocol != "udp" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Protocol specified in body must either be 'tcp' or 'udp'",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
proxies := []db.Proxy{}
|
||||
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
|
||||
if req.Id != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.Id)
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
queryString = append(queryString, "description = ?")
|
||||
queryParameters = append(queryParameters, req.Description)
|
||||
}
|
||||
|
||||
if req.SourceIP != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
if req.SourcePort != nil {
|
||||
queryString = append(queryString, "source_port = ?")
|
||||
queryParameters = append(queryParameters, req.SourcePort)
|
||||
}
|
||||
|
||||
if req.DestinationPort != nil {
|
||||
queryString = append(queryString, "destination_port = ?")
|
||||
queryParameters = append(queryParameters, req.DestinationPort)
|
||||
}
|
||||
|
||||
if req.ProviderID != nil {
|
||||
queryString = append(queryString, "backend_id = ?")
|
||||
queryParameters = append(queryParameters, req.ProviderID)
|
||||
}
|
||||
|
||||
if req.AutoStart != nil {
|
||||
queryString = append(queryString, "auto_start = ?")
|
||||
queryParameters = append(queryParameters, req.AutoStart)
|
||||
}
|
||||
|
||||
if req.Protocol != nil {
|
||||
queryString = append(queryString, "protocol = ?")
|
||||
queryParameters = append(queryParameters, req.Protocol)
|
||||
}
|
||||
|
||||
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil {
|
||||
log.Warnf("failed to get proxies: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
"error": "Failed to get proxies",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.visible") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
sanitizedProxies := make([]*SanitizedProxy, len(proxies))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if req.Protocol != nil {
|
||||
if *req.Protocol != "tcp" && *req.Protocol != "udp" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Protocol specified in body must either be 'tcp' or 'udp'",
|
||||
})
|
||||
|
||||
return
|
||||
for proxyIndex, proxy := range proxies {
|
||||
sanitizedProxies[proxyIndex] = &SanitizedProxy{
|
||||
Id: proxy.ID,
|
||||
Name: proxy.Name,
|
||||
Description: proxy.Description,
|
||||
Protcol: proxy.Protocol,
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestinationPort: proxy.DestinationPort,
|
||||
ProviderID: proxy.BackendID,
|
||||
AutoStart: proxy.AutoStart,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxies := []dbcore.Proxy{}
|
||||
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
|
||||
if req.Id != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.Id)
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
queryString = append(queryString, "description = ?")
|
||||
queryParameters = append(queryParameters, req.Description)
|
||||
}
|
||||
|
||||
if req.SourceIP != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
if req.SourcePort != nil {
|
||||
queryString = append(queryString, "source_port = ?")
|
||||
queryParameters = append(queryParameters, req.SourcePort)
|
||||
}
|
||||
|
||||
if req.DestinationPort != nil {
|
||||
queryString = append(queryString, "destination_port = ?")
|
||||
queryParameters = append(queryParameters, req.DestinationPort)
|
||||
}
|
||||
|
||||
if req.ProviderID != nil {
|
||||
queryString = append(queryString, "backend_id = ?")
|
||||
queryParameters = append(queryParameters, req.ProviderID)
|
||||
}
|
||||
|
||||
if req.AutoStart != nil {
|
||||
queryString = append(queryString, "auto_start = ?")
|
||||
queryParameters = append(queryParameters, req.AutoStart)
|
||||
}
|
||||
|
||||
if req.Protocol != nil {
|
||||
queryString = append(queryString, "protocol = ?")
|
||||
queryParameters = append(queryParameters, req.Protocol)
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&proxies).Error; err != nil {
|
||||
log.Warnf("failed to get proxies: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get proxies",
|
||||
c.JSON(http.StatusOK, &ProxyLookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedProxies,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedProxies := make([]*SanitizedProxy, len(proxies))
|
||||
|
||||
for proxyIndex, proxy := range proxies {
|
||||
sanitizedProxies[proxyIndex] = &SanitizedProxy{
|
||||
Id: proxy.ID,
|
||||
Name: proxy.Name,
|
||||
Description: proxy.Description,
|
||||
Protcol: proxy.Protocol,
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestinationPort: proxy.DestinationPort,
|
||||
ProviderID: proxy.BackendID,
|
||||
AutoStart: proxy.AutoStart,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &ProxyLookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedProxies,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ProxyRemovalRequest struct {
|
||||
|
@ -19,134 +18,133 @@ type ProxyRemovalRequest struct {
|
|||
ID uint `validate:"required" json:"id"`
|
||||
}
|
||||
|
||||
func RemoveProxy(c *gin.Context) {
|
||||
var req ProxyRemovalRequest
|
||||
func SetupRemoveProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/remove", func(c *gin.Context) {
|
||||
var req ProxyRemovalRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
var proxy *db.Proxy
|
||||
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||
|
||||
return
|
||||
}
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||
|
||||
var proxy *dbcore.Proxy
|
||||
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if forward rule exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Forward rule doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Delete(proxy).Error; err != nil {
|
||||
log.Warnf("failed to delete proxy: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to delete forward rule",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get response from backend. Proxy was still successfully deleted",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if responseMessage.IsActive {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to stop proxy. Proxy was still successfully deleted",
|
||||
"error": "Failed to find if forward rule exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Forward rule doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := state.DB.DB.Delete(proxy).Error; err != nil {
|
||||
log.Warnf("failed to delete proxy: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to delete forward rule",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get response from backend. Proxy was still successfully deleted",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if responseMessage.IsActive {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to stop proxy. Proxy was still successfully deleted",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ProxyStartRequest struct {
|
||||
|
@ -19,124 +18,119 @@ type ProxyStartRequest struct {
|
|||
ID uint `validate:"required" json:"id"`
|
||||
}
|
||||
|
||||
func StartProxy(c *gin.Context) {
|
||||
var req ProxyStartRequest
|
||||
func SetupStartProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/start", func(c *gin.Context) {
|
||||
var req ProxyStartRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.start") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.start") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var proxy *dbcore.Proxy
|
||||
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if forward rule exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Forward rule doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case error:
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to get response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if !responseMessage.IsActive {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to start proxy",
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
var proxy *db.Proxy
|
||||
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if forward rule exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Forward rule doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.AddProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case error:
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to get response from backend",
|
||||
})
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if !responseMessage.IsActive {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to start proxy",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got invalid response from backend. Proxy was likely still successfully started",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@ import (
|
|||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type ProxyStopRequest struct {
|
||||
|
@ -19,124 +18,119 @@ type ProxyStopRequest struct {
|
|||
ID uint `validate:"required" json:"id"`
|
||||
}
|
||||
|
||||
func StopProxy(c *gin.Context) {
|
||||
var req ProxyStopRequest
|
||||
func SetupStopProxy(state *state.State) {
|
||||
state.Engine.POST("/api/v1/forward/stop", func(c *gin.Context) {
|
||||
var req ProxyStartRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.stop") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !permissions.UserHasPermission(user, "routes.stop") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
var proxy *db.Proxy
|
||||
proxyRequest := state.DB.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||
|
||||
return
|
||||
}
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||
|
||||
var proxy *dbcore.Proxy
|
||||
proxyRequest := dbcore.DB.Where("id = ?", req.ID).Find(&proxy)
|
||||
|
||||
if proxyRequest.Error != nil {
|
||||
log.Warnf("failed to find if proxy exists or not: %s", proxyRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if forward rule exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Forward rule doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to get response from backend",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if responseMessage.IsActive {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to stop proxy",
|
||||
"error": "Failed to find if forward rule exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got invalid response from backend. Proxy was still successfully deleted",
|
||||
proxyExists := proxyRequest.RowsAffected > 0
|
||||
|
||||
if !proxyExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Forward rule doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backend, ok := backendruntime.RunningBackends[proxy.BackendID]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Couldn't fetch backend runtime from backend ID #%d", proxy.BackendID)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Couldn't fetch backend runtime",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
backendResponse, err := backend.ProcessCommand(&commonbackend.RemoveProxy{
|
||||
SourceIP: proxy.SourceIP,
|
||||
SourcePort: proxy.SourcePort,
|
||||
DestPort: proxy.DestinationPort,
|
||||
Protocol: proxy.Protocol,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
switch responseMessage := backendResponse.(type) {
|
||||
case error:
|
||||
log.Warnf("Failed to get response for backend #%d: %s", proxy.BackendID, responseMessage.Error())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to get response from backend",
|
||||
})
|
||||
case *commonbackend.ProxyStatusResponse:
|
||||
if responseMessage.IsActive {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to stop proxy",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
default:
|
||||
log.Errorf("Got illegal response type for backend #%d: %T", proxy.BackendID, responseMessage)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Got invalid response from backend. Proxy was likely still successfully stopped",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,11 +7,9 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
permissionHelper "git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -22,142 +20,141 @@ type UserCreationRequest struct {
|
|||
Email string `validate:"required"`
|
||||
Password string `validate:"required"`
|
||||
Username string `validate:"required"`
|
||||
|
||||
// TODO: implement support
|
||||
ExistingUserToken string `json:"token"`
|
||||
IsBot bool
|
||||
IsBot bool
|
||||
}
|
||||
|
||||
func CreateUser(c *gin.Context) {
|
||||
if !signupEnabled && !unsafeSignup {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Signing up is not enabled at this time.",
|
||||
})
|
||||
func SetupCreateUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/create", func(c *gin.Context) {
|
||||
if !signupEnabled && !unsafeSignup {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Signing up is not enabled at this time.",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var req UserCreationRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var user *dbcore.User
|
||||
userRequest := dbcore.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if user exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if userExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User already exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
passwordHashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate password for client upon signup: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate password hash",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
permissions := []dbcore.Permission{}
|
||||
|
||||
for _, permission := range permissionHelper.DefaultPermissionNodes {
|
||||
permissionEnabledState := false
|
||||
|
||||
if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" {
|
||||
permissionEnabledState = true
|
||||
return
|
||||
}
|
||||
|
||||
permissions = append(permissions, dbcore.Permission{
|
||||
PermissionNode: permission,
|
||||
HasPermission: permissionEnabledState,
|
||||
})
|
||||
}
|
||||
var req UserCreationRequest
|
||||
|
||||
tokenRandomData := make([]byte, 80)
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
if _, err := rand.Read(tokenRandomData); err != nil {
|
||||
log.Warnf("Failed to read random data to use as token: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user = &dbcore.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Name: req.Name,
|
||||
IsBot: &req.IsBot,
|
||||
Password: base64.StdEncoding.EncodeToString(passwordHashed),
|
||||
Permissions: permissions,
|
||||
Tokens: []dbcore.Token{
|
||||
{
|
||||
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
DisableExpiry: forceNoExpiryTokens,
|
||||
CreationIPAddr: c.ClientIP(),
|
||||
var user *db.User
|
||||
userRequest := state.DB.DB.Where("email = ? OR username = ?", req.Email, req.Username).Find(&user)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if user exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if userExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User already exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
passwordHashed, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate password for client upon signup: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate password hash",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
permissions := []db.Permission{}
|
||||
|
||||
for _, permission := range permissionHelper.DefaultPermissionNodes {
|
||||
permissionEnabledState := false
|
||||
|
||||
if unsafeSignup || strings.HasPrefix(permission, "routes.") || permission == "permissions.see" {
|
||||
permissionEnabledState = true
|
||||
}
|
||||
|
||||
permissions = append(permissions, db.Permission{
|
||||
PermissionNode: permission,
|
||||
HasPermission: permissionEnabledState,
|
||||
})
|
||||
}
|
||||
|
||||
tokenRandomData := make([]byte, 80)
|
||||
|
||||
if _, err := rand.Read(tokenRandomData); err != nil {
|
||||
log.Warnf("Failed to read random data to use as token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user = &db.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Name: req.Name,
|
||||
IsBot: &req.IsBot,
|
||||
Password: base64.StdEncoding.EncodeToString(passwordHashed),
|
||||
Permissions: permissions,
|
||||
Tokens: []db.Token{
|
||||
{
|
||||
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
DisableExpiry: forceNoExpiryTokens,
|
||||
CreationIPAddr: c.ClientIP(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if result := dbcore.DB.Create(&user); result.Error != nil {
|
||||
log.Warnf("Failed to create user: %s", result.Error.Error())
|
||||
if result := state.DB.DB.Create(&user); result.Error != nil {
|
||||
log.Warnf("Failed to create user: %s", result.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add user into database",
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add user into database",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := state.JWT.Generate(user.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": jwt,
|
||||
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := jwtcore.Generate(user.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": jwt,
|
||||
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,11 +6,10 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
|
@ -21,137 +20,139 @@ type UserLoginRequest struct {
|
|||
Password string `validate:"required"`
|
||||
}
|
||||
|
||||
func LoginUser(c *gin.Context) {
|
||||
var req UserLoginRequest
|
||||
func SetupLoginUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/login", func(c *gin.Context) {
|
||||
var req UserLoginRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == nil && req.Username == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Missing both email and username in body",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userFindRequestArguments := make([]interface{}, 1)
|
||||
userFindRequest := ""
|
||||
|
||||
if req.Email != nil {
|
||||
userFindRequestArguments[0] = &req.Email
|
||||
userFindRequest += "email = ?"
|
||||
}
|
||||
|
||||
if req.Username != nil {
|
||||
userFindRequestArguments[0] = &req.Username
|
||||
userFindRequest += "username = ?"
|
||||
}
|
||||
|
||||
var user *db.User
|
||||
userRequest := state.DB.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if user exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password)))
|
||||
_, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password))
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("failed to decode password in database: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse database result for password",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Invalid password",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tokenRandomData := make([]byte, 80)
|
||||
|
||||
if _, err := rand.Read(tokenRandomData); err != nil {
|
||||
log.Warnf("Failed to read random data to use as token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
token := &db.Token{
|
||||
UserID: user.ID,
|
||||
|
||||
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
DisableExpiry: forceNoExpiryTokens,
|
||||
CreationIPAddr: c.ClientIP(),
|
||||
}
|
||||
|
||||
if result := state.DB.DB.Create(&token); result.Error != nil {
|
||||
log.Warnf("Failed to create user: %s", result.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add refresh token into database",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := state.JWT.Generate(user.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": jwt,
|
||||
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == nil && req.Username == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Missing both email and username in body",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userFindRequestArguments := make([]interface{}, 1)
|
||||
userFindRequest := ""
|
||||
|
||||
if req.Email != nil {
|
||||
userFindRequestArguments[0] = &req.Email
|
||||
userFindRequest += "email = ?"
|
||||
}
|
||||
|
||||
if req.Username != nil {
|
||||
userFindRequestArguments[0] = &req.Username
|
||||
userFindRequest += "username = ?"
|
||||
}
|
||||
|
||||
var user *dbcore.User
|
||||
userRequest := dbcore.DB.Where(userFindRequest, userFindRequestArguments...).Find(&user)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if user exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
decodedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(user.Password)))
|
||||
_, err := base64.StdEncoding.Decode(decodedPassword, []byte(user.Password))
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("failed to decode password in database: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse database result for password",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(decodedPassword, []byte(req.Password))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Invalid password",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tokenRandomData := make([]byte, 80)
|
||||
|
||||
if _, err := rand.Read(tokenRandomData); err != nil {
|
||||
log.Warnf("Failed to read random data to use as token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
token := &dbcore.Token{
|
||||
UserID: user.ID,
|
||||
|
||||
Token: base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
DisableExpiry: forceNoExpiryTokens,
|
||||
CreationIPAddr: c.ClientIP(),
|
||||
}
|
||||
|
||||
if result := dbcore.DB.Create(&token); result.Error != nil {
|
||||
log.Warnf("Failed to create user: %s", result.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to add refresh token into database",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := jwtcore.Generate(user.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": jwt,
|
||||
"refreshToken": base64.StdEncoding.EncodeToString(tokenRandomData),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type UserLookupRequest struct {
|
||||
|
@ -35,102 +34,104 @@ type LookupResponse struct {
|
|||
Data []*SanitizedUsers `json:"data"`
|
||||
}
|
||||
|
||||
func LookupUser(c *gin.Context) {
|
||||
var req UserLookupRequest
|
||||
func SetupLookupUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/lookup", func(c *gin.Context) {
|
||||
var req UserLookupRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
users := []db.User{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
|
||||
if !permissions.UserHasPermission(user, "users.lookup") {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, user.ID)
|
||||
} else if permissions.UserHasPermission(user, "users.lookup") && req.UID != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.UID)
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
queryString = append(queryString, "email = ?")
|
||||
queryParameters = append(queryParameters, req.Email)
|
||||
}
|
||||
|
||||
if req.IsBot != nil {
|
||||
queryString = append(queryString, "is_bot = ?")
|
||||
queryParameters = append(queryParameters, req.IsBot)
|
||||
}
|
||||
|
||||
if err := state.DB.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil {
|
||||
log.Warnf("Failed to get users: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
"error": "Failed to get users",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
users := []dbcore.User{}
|
||||
queryString := []string{}
|
||||
queryParameters := []interface{}{}
|
||||
sanitizedUsers := make([]*SanitizedUsers, len(users))
|
||||
|
||||
if !permissions.UserHasPermission(user, "users.lookup") {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, user.ID)
|
||||
} else if permissions.UserHasPermission(user, "users.lookup") && req.UID != nil {
|
||||
queryString = append(queryString, "id = ?")
|
||||
queryParameters = append(queryParameters, req.UID)
|
||||
}
|
||||
for userIndex, user := range users {
|
||||
isBot := false
|
||||
|
||||
if req.Name != nil {
|
||||
queryString = append(queryString, "name = ?")
|
||||
queryParameters = append(queryParameters, req.Name)
|
||||
}
|
||||
if user.IsBot != nil {
|
||||
isBot = *user.IsBot
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
queryString = append(queryString, "email = ?")
|
||||
queryParameters = append(queryParameters, req.Email)
|
||||
}
|
||||
sanitizedUsers[userIndex] = &SanitizedUsers{
|
||||
UID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
IsBot: isBot,
|
||||
}
|
||||
}
|
||||
|
||||
if req.IsBot != nil {
|
||||
queryString = append(queryString, "is_bot = ?")
|
||||
queryParameters = append(queryParameters, req.IsBot)
|
||||
}
|
||||
|
||||
if err := dbcore.DB.Where(strings.Join(queryString, " AND "), queryParameters...).Find(&users).Error; err != nil {
|
||||
log.Warnf("Failed to get users: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to get users",
|
||||
c.JSON(http.StatusOK, &LookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedUsers,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedUsers := make([]*SanitizedUsers, len(users))
|
||||
|
||||
for userIndex, user := range users {
|
||||
isBot := false
|
||||
|
||||
if user.IsBot != nil {
|
||||
isBot = *user.IsBot
|
||||
}
|
||||
|
||||
sanitizedUsers[userIndex] = &SanitizedUsers{
|
||||
UID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
IsBot: isBot,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &LookupResponse{
|
||||
Success: true,
|
||||
Data: sanitizedUsers,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,113 +5,114 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type UserRefreshRequest struct {
|
||||
Token string `validate:"required"`
|
||||
}
|
||||
|
||||
func RefreshUserToken(c *gin.Context) {
|
||||
var req UserRefreshRequest
|
||||
func SetupRefreshUserToken(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/refresh", func(c *gin.Context) {
|
||||
var req UserRefreshRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var tokenInDatabase *dbcore.Token
|
||||
tokenRequest := dbcore.DB.Where("token = ?", req.Token).Find(&tokenInDatabase)
|
||||
|
||||
if tokenRequest.Error != nil {
|
||||
log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if token exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tokenExists := tokenRequest.RowsAffected > 0
|
||||
|
||||
if !tokenExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Token not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// First, we check to make sure that the key expiry is disabled before checking if the key is expired.
|
||||
// Then, we check if the IP addresses differ, or if it has been 7 days since the token has been created.
|
||||
if !tokenInDatabase.DisableExpiry && (c.ClientIP() != tokenInDatabase.CreationIPAddr || time.Now().Before(tokenInDatabase.CreatedAt.Add((24*7)*time.Hour))) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Token has expired",
|
||||
})
|
||||
|
||||
tx := dbcore.DB.Delete(tokenInDatabase)
|
||||
|
||||
if tx.Error != nil {
|
||||
log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
// Get the user to check if the user exists before doing anything
|
||||
var user *dbcore.User
|
||||
userRequest := dbcore.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenRequest.Error != nil {
|
||||
log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error())
|
||||
var tokenInDatabase *db.Token
|
||||
tokenRequest := state.DB.DB.Where("token = ?", req.Token).Find(&tokenInDatabase)
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find user",
|
||||
if tokenRequest.Error != nil {
|
||||
log.Warnf("failed to find if token exists or not: %s", tokenRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if token exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tokenExists := tokenRequest.RowsAffected > 0
|
||||
|
||||
if !tokenExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Token not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// First, we check to make sure that the key expiry is disabled before checking if the key is expired.
|
||||
// Then, we check if the IP addresses differ, or if it has been 7 days since the token has been created.
|
||||
if !tokenInDatabase.DisableExpiry && (c.ClientIP() != tokenInDatabase.CreationIPAddr || time.Now().Before(tokenInDatabase.CreatedAt.Add((24*7)*time.Hour))) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Token has expired",
|
||||
})
|
||||
|
||||
tx := state.DB.DB.Delete(tokenInDatabase)
|
||||
|
||||
if tx.Error != nil {
|
||||
log.Warnf("Failed to delete expired token from database: %s", tx.Error.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get the user to check if the user exists before doing anything
|
||||
var user *db.User
|
||||
userRequest := state.DB.DB.Where("id = ?", tokenInDatabase.UserID).Find(&user)
|
||||
|
||||
if tokenRequest.Error != nil {
|
||||
log.Warnf("failed to find if token user or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find user",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "User not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := state.JWT.Generate(user.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": jwt,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "User not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := jwtcore.Generate(user.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to generate JWT: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate refresh token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": jwt,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,12 +4,11 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/permissions"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type UserRemovalRequest struct {
|
||||
|
@ -17,89 +16,91 @@ type UserRemovalRequest struct {
|
|||
UID *uint `json:"uid"`
|
||||
}
|
||||
|
||||
func RemoveUser(c *gin.Context) {
|
||||
var req UserRemovalRequest
|
||||
func SetupRemoveUser(state *state.State) {
|
||||
state.Engine.POST("/api/v1/users/remove", func(c *gin.Context) {
|
||||
var req UserRemovalRequest
|
||||
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := jwtcore.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uid := user.ID
|
||||
|
||||
if req.UID != nil {
|
||||
uid = *req.UID
|
||||
|
||||
if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the user exists first if we have a custom UserID
|
||||
|
||||
if uid != user.ID {
|
||||
var customUser *dbcore.User
|
||||
userRequest := dbcore.DB.Where("id = ?", uid).Find(customUser)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if user exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
if err := c.BindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User doesn't exist",
|
||||
"error": fmt.Sprintf("Failed to parse body: %s", err.Error()),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dbcore.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
|
||||
if err := state.Validator.Struct(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Failed to validate body: %s", err.Error()),
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
return
|
||||
}
|
||||
|
||||
user, err := state.JWT.GetUserFromJWT(req.Token)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "token is expired" || err.Error() == "user does not exist" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to get user from the provided JWT token: %s", err.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to parse token",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uid := user.ID
|
||||
|
||||
if req.UID != nil {
|
||||
uid = *req.UID
|
||||
|
||||
if uid != user.ID && !permissions.UserHasPermission(user, "users.remove") {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Missing permissions",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the user exists first if we have a custom UserID
|
||||
|
||||
if uid != user.ID {
|
||||
var customUser *db.User
|
||||
userRequest := state.DB.DB.Where("id = ?", uid).Find(customUser)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
log.Warnf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to find if user exists",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "User doesn't exist",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
state.DB.DB.Select("Tokens", "Permissions", "Proxys", "Backends").Where("id = ?", uid).Delete(user)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
77
backend/api/db/db.go
Normal file
77
backend/api/db/db.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func New(backend, params string) (*DB, error) {
|
||||
var err error
|
||||
|
||||
dialector, err := initDialector(backend, params)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize physical database: %s", err)
|
||||
}
|
||||
|
||||
database, err := gorm.Open(dialector)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %s", err)
|
||||
}
|
||||
|
||||
return &DB{DB: database}, nil
|
||||
}
|
||||
|
||||
func (db *DB) DoMigrations() error {
|
||||
if err := db.DB.AutoMigrate(&Proxy{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.DB.AutoMigrate(&Backend{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.DB.AutoMigrate(&Permission{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.DB.AutoMigrate(&Token{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.DB.AutoMigrate(&User{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initDialector(backend, params string) (gorm.Dialector, error) {
|
||||
switch backend {
|
||||
case "sqlite":
|
||||
if params == "" {
|
||||
return nil, fmt.Errorf("sqlite database file not specified")
|
||||
}
|
||||
|
||||
return sqlite.Open(params), nil
|
||||
case "postgresql":
|
||||
if params == "" {
|
||||
return nil, fmt.Errorf("postgres DSN not specified")
|
||||
}
|
||||
|
||||
return postgres.Open(params), nil
|
||||
case "":
|
||||
return nil, fmt.Errorf("no database backend specified in environment variables")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(backend))
|
||||
}
|
||||
}
|
66
backend/api/db/models.go
Normal file
66
backend/api/db/models.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
gorm.Model
|
||||
|
||||
UserID uint
|
||||
|
||||
Name string
|
||||
Description *string
|
||||
Backend string
|
||||
BackendParameters string
|
||||
|
||||
Proxies []Proxy
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
gorm.Model
|
||||
|
||||
BackendID uint
|
||||
UserID uint
|
||||
|
||||
Name string
|
||||
Description *string
|
||||
Protocol string
|
||||
SourceIP string
|
||||
SourcePort uint16
|
||||
DestinationPort uint16
|
||||
AutoStart bool
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
gorm.Model
|
||||
|
||||
PermissionNode string
|
||||
HasPermission bool
|
||||
UserID uint
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
gorm.Model
|
||||
|
||||
UserID uint
|
||||
|
||||
Token string
|
||||
DisableExpiry bool
|
||||
CreationIPAddr string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
|
||||
Email string `gorm:"unique"`
|
||||
Username string `gorm:"unique"`
|
||||
Name string
|
||||
Password string
|
||||
IsBot *bool
|
||||
|
||||
Permissions []Permission
|
||||
OwnedProxies []Proxy
|
||||
OwnedBackends []Backend
|
||||
Tokens []Token
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package dbcore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
gorm.Model
|
||||
|
||||
UserID uint
|
||||
|
||||
Name string
|
||||
Description *string
|
||||
Backend string
|
||||
BackendParameters string
|
||||
|
||||
Proxies []Proxy
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
gorm.Model
|
||||
|
||||
BackendID uint
|
||||
UserID uint
|
||||
|
||||
Name string
|
||||
Description *string
|
||||
Protocol string
|
||||
SourceIP string
|
||||
SourcePort uint16
|
||||
DestinationPort uint16
|
||||
AutoStart bool
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
gorm.Model
|
||||
|
||||
PermissionNode string
|
||||
HasPermission bool
|
||||
UserID uint
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
gorm.Model
|
||||
|
||||
UserID uint
|
||||
|
||||
Token string
|
||||
DisableExpiry bool
|
||||
CreationIPAddr string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
|
||||
Email string `gorm:"unique"`
|
||||
Username string `gorm:"unique"`
|
||||
Name string
|
||||
Password string
|
||||
IsBot *bool
|
||||
|
||||
Permissions []Permission
|
||||
OwnedProxies []Proxy
|
||||
OwnedBackends []Backend
|
||||
Tokens []Token
|
||||
}
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func InitializeDatabaseDialector() (gorm.Dialector, error) {
|
||||
databaseBackend := os.Getenv("HERMES_DATABASE_BACKEND")
|
||||
|
||||
switch databaseBackend {
|
||||
case "sqlite":
|
||||
filePath := os.Getenv("HERMES_SQLITE_FILEPATH")
|
||||
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("sqlite database file not specified (missing HERMES_SQLITE_FILEPATH)")
|
||||
}
|
||||
|
||||
return sqlite.Open(filePath), nil
|
||||
case "postgresql":
|
||||
postgresDSN := os.Getenv("HERMES_POSTGRES_DSN")
|
||||
|
||||
if postgresDSN == "" {
|
||||
return nil, fmt.Errorf("postgres DSN not specified (missing HERMES_POSTGRES_DSN)")
|
||||
}
|
||||
|
||||
return postgres.Open(postgresDSN), nil
|
||||
case "":
|
||||
return nil, fmt.Errorf("no database backend specified in environment variables (missing HERMES_DATABASE_BACKEND)")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown database backend specified: %s", os.Getenv(databaseBackend))
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeDatabase(config *gorm.Config) error {
|
||||
var err error
|
||||
|
||||
dialector, err := InitializeDatabaseDialector()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize physical database: %s", err)
|
||||
}
|
||||
|
||||
DB, err = gorm.Open(dialector, config)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DoDatabaseMigrations(db *gorm.DB) error {
|
||||
if err := db.AutoMigrate(&Proxy{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&Backend{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&Permission{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&Token{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&User{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
107
backend/api/jwt/jwt.go
Normal file
107
backend/api/jwt/jwt.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
DevelopmentModeTimings = time.Duration(60*24) * time.Minute
|
||||
NormalModeTimings = time.Duration(3) * time.Minute
|
||||
)
|
||||
|
||||
type JWTCore struct {
|
||||
Key []byte
|
||||
Database *db.DB
|
||||
TimeMultiplier time.Duration
|
||||
}
|
||||
|
||||
func New(key []byte, database *db.DB, timeMultiplier time.Duration) *JWTCore {
|
||||
jwtCore := &JWTCore{
|
||||
Key: key,
|
||||
Database: database,
|
||||
TimeMultiplier: timeMultiplier,
|
||||
}
|
||||
|
||||
return jwtCore
|
||||
}
|
||||
|
||||
func (jwtCore *JWTCore) Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) {
|
||||
return jwt.Parse(tokenString, jwtCore.jwtKeyCallback, options...)
|
||||
}
|
||||
|
||||
func (jwtCore *JWTCore) GetUserFromJWT(token string) (*db.User, error) {
|
||||
if jwtCore.Database == nil {
|
||||
return nil, fmt.Errorf("database is not initialized")
|
||||
}
|
||||
|
||||
parsedJWT, err := jwtCore.Parse(token)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, fmt.Errorf("token is expired")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
audience, err := parsedJWT.Claims.GetAudience()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(audience) < 1 {
|
||||
return nil, fmt.Errorf("audience is too small")
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(audience[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &db.User{}
|
||||
userRequest := jwtCore.Database.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
return user, fmt.Errorf("user does not exist")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (jwtCore *JWTCore) Generate(uid uint) (string, error) {
|
||||
currentJWTTime := jwt.NewNumericDate(time.Now())
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtCore.TimeMultiplier)),
|
||||
IssuedAt: currentJWTTime,
|
||||
NotBefore: currentJWTTime,
|
||||
// Convert the user ID to a string, and then set it as the audience parameters only value (there's only 1 user per key)
|
||||
Audience: []string{strconv.Itoa(int(uid))},
|
||||
})
|
||||
|
||||
signedToken, err := token.SignedString(jwtCore.Key)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
func (jwtCore *JWTCore) jwtKeyCallback(*jwt.Token) (any, error) {
|
||||
return jwtCore.Key, nil
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
package jwtcore
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
JWTKey []byte
|
||||
developmentMode bool
|
||||
)
|
||||
|
||||
func SetupJWT() error {
|
||||
var err error
|
||||
jwtDataString := os.Getenv("HERMES_JWT_SECRET")
|
||||
|
||||
if jwtDataString == "" {
|
||||
return fmt.Errorf("JWT secret isn't set (missing HERMES_JWT_SECRET)")
|
||||
}
|
||||
|
||||
if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" {
|
||||
JWTKey, err = base64.StdEncoding.DecodeString(jwtDataString)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode base64 JWT: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
JWTKey = []byte(jwtDataString)
|
||||
}
|
||||
|
||||
if os.Getenv("HERMES_DEVELOPMENT_MODE") != "" {
|
||||
developmentMode = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Parse(tokenString string, options ...jwt.ParserOption) (*jwt.Token, error) {
|
||||
return jwt.Parse(tokenString, JWTKeyCallback, options...)
|
||||
}
|
||||
|
||||
func GetUserFromJWT(token string) (*dbcore.User, error) {
|
||||
parsedJWT, err := Parse(token)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, fmt.Errorf("token is expired")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
audience, err := parsedJWT.Claims.GetAudience()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(audience) < 1 {
|
||||
return nil, fmt.Errorf("audience is too small")
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(audience[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &dbcore.User{}
|
||||
userRequest := dbcore.DB.Preload("Permissions").Where("id = ?", uint(uid)).Find(&user)
|
||||
|
||||
if userRequest.Error != nil {
|
||||
return user, fmt.Errorf("failed to find if user exists or not: %s", userRequest.Error.Error())
|
||||
}
|
||||
|
||||
userExists := userRequest.RowsAffected > 0
|
||||
|
||||
if !userExists {
|
||||
return user, fmt.Errorf("user does not exist")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func Generate(uid uint) (string, error) {
|
||||
timeMultiplier := 3
|
||||
|
||||
if developmentMode {
|
||||
timeMultiplier = 60 * 24
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeMultiplier) * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Audience: []string{strconv.Itoa(int(uid))},
|
||||
})
|
||||
|
||||
signedToken, err := token.SignedString(JWTKey)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
func JWTKeyCallback(*jwt.Token) (interface{}, error) {
|
||||
return JWTKey, nil
|
||||
}
|
|
@ -9,18 +9,19 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
|
||||
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/backends"
|
||||
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/proxies"
|
||||
"git.terah.dev/imterah/hermes/backend/api/controllers/v1/users"
|
||||
"git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwt"
|
||||
"git.terah.dev/imterah/hermes/backend/api/state"
|
||||
"git.terah.dev/imterah/hermes/backend/commonbackend"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func apiEntrypoint(cCtx *cli.Context) error {
|
||||
|
@ -34,7 +35,26 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
log.Info("Hermes is initializing...")
|
||||
log.Debug("Initializing database and opening it...")
|
||||
|
||||
err := dbcore.InitializeDatabase(&gorm.Config{})
|
||||
databaseBackendName := os.Getenv("HERMES_DATABASE_BACKEND")
|
||||
var databaseBackendParams string
|
||||
|
||||
if databaseBackendName == "sqlite" {
|
||||
databaseBackendParams = os.Getenv("HERMES_SQLITE_FILEPATH")
|
||||
|
||||
if databaseBackendParams == "" {
|
||||
log.Fatal("HERMES_SQLITE_FILEPATH is not set")
|
||||
}
|
||||
} else if databaseBackendName == "postgresql" {
|
||||
databaseBackendParams = os.Getenv("HERMES_POSTGRES_DSN")
|
||||
|
||||
if databaseBackendParams == "" {
|
||||
log.Fatal("HERMES_POSTGRES_DSN is not set")
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("Unsupported database backend: %s", databaseBackendName)
|
||||
}
|
||||
|
||||
dbInstance, err := db.New(databaseBackendName, databaseBackendParams)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %s", err)
|
||||
|
@ -42,16 +62,38 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Debug("Running database migrations...")
|
||||
|
||||
if err := dbcore.DoDatabaseMigrations(dbcore.DB); err != nil {
|
||||
if err := dbInstance.DoMigrations(); err != nil {
|
||||
return fmt.Errorf("Failed to run database migrations: %s", err)
|
||||
}
|
||||
|
||||
log.Debug("Initializing the JWT subsystem...")
|
||||
|
||||
if err := jwtcore.SetupJWT(); err != nil {
|
||||
return fmt.Errorf("Failed to initialize the JWT subsystem: %s", err.Error())
|
||||
jwtDataString := os.Getenv("HERMES_JWT_SECRET")
|
||||
var jwtKey []byte
|
||||
var jwtValidityTimeDuration time.Duration
|
||||
|
||||
if jwtDataString == "" {
|
||||
log.Fatalf("HERMES_JWT_SECRET is not set")
|
||||
}
|
||||
|
||||
if os.Getenv("HERMES_JWT_BASE64_ENCODED") != "" {
|
||||
jwtKey, err = base64.StdEncoding.DecodeString(jwtDataString)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode base64 JWT: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
jwtKey = []byte(jwtDataString)
|
||||
}
|
||||
|
||||
if developmentMode {
|
||||
jwtValidityTimeDuration = jwt.DevelopmentModeTimings
|
||||
} else {
|
||||
jwtValidityTimeDuration = jwt.NormalModeTimings
|
||||
}
|
||||
|
||||
jwtInstance := jwt.New(jwtKey, dbInstance, jwtValidityTimeDuration)
|
||||
|
||||
log.Debug("Initializing the backend subsystem...")
|
||||
|
||||
backendMetadataPath := cCtx.String("backends-path")
|
||||
|
@ -76,9 +118,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Debug("Enumerating backends...")
|
||||
|
||||
backendList := []dbcore.Backend{}
|
||||
backendList := []db.Backend{}
|
||||
|
||||
if err := dbcore.DB.Find(&backendList).Error; err != nil {
|
||||
if err := dbInstance.DB.Find(&backendList).Error; err != nil {
|
||||
return fmt.Errorf("Failed to enumerate backends: %s", err.Error())
|
||||
}
|
||||
|
||||
|
@ -141,9 +183,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Warnf("Backend #%d has reinitialized! Starting up auto-starting proxies...", backend.ID)
|
||||
|
||||
autoStartProxies := []dbcore.Proxy{}
|
||||
autoStartProxies := []db.Proxy{}
|
||||
|
||||
if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -243,9 +285,9 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
|
||||
log.Infof("Successfully initialized backend #%d", backend.ID)
|
||||
|
||||
autoStartProxies := []dbcore.Proxy{}
|
||||
autoStartProxies := []db.Proxy{}
|
||||
|
||||
if err := dbcore.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
if err := dbInstance.DB.Where("backend_id = ? AND auto_start = true", backend.ID).Find(&autoStartProxies).Error; err != nil {
|
||||
log.Errorf("Failed to query proxies to autostart: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
@ -309,23 +351,25 @@ func apiEntrypoint(cCtx *cli.Context) error {
|
|||
engine.SetTrustedProxies(nil)
|
||||
}
|
||||
|
||||
state := state.New(dbInstance, jwtInstance, engine)
|
||||
|
||||
// Initialize routes
|
||||
engine.POST("/api/v1/users/create", users.CreateUser)
|
||||
engine.POST("/api/v1/users/login", users.LoginUser)
|
||||
engine.POST("/api/v1/users/refresh", users.RefreshUserToken)
|
||||
engine.POST("/api/v1/users/remove", users.RemoveUser)
|
||||
engine.POST("/api/v1/users/lookup", users.LookupUser)
|
||||
users.SetupCreateUser(state)
|
||||
users.SetupLoginUser(state)
|
||||
users.SetupRefreshUserToken(state)
|
||||
users.SetupRemoveUser(state)
|
||||
users.SetupLookupUser(state)
|
||||
|
||||
engine.POST("/api/v1/backends/create", backends.CreateBackend)
|
||||
engine.POST("/api/v1/backends/remove", backends.RemoveBackend)
|
||||
engine.POST("/api/v1/backends/lookup", backends.LookupBackend)
|
||||
backends.SetupCreateBackend(state)
|
||||
backends.SetupRemoveBackend(state)
|
||||
backends.SetupLookupBackend(state)
|
||||
|
||||
engine.POST("/api/v1/forward/create", proxies.CreateProxy)
|
||||
engine.POST("/api/v1/forward/lookup", proxies.LookupProxy)
|
||||
engine.POST("/api/v1/forward/remove", proxies.RemoveProxy)
|
||||
engine.POST("/api/v1/forward/start", proxies.StartProxy)
|
||||
engine.POST("/api/v1/forward/stop", proxies.StopProxy)
|
||||
engine.POST("/api/v1/forward/connections", proxies.GetConnections)
|
||||
proxies.SetupCreateProxy(state)
|
||||
proxies.SetupRemoveProxy(state)
|
||||
proxies.SetupLookupProxy(state)
|
||||
proxies.SetupStartProxy(state)
|
||||
proxies.SetupStopProxy(state)
|
||||
proxies.SetupGetConnections(state)
|
||||
|
||||
log.Infof("Listening on '%s'", listeningAddress)
|
||||
err = engine.Run(listeningAddress)
|
||||
|
@ -362,22 +406,6 @@ func main() {
|
|||
app := &cli.App{
|
||||
Name: "hermes",
|
||||
Usage: "port forwarding across boundaries",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "import",
|
||||
Usage: "imports from legacy NextNet/Hermes source",
|
||||
Aliases: []string{"i"},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "backup-path",
|
||||
Aliases: []string{"bp"},
|
||||
Usage: "path to the backup file",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Action: backupRestoreEntrypoint,
|
||||
},
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "backends-path",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package permissions
|
||||
|
||||
import "git.terah.dev/imterah/hermes/backend/api/dbcore"
|
||||
import "git.terah.dev/imterah/hermes/backend/api/db"
|
||||
|
||||
var DefaultPermissionNodes []string = []string{
|
||||
"routes.add",
|
||||
|
@ -27,7 +27,7 @@ var DefaultPermissionNodes []string = []string{
|
|||
"users.edit",
|
||||
}
|
||||
|
||||
func UserHasPermission(user *dbcore.User, node string) bool {
|
||||
func UserHasPermission(user *db.User, node string) bool {
|
||||
for _, permission := range user.Permissions {
|
||||
if permission.PermissionNode == node && permission.HasPermission {
|
||||
return true
|
||||
|
|
24
backend/api/state/state.go
Normal file
24
backend/api/state/state.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"git.terah.dev/imterah/hermes/backend/api/db"
|
||||
"git.terah.dev/imterah/hermes/backend/api/jwt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
DB *db.DB
|
||||
JWT *jwt.JWTCore
|
||||
Engine *gin.Engine
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
func New(db *db.DB, jwt *jwt.JWTCore, engine *gin.Engine) *State {
|
||||
return &State{
|
||||
DB: db,
|
||||
JWT: jwt,
|
||||
Engine: engine,
|
||||
Validator: validator.New(),
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var validatorInstance *validator.Validate
|
||||
|
||||
type TCPProxy struct {
|
||||
proxyInformation *commonbackend.AddProxy
|
||||
connections map[uint16]net.Conn
|
||||
|
@ -62,6 +64,11 @@ type SSHAppBackend struct {
|
|||
|
||||
func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) {
|
||||
log.Info("SSHAppBackend is initializing...")
|
||||
|
||||
if validatorInstance == nil {
|
||||
validatorInstance = validator.New()
|
||||
}
|
||||
|
||||
backend.globalNonCriticalMessageChan = make(chan interface{})
|
||||
backend.tcpProxies = map[uint16]*TCPProxy{}
|
||||
backend.udpProxies = map[uint16]*UDPProxy{}
|
||||
|
@ -72,7 +79,7 @@ func (backend *SSHAppBackend) StartBackend(configBytes []byte) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -585,6 +592,10 @@ func (backend *SSHAppBackend) CheckParametersForConnections(clientParameters *co
|
|||
func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse {
|
||||
var backendData SSHAppBackendData
|
||||
|
||||
if validatorInstance == nil {
|
||||
validatorInstance = validator.New()
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(arguments, &backendData); err != nil {
|
||||
return &commonbackend.CheckParametersResponse{
|
||||
IsValid: false,
|
||||
|
@ -592,7 +603,7 @@ func (backend *SSHAppBackend) CheckParametersForBackend(arguments []byte) *commo
|
|||
}
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
return &commonbackend.CheckParametersResponse{
|
||||
IsValid: false,
|
||||
Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()),
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -18,6 +19,34 @@ 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
|
||||
|
@ -26,31 +55,46 @@ type SSHListener struct {
|
|||
Listeners []net.Listener
|
||||
}
|
||||
|
||||
type SSHBackendData struct {
|
||||
IP string `json:"ip" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
PrivateKey string `json:"privateKey" validate:"required"`
|
||||
DisablePIDCheck bool `json:"disablePIDCheck"`
|
||||
ListenOnIPs []string `json:"listenOnIPs"`
|
||||
}
|
||||
|
||||
type SSHBackend struct {
|
||||
config *SSHBackendData
|
||||
conn *ssh.Client
|
||||
clients []*commonbackend.ProxyClientConnection
|
||||
proxies []*SSHListener
|
||||
arrayPropMutex sync.Mutex
|
||||
}
|
||||
|
||||
type SSHBackendData struct {
|
||||
IP string `json:"ip" validate:"required"`
|
||||
Port uint16 `json:"port" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
PrivateKey string `json:"privateKey" validate:"required"`
|
||||
ListenOnIPs []string `json:"listenOnIPs"`
|
||||
pid int
|
||||
isReady bool
|
||||
inReinitLoop bool
|
||||
}
|
||||
|
||||
func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) {
|
||||
log.Info("SSHBackend is initializing...")
|
||||
|
||||
if 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 := validator.New().Struct(&backendData); err != nil {
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -76,16 +120,70 @@ func (backend *SSHBackend) StartBackend(bytes []byte) (bool, error) {
|
|||
},
|
||||
}
|
||||
|
||||
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backendData.IP, backendData.Port), config)
|
||||
addr := fmt.Sprintf("%s:%d", backendData.IP, backendData.Port)
|
||||
timeout := time.Duration(10 * time.Second)
|
||||
|
||||
rawTCPConn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
backend.conn = conn
|
||||
connWithTimeout := &ConnWithTimeout{
|
||||
Conn: rawTCPConn,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
}
|
||||
|
||||
c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := ssh.NewClient(c, chans, reqs)
|
||||
backend.conn = client
|
||||
|
||||
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()
|
||||
|
||||
log.Info("SSHBackend has initialized successfully.")
|
||||
go backend.backendDisconnectHandler()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
@ -203,8 +301,7 @@ func (backend *SSHBackend) StartProxy(command *commonbackend.AddProxy) (bool, er
|
|||
// Splice out the clientInstance by clientIndex
|
||||
|
||||
// TODO: change approach. It works but it's a bit wonky imho
|
||||
// I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively
|
||||
backend.clients = append(backend.clients[:clientIndex], backend.clients[clientIndex+1:]...)
|
||||
backend.clients = slices.Delete(backend.clients, clientIndex, clientIndex+1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -288,10 +385,8 @@ func (backend *SSHBackend) StopProxy(command *commonbackend.RemoveProxy) (bool,
|
|||
}
|
||||
|
||||
// Splice out the proxy instance by proxyIndex
|
||||
|
||||
// TODO: change approach. It works but it's a bit wonky imho
|
||||
// I asked AI to do this as it's a relatively simple task and I forgot how to do this effectively
|
||||
backend.proxies = append(backend.proxies[:proxyIndex], backend.proxies[proxyIndex+1:]...)
|
||||
backend.proxies = slices.Delete(backend.proxies, proxyIndex, proxyIndex+1)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
@ -322,6 +417,10 @@ func (backend *SSHBackend) CheckParametersForConnections(clientParameters *commo
|
|||
func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonbackend.CheckParametersResponse {
|
||||
var backendData SSHBackendData
|
||||
|
||||
if validatorInstance == nil {
|
||||
validatorInstance = validator.New()
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(arguments, &backendData); err != nil {
|
||||
return &commonbackend.CheckParametersResponse{
|
||||
IsValid: false,
|
||||
|
@ -329,7 +428,7 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba
|
|||
}
|
||||
}
|
||||
|
||||
if err := validator.New().Struct(&backendData); err != nil {
|
||||
if err := validatorInstance.Struct(&backendData); err != nil {
|
||||
return &commonbackend.CheckParametersResponse{
|
||||
IsValid: false,
|
||||
Message: fmt.Sprintf("failed validation of parameters: %s", err.Error()),
|
||||
|
@ -341,17 +440,34 @@ func (backend *SSHBackend) CheckParametersForBackend(arguments []byte) *commonba
|
|||
}
|
||||
}
|
||||
|
||||
func (backend *SSHBackend) backendDisconnectHandler() {
|
||||
func (backend *SSHBackend) backendKeepaliveHandler() {
|
||||
for {
|
||||
if backend.conn != nil {
|
||||
err := backend.conn.Wait()
|
||||
_, _, err := backend.conn.SendRequest("keepalive@openssh.com", true, nil)
|
||||
|
||||
if err == nil || err.Error() != "EOF" {
|
||||
continue
|
||||
if err != nil {
|
||||
log.Warn("Keepalive message failed!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Disconnected from the remote SSH server. Attempting to reconnect in 5 seconds...")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *SSHBackend) backendDisconnectHandler() {
|
||||
for {
|
||||
if backend.conn != nil {
|
||||
backend.conn.Wait()
|
||||
backend.conn.Close()
|
||||
|
||||
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...")
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
|
@ -376,14 +492,74 @@ func (backend *SSHBackend) backendDisconnectHandler() {
|
|||
},
|
||||
}
|
||||
|
||||
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port), config)
|
||||
addr := fmt.Sprintf("%s:%d", backend.config.IP, backend.config.Port)
|
||||
timeout := time.Duration(10 * time.Second)
|
||||
|
||||
rawTCPConn, err := net.DialTimeout("tcp", addr, timeout)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to connect to the server: %s", err.Error())
|
||||
return
|
||||
log.Errorf("Failed to establish connection to the server: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
backend.conn = conn
|
||||
connWithTimeout := &ConnWithTimeout{
|
||||
Conn: rawTCPConn,
|
||||
ReadTimeout: timeout,
|
||||
WriteTimeout: timeout,
|
||||
}
|
||||
|
||||
c, chans, reqs, err := ssh.NewClientConn(connWithTimeout, addr, config)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create SSH client connection: %s", err.Error())
|
||||
rawTCPConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
client := ssh.NewClient(c, chans, reqs)
|
||||
backend.conn = client
|
||||
|
||||
if !backend.config.DisablePIDCheck {
|
||||
if backend.pid != 0 {
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to create SSH command session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = session.Run(fmt.Sprintf("kill -9 %d", backend.pid))
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to kill process: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
session, err := client.NewSession()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to create SSH command session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get the parent PID of the shell so we can kill it if we disconnect
|
||||
output, err := session.Output("ps --no-headers -fp $$ | awk '{print $3}'")
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to execute command to fetch PID: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Strip the new line and convert to int
|
||||
backend.pid, err = strconv.Atoi(string(output)[:len(output)-1])
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse PID: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go backend.backendKeepaliveHandler()
|
||||
|
||||
log.Info("SSHBackend has reconnected successfully. Attempting to set up proxies again...")
|
||||
|
||||
|
|
|
@ -6,27 +6,12 @@ services:
|
|||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB}?schema=nextnet
|
||||
HERMES_POSTGRES_DSN: postgres://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@nextnet-postgres:5432/${POSTGRES_DB}
|
||||
HERMES_JWT_SECRET: ${JWT_SECRET}
|
||||
HERMES_DATABASE_BACKEND: postgresql
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
# WARN: The LOM is deprecated and likely broken currently.
|
||||
#
|
||||
# NOTE: For this to work correctly, the nextnet-api must be version > 0.1.1
|
||||
# or have a version with backported username support, incl. logins
|
||||
lom:
|
||||
image: ghcr.io/imterah/hermes-lom:latest
|
||||
container_name: hermes-lom
|
||||
restart: always
|
||||
ports:
|
||||
- 2222:2222
|
||||
depends_on:
|
||||
- api
|
||||
volumes:
|
||||
- ssh_key_data:/app/keys
|
||||
|
||||
db:
|
||||
image: postgres:17.2
|
||||
container_name: nextnet-postgres
|
||||
|
@ -37,7 +22,6 @@ services:
|
|||
POSTGRES_USER: ${POSTGRES_USERNAME}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
ssh_key_data:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# These are default values, please change these!
|
||||
|
||||
POSTGRES_USERNAME=hermes
|
||||
POSTGRES_PASSWORD=hermes
|
||||
POSTGRES_DB=hermes
|
||||
JWT_SECRET=hermes
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
}: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# api/
|
||||
nodejs
|
||||
go
|
||||
gopls
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue