573 lines
14 KiB
Go
573 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
|
|
}
|
|
|
|
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) ServerAuthorized(permissions models.Permission) func(*gin.Context) {
|
|
return func(ctx *gin.Context) {
|
|
claimsPointer, exists := ctx.Get("claims")
|
|
if !exists {
|
|
ctx.AbortWithStatus(403)
|
|
return
|
|
}
|
|
|
|
claims := claimsPointer.(*auth.AuthClaims)
|
|
|
|
serverId := ctx.Param("server_id")
|
|
if serverId == "" {
|
|
ctx.AbortWithStatus(403)
|
|
return
|
|
}
|
|
|
|
userPermissions, err := con.ServerAuthorization.GetPermissions(ctx, claims.Username, serverId)
|
|
if err != nil {
|
|
ctx.AbortWithError(500, err)
|
|
return
|
|
}
|
|
|
|
if userPermissions&permissions == permissions || userPermissions&models.Admin == models.Admin {
|
|
return
|
|
}
|
|
|
|
ctx.AbortWithStatus(403)
|
|
return
|
|
}
|
|
}
|
|
|
|
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.AddPermissions(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 instanceServer.Running {
|
|
ctx.Status(200)
|
|
return
|
|
}
|
|
|
|
server, err := con.ServersDbHandler.GetServer(ctx, serverId)
|
|
if err != nil {
|
|
ctx.AbortWithError(500, err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
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), connection.StartServer)
|
|
group.POST("", auth.AuthorizedTo(models.Create), connection.CreateServer)
|
|
group.GET("", auth.AuthorizedTo(0), connection.GetServers)
|
|
group.POST("/:server_id/stop", auth.AuthorizedTo(models.Stop), connection.ServerAuthorized(models.Stop), connection.StopServer)
|
|
group.DELETE("/:server_id", auth.AuthorizedTo(models.Delete), connection.ServerAuthorized(models.Delete), connection.DeleteServer)
|
|
group.POST("/:server_id/run_command", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), connection.RunCommand)
|
|
group.GET("/:server_id/attach", auth.AuthorizedTo(models.RunCommand), connection.ServerAuthorized(models.RunCommand), connection.AttachServer)
|
|
group.PATCH("/:server_id", auth.AuthorizedTo(models.Admin), connection.ServerAuthorized(models.Admin), connection.UpdateServer)
|
|
group.POST("/:server_id/browse", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), connection.BrowseServer)
|
|
group.GET("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), connection.GetServerUserPermissions)
|
|
group.POST("/:server_id/permissions", auth.AuthorizedTo(models.Browse), connection.ServerAuthorized(models.Admin), connection.SetServerUserPermissions)
|
|
}
|