package servers import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" "acooldomain.co/backend/auth" "acooldomain.co/backend/models" "github.com/buildkite/shellwords" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" v1 "github.com/opencontainers/image-spec/specs-go/v1" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" ) type Connection struct { connection *mongo.Client apiClient *client.Client } type ContainerLabels struct { OwnerId string `json:"user_id"` ImageId string `json:"image_id"` VolumeId string `json:"volume_id"` Type string `json:"type"` } type VolumeLabels struct { OwnerId string `json:"user_id"` ImageId string `json:"image_id"` Type string `json:"type"` } type ImageLabels struct { Type string `json:"type"` } var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, error) { var volumeLabels VolumeLabels jsonData, err := json.Marshal(volume.Labels) if err != nil { return nil, err } json.Unmarshal(jsonData, &volumeLabels) imageList, err := con.apiClient.ImageList(context.Background(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("reference", volumeLabels.ImageId))}) if len(imageList) == 0 { return nil, fmt.Errorf("ImageId %s does not exist", volumeLabels.ImageId) } imageSummery := imageList[0] imageInspect, _, err := con.apiClient.ImageInspectWithRaw(context.Background(), imageSummery.ID) if err != nil { return nil, err } var imagePorts []models.Port = make([]models.Port, len(imageInspect.Config.ExposedPorts)) i := 0 for imagePort := range imageInspect.Config.ExposedPorts { imagePorts[i] = models.Port{Protocol: imagePort.Proto(), Number: imagePort.Int()} i += 1 } imageNameAndVersion := strings.Split(volumeLabels.ImageId, ":") imageName := imageNameAndVersion[0] imageVersion := imageNameAndVersion[1] containers, err := con.apiClient.ContainerList(context.Background(), container.ListOptions{ All: true, Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("label", fmt.Sprintf("volume_id=%s", volume.Name))), }) var state bool var ports []models.Port if err != nil || len(containers) == 0 { state = false ports = nil } else { container := containers[0] state = container.State == "running" ports = transformContainerPortsToModel(container.Ports) } var serverData models.ServerData con.connection.Database("backend").Collection("servers").FindOne(context.TODO(), bson.D{{Key: "volume_id", Value: volume.Name}}).Decode(&serverData) serverInfo := models.ServerInfo{ Id: volume.Name, Image: models.ImageInfo{ Name: imageName, Version: imageVersion, Ports: imagePorts, }, OwnerId: volumeLabels.OwnerId, On: state, Ports: ports, Nickname: serverData.Nickname, } return &serverInfo, nil } func (con Connection) getServerInfoFromId(ServerId string) (*models.ServerInfo, error) { volume, err := con.apiClient.VolumeInspect(context.Background(), ServerId) if err != nil { return nil, err } return con.getServerInfo(volume) } func transformContainerPortsToModel(ports []types.Port) []models.Port { modelPorts := make([]models.Port, len(ports)) for index, port := range ports { modelPorts[index] = models.Port{ Number: int(port.PublicPort), Protocol: port.Type, } } return modelPorts } func convertLabelsToMap(v any) (map[string]string, error) { data, err := json.Marshal(v) if err != nil { return nil, err } x := map[string]string{} json.Unmarshal(data, &x) return x, nil } type CreateServerRequest struct { ImageId string `json:"image_id"` } func (con Connection) CreateServer(ctx *gin.Context) { claims, exists := ctx.Get("claims") if !exists { ctx.AbortWithStatus(500) return } var request CreateServerRequest err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { ctx.AbortWithError(400, err) return } imageList, err := con.apiClient.ImageList(context.Background(), image.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("reference", request.ImageId))}) if err != nil { ctx.AbortWithError(400, err) return } if len(imageList) == 0 { ctx.AbortWithStatusJSON(404, "imageNotFound") return } imageSummary := imageList[0] labels, err := convertLabelsToMap(VolumeLabels{OwnerId: claims.(*auth.AuthClaims).Username, ImageId: imageSummary.RepoTags[0], Type: "GAME"}) if err != nil { ctx.AbortWithError(400, err) return } volumeResponse, err := con.apiClient.VolumeCreate(context.Background(), volume.CreateOptions{ Labels: labels, }) if err != nil { ctx.AbortWithError(500, err) return } ctx.JSON(200, volumeResponse.Name) } type PortMappingRequest struct { Source models.Port Destination models.Port } type StartServerRequest struct { Command string `json:"command"` Ports []PortMappingRequest `json:"ports"` } func (con Connection) StartServer(ctx *gin.Context) { serverId := ctx.Param("server_id") claims, exists := ctx.Get("claims") var request StartServerRequest json.NewDecoder(ctx.Request.Body).Decode(&request) if !exists { ctx.AbortWithStatus(403) return } serverInfo, err := con.getServerInfoFromId(serverId) if err != nil { ctx.AbortWithError(500, err) return } if serverInfo.On { ctx.Status(200) return } imageId := serverInfo.Image.Name + ":" + serverInfo.Image.Version labels := ContainerLabels{ OwnerId: claims.(*auth.AuthClaims).Username, ImageId: imageId, VolumeId: serverInfo.Id, Type: "GAME", } jsonString, err := json.Marshal(labels) if err != nil { ctx.AbortWithError(500, err) return } jsonLabels := make(map[string]string) json.Unmarshal(jsonString, &jsonLabels) volumes := make(map[string]struct{}) image, _, err := con.apiClient.ImageInspectWithRaw(context.Background(), imageId) if err != nil { ctx.AbortWithError(500, err) return } var portMapping nat.PortMap = make(nat.PortMap) if len(request.Ports) == 0 { for _, port := range serverInfo.Image.Ports { dockerPort, err := nat.NewPort(port.Protocol, fmt.Sprint(port.Number)) if err != nil { ctx.AbortWithError(500, err) return } portMapping[dockerPort] = []nat.PortBinding{{HostIP: "0.0.0.0"}} } } else { for _, portCouple := range request.Ports { portMapping[nat.Port(fmt.Sprintf("%d/%s", portCouple.Source.Number, portCouple.Source.Protocol))] = []nat.PortBinding{{HostPort: fmt.Sprint(portCouple.Destination.Number)}} } } words, err := shellwords.Split(request.Command) if err != nil { ctx.AbortWithError(500, err) return } if len(words) == 0 { words = nil } response, err := con.apiClient.ContainerCreate( context.Background(), &container.Config{ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, OpenStdin: true, StdinOnce: false, Image: imageId, Volumes: volumes, Labels: jsonLabels, Cmd: words, }, &container.HostConfig{ AutoRemove: true, Mounts: []mount.Mount{{Source: serverInfo.Id, Target: image.Config.WorkingDir, Type: "volume"}}, PortBindings: portMapping, }, &network.NetworkingConfig{}, &v1.Platform{}, "", ) if err != nil { ctx.AbortWithError(500, err) return } if err := con.apiClient.ContainerStart(ctx, response.ID, container.StartOptions{}); err != nil { ctx.AbortWithError(500, err) return } UPNPPath, exists := os.LookupEnv("UPNP_PATH") HostIP, hostIPexists := os.LookupEnv("HOST_IP") if exists && hostIPexists { time.Sleep(time.Millisecond * 100) containerData, err := con.apiClient.ContainerInspect(context.Background(), response.ID) if err != nil { ctx.AbortWithError(500, err) return } fo, err := os.OpenFile(UPNPPath, os.O_APPEND, os.ModeAppend) if err != nil { panic(err) } defer func() { if err := fo.Close(); err != nil { panic(err) } }() for containerPort, portBindings := range containerData.NetworkSettings.Ports { for _, hostPort := range portBindings { number, proto := hostPort.HostPort, containerPort.Proto() _, err := fo.Write([]byte(fmt.Sprintf("%s|%s|%s|%s|%s\n", HostIP, number, "0.0.0.0", number, strings.ToUpper(proto)))) if err != nil { ctx.AbortWithError(500, err) return } } } } ctx.JSON(200, response.ID) } func (con Connection) GetServers(ctx *gin.Context) { volumes, err := con.apiClient.VolumeList( context.TODO(), volume.ListOptions{ Filters: filters.NewArgs(filters.Arg("label", "type=GAME")), }, ) if err != nil { ctx.AbortWithError(500, err) } var servers []models.ServerInfo for _, volume := range volumes.Volumes { serverInfo, err := con.getServerInfo(*volume) if err != nil { continue } servers = append(servers, *serverInfo) } if err != nil { ctx.AbortWithError(500, err) } ctx.JSON(200, servers) } func (con Connection) StopServer(ctx *gin.Context) { serverId := ctx.Param("server_id") containersList, err := con.apiClient.ContainerList(context.Background(), container.ListOptions{ Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME")), }) if err != nil { ctx.AbortWithError(500, err) return } if len(containersList) == 0 { ctx.Status(200) return } for _, containerData := range containersList { con.apiClient.ContainerStop(context.Background(), containerData.ID, container.StopOptions{}) } ctx.Status(200) } func (con Connection) DeleteServer(ctx *gin.Context) { serverId := ctx.Param("server_id") containers, err := con.apiClient.ContainerList(context.Background(), container.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) if err != nil { ctx.AbortWithError(500, err) return } for _, containerInstance := range containers { con.apiClient.ContainerStop(context.Background(), containerInstance.ID, container.StopOptions{}) err := con.apiClient.ContainerRemove(context.Background(), containerInstance.ID, container.RemoveOptions{Force: true, RemoveLinks: true}) if err != nil { ctx.AbortWithError(500, err) return } } con.apiClient.VolumeRemove(context.Background(), serverId, false) ctx.JSON(200, "ok") } type RunCommandRequest struct { Command string `json:"command"` } func (con Connection) RunCommand(ctx *gin.Context) { var request RunCommandRequest err := json.NewDecoder(ctx.Request.Body).Decode(&request) serverId := ctx.Param("server_id") log.Print("Writing command \"", request.Command, "\"") containers, err := con.apiClient.ContainerList(context.Background(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) if err != nil { ctx.AbortWithError(500, err) return } for _, containerData := range containers { hijacked, err := con.apiClient.ContainerAttach(context.Background(), containerData.ID, container.AttachOptions{Stream: true, Stdin: true}) defer func() { hijacked.Close(); hijacked.CloseWrite() }() if err != nil { ctx.AbortWithError(500, err) return } number, err := hijacked.Conn.Write([]byte(request.Command + "\n")) log.Print("Wrote ", number, " bytes") if err != nil { ctx.AbortWithError(500, err) return } } ctx.JSON(200, "OK") } type Commands struct { CommandType string `json:"command_type"` Arguments []string `json:"arguments"` } func (con Connection) AttachServer(ctx *gin.Context) { serverId := ctx.Param("server_id") stop := false websocketRead := make(chan Commands) containerRead := make(chan string) defer func() { close(websocketRead) close(containerRead) }() containers, err := con.apiClient.ContainerList(context.TODO(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) if err != nil { ctx.AbortWithError(500, err) return } if len(containers) == 0 { ctx.AbortWithStatus(404) return } hijacked, err := con.apiClient.ContainerAttach(context.Background(), containers[0].ID, container.AttachOptions{Stream: true, Stdin: true, Stdout: true, Stderr: true, Logs: true}) if err != nil { ctx.AbortWithError(500, err) return } defer hijacked.Close() ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { return } defer ws.Close() go func() { data := make([]byte, 0, 1024) for { if stop { break } count, err := hijacked.Reader.Read(data) if err != nil { stop = true break } if count > 0 { log.Println("Got ", count, " bytes: ", data) } containerRead <- string(data) } }() go func() { var command Commands for { if stop { break } err := ws.ReadJSON(&command) if err != nil { command.CommandType = "close" websocketRead <- command break } websocketRead <- command } }() if err != nil { ctx.AbortWithError(500, err) return } for { select { case Command := <-websocketRead: switch Command.CommandType { case "insert": _, err = hijacked.Conn.Write([]byte(Command.Arguments[0])) if err != nil { stop = true break } case "close": stop = true break } case data := <-containerRead: err := ws.WriteJSON(data) if err != nil { stop = true break } } } } func (con Connection) serverAuthorized(permissions models.Permission) func(*gin.Context) bool { return func(ctx *gin.Context) bool { claims, exists := ctx.Get("claims") if !exists { return false } server_id := ctx.Param("server_id") if server_id == "" { return false } var serverData models.ServerData con.connection.Database("Backend").Collection("Servers").FindOne(context.Background(), bson.D{{Key: "ServerId", Value: server_id}}).Decode(&serverData) if serverData.OwnerId == claims.(*auth.AuthClaims).Username { return true } userPermissions := serverData.UserPermissions[claims.(*auth.AuthClaims).Username] if userPermissions&permissions == permissions || userPermissions&models.Admin == models.Admin { return true } return false } } func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client) { apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } defer apiClient.Close() connection := Connection{connection: mongo_client, apiClient: apiClient} 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.POST("/:server_id/delete", 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) }