feature: Change state management from global variables to object passing

This restructures dbcore (now the db package) and jwtcore (now the jwt
package) to use a single struct. There is now a state package, which
contains a struct with the full application state.

After this, instead of initializing the API routes directly in the main
function, the state object gets passed, and the API routes get
initialized with their accompanying code.

One fix done to reduce memory usage and increase speed is that the
validator object is now persistent across requests, instead of
recreating it each time. This should speed things up slightly, and
improve memory usage.

One additional chore done is that the database models have been moved to
be a seperate file from the DB initialization itself.
This commit is contained in:
Tera << 8 2025-03-21 12:59:51 -04:00
parent 71d53990de
commit d56a8eb7bf
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
23 changed files with 1901 additions and 2161 deletions

View file

@ -5,13 +5,12 @@ import (
"net/http"
"git.terah.dev/imterah/hermes/backend/api/backendruntime"
"git.terah.dev/imterah/hermes/backend/api/dbcore"
"git.terah.dev/imterah/hermes/backend/api/jwtcore"
"git.terah.dev/imterah/hermes/backend/api/db"
"git.terah.dev/imterah/hermes/backend/api/permissions"
"git.terah.dev/imterah/hermes/backend/api/state"
"git.terah.dev/imterah/hermes/backend/commonbackend"
"github.com/charmbracelet/log"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type ProxyCreationRequest struct {
@ -26,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,
})
})
}