backend/servers/servers.go
2025-03-24 09:45:02 +02:00

550 lines
14 KiB
Go

package servers
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"git.acooldomain.co/server-manager/backend/auth"
"git.acooldomain.co/server-manager/backend/dbhandler"
"git.acooldomain.co/server-manager/backend/factories"
instancemanager "git.acooldomain.co/server-manager/backend/instancemanager"
"git.acooldomain.co/server-manager/backend/models"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type ServersApi struct {
ServersDbHandler dbhandler.ServersDbHandler
InstanceManager instancemanager.InstanceManager
ServerAuthorization dbhandler.ServersAuthorizationDbHandler
config models.GlobalConfig
}
type ImageInfo struct {
Name string `json:"Name"`
Version string `json:"Version"`
Ports []models.Port `json:"Ports"`
}
type ServerInfo struct {
Id string `json:"Id"`
OwnerId string `json:"OwnerId"`
DefaultCommand string `json:"DefaultCommand"`
Image ImageInfo `json:"Image"`
On bool `json:"On"`
Nickname string `json:"Nickname"`
Ports []models.Port `json:"Ports"`
Domain string `json:"Domain"`
}
type FileBrowserInfo struct {
Id string `json:"Id"`
OwnerId string `json:"OwnerId"`
ConnectedTo ServerInfo `json:"ConnectedTo"`
Url string `json:"Url"`
}
type CreateServerRequest struct {
ImageId string `json:"ImageId"`
DefaultPorts []models.Port `json:"DefaultPorts"`
DefaultCommand string `json:"DefaultCommand"`
Nickname string `json:"Nickname"`
}
func (con ServersApi) CreateServer(ctx *gin.Context) {
claims, exists := ctx.Get("claims")
if !exists {
ctx.AbortWithStatus(500)
return
}
serverClaims := claims.(*auth.AuthClaims)
var request CreateServerRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil {
ctx.AbortWithError(400, err)
return
}
instanceServer, err := con.InstanceManager.CreateServer(ctx)
if err != nil {
ctx.AbortWithError(500, err)
return
}
imageSegments := strings.Split(request.ImageId, ":")
registry := imageSegments[0]
tag := imageSegments[1]
err = con.ServersDbHandler.CreateServer(ctx, dbhandler.Server{
Id: instanceServer.Id,
Owner: serverClaims.Username,
Image: &models.Image{
Registry: registry,
Tag: tag,
},
Nickname: request.Nickname,
Command: request.DefaultCommand,
Ports: request.DefaultPorts,
})
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = con.ServerAuthorization.SetPermissions(ctx, serverClaims.Username, instanceServer.Id, models.Admin)
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, instanceServer.Id)
}
type PortMappingRequest struct {
Source models.Port
Destination models.Port
}
type StartServerRequest struct {
Command string `json:"Command"`
Ports []PortMappingRequest `json:"Ports"`
}
func (con ServersApi) StartServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
var request StartServerRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil {
ctx.AbortWithError(500, err)
return
}
instanceServer, err := con.InstanceManager.GetServer(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if instanceServer.Running {
ctx.Status(200)
return
}
server, err := con.ServersDbHandler.GetServer(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = con.InstanceManager.StartServer(
ctx,
instanceServer.Id,
server.Image.Registry+":"+server.Image.Tag,
server.Command,
server.Ports,
)
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, instanceServer.Id)
}
func (con ServersApi) GetServers(ctx *gin.Context) {
instanceServers, err := con.InstanceManager.ListServers(ctx)
if err != nil {
ctx.AbortWithError(500, err)
return
}
serverConfigs, err := con.ServersDbHandler.ListServers(ctx)
if err != nil {
ctx.AbortWithError(500, err)
return
}
serverConfigsMap := make(map[string]dbhandler.Server, len(serverConfigs))
for _, serverConfig := range serverConfigs {
serverConfigsMap[serverConfig.Id] = serverConfig
}
servers := make([]ServerInfo, len(instanceServers))
for i, instanceServer := range instanceServers {
server := serverConfigsMap[instanceServer.Id]
var image ImageInfo
if instanceServer.Running {
image = ImageInfo{
Name: instanceServer.RunningImage.Registry,
Version: instanceServer.RunningImage.Tag,
}
} else {
image = ImageInfo{
Name: server.Image.Registry,
Version: server.Image.Tag,
}
}
servers[i] = ServerInfo{
Id: instanceServer.Id,
Image: image,
OwnerId: server.Owner,
DefaultCommand: server.Command,
Ports: instanceServer.Ports,
On: instanceServer.Running,
Nickname: server.Nickname,
Domain: instanceServer.Domain,
}
}
ctx.JSON(200, servers)
}
func (con ServersApi) StopServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
err := con.InstanceManager.StopServer(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.Status(200)
}
func (con ServersApi) DeleteServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
err := con.InstanceManager.DeleteServer(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = con.ServersDbHandler.DeleteServer(ctx, serverId)
if err != nil {
ctx.AbortWithError(501, err)
return
}
ctx.JSON(200, "ok")
}
type RunCommandRequest struct {
Command string `json:"Command"`
}
func (con ServersApi) RunCommand(ctx *gin.Context) {
var request RunCommandRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil {
ctx.AbortWithError(500, err)
}
serverId := ctx.Param("server_id")
log.Print("Writing command \"", request.Command, "\"")
consolePointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
console := *consolePointer
defer console.Close()
_, err = console.Write([]byte(request.Command + "\n"))
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, "OK")
}
type Commands struct {
CommandType string `json:"CommandType"`
Arguments string `json:"Arguments"`
}
func (con ServersApi) AttachServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
stop := false
var err error
websocketRead := make(chan Commands)
containerRead := make(chan string)
defer func() {
if err != nil {
log.Printf("The latest error is %s", err)
}
close(websocketRead)
close(containerRead)
}()
hijackedPointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
hijacked := *hijackedPointer
defer hijacked.Close()
ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
ctx.AbortWithError(500, err)
return
}
defer ws.Close()
go func() {
data := make([]byte, 1024)
for {
if stop {
break
}
count, err := hijacked.Read(data)
if err != nil {
time.Sleep(500)
hijackedPointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
stop = true
return
}
hijacked = *hijackedPointer
}
if count > 0 {
containerRead <- string(data[:count])
}
}
}()
go func() {
var command Commands
for {
if stop {
break
}
err := ws.ReadJSON(&command)
if err != nil {
fmt.Printf("Failed to read from websocket: %s", err)
command.CommandType = "close"
websocketRead <- command
break
}
websocketRead <- command
}
}()
for {
if stop {
break
}
select {
case Command := <-websocketRead:
switch Command.CommandType {
case "insert":
_, err = hijacked.Write([]byte(Command.Arguments))
if err != nil {
log.Printf("Write to docker failed %s", errors.Unwrap(err))
stop = true
break
}
case "close":
stop = true
case "resize":
args := strings.Split(Command.Arguments, "x")
i_width, err2 := strconv.Atoi(args[0])
if err2 != nil {
break
}
width := uint(i_width)
i_height, err2 := strconv.Atoi(args[1])
if err2 != nil {
break
}
height := uint(i_height)
err2 = con.InstanceManager.ResizeTerminal(ctx, serverId, width, height)
if err2 != nil {
log.Printf("Failed to resize container to %dx%d: %s", width, height, err)
}
}
case data := <-containerRead:
err := ws.WriteJSON(data)
if err != nil {
log.Printf("Write to socket failed %s", errors.Unwrap(err))
stop = true
}
}
}
}
type UpdateServerRequest struct {
DefaultPorts []models.Port `json:"DefaultPorts"`
DefaultCommand string `json:"DefaultCommand"`
Nickname string `json:"Nickname"`
UserPermissions map[string]models.Permission `json:"UserPermissions"`
}
func (con ServersApi) UpdateServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
var request UpdateServerRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = con.ServersDbHandler.UpdateServer(ctx, serverId, dbhandler.ServerUpdateRequest{
Ports: request.DefaultPorts,
Nickname: request.Nickname,
Command: request.DefaultCommand,
})
if err != nil {
ctx.AbortWithError(500, err)
return
}
for user, permissions := range request.UserPermissions {
err = con.ServerAuthorization.SetPermissions(ctx, user, serverId, permissions)
if err != nil {
log.Printf("failed to change user %s permissions for server %s due to %e", user, serverId, err)
continue
}
}
ctx.JSON(200, "OK")
}
func (con ServersApi) BrowseServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
browserInfo, err := con.InstanceManager.StartFileBrowser(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, browserInfo.Url)
}
func (con ServersApi) GetServerUserPermissions(ctx *gin.Context) {
claims, exists := ctx.Get("claims")
if !exists {
ctx.AbortWithStatus(500)
return
}
authClaims := claims.(*auth.AuthClaims)
serverId := ctx.Param("server_id")
if serverId == "" {
ctx.AbortWithStatus(500)
return
}
permissions, err := con.ServerAuthorization.GetPermissions(ctx, authClaims.Username, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, permissions)
}
type SetServerUserPermissionsRequest struct {
Username string
Permissions models.Permission
}
func (con ServersApi) SetServerUserPermissions(ctx *gin.Context) {
serverId := ctx.Param("server_id")
if serverId == "" {
ctx.AbortWithStatus(500)
return
}
var request SetServerUserPermissionsRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = con.ServerAuthorization.SetPermissions(ctx, request.Username, serverId, request.Permissions)
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, "OK")
}
func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) {
instanceManager, err := factories.GetInstanceManager(config.InstanceManager, config.Domain)
if err != nil {
panic(err)
}
serversDbHandler, err := factories.GetServersDbHandler(config.ServersDatabase)
if err != nil {
panic(err)
}
serversAuthorizationHandler, err := factories.GetServersAuthorizationDbHandler(config.ServersAuthorizationDatabase)
if err != nil {
panic(err)
}
connection := ServersApi{
ServersDbHandler: serversDbHandler,
ServerAuthorization: serversAuthorizationHandler,
InstanceManager: instanceManager,
}
group.POST("/:server_id/start", auth.AuthorizedTo(models.Start), connection.ServerAuthorized(models.Start), auth.AuthorizationEnforcer(), connection.StartServer)
group.POST("", auth.AuthorizedTo(models.Create), auth.AuthorizationEnforcer(), connection.CreateServer)
group.GET("", connection.GetServers)
group.POST("/:server_id/stop", auth.AuthorizedTo(models.Stop), connection.ServerAuthorized(models.Stop), auth.AuthorizationEnforcer(), connection.StopServer)
group.DELETE("/:server_id", auth.AuthorizedTo(models.Delete), connection.ServerAuthorized(models.Delete), auth.AuthorizationEnforcer(), connection.DeleteServer)
group.POST("/:server_id/run_command", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), auth.AuthorizationEnforcer(), connection.RunCommand)
group.GET("/:server_id/attach", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), auth.AuthorizationEnforcer(), connection.AttachServer)
group.PATCH("/:server_id", auth.AuthorizedTo(models.Admin), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.UpdateServer)
group.POST("/:server_id/browse", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.BrowseServer)
group.GET("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.GetServerUserPermissions)
group.POST("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), auth.AuthorizationEnforcer(), connection.SetServerUserPermissions)
}