diff --git a/models/server.go b/models/server.go index d3984b3..11dae6f 100644 --- a/models/server.go +++ b/models/server.go @@ -28,12 +28,12 @@ type FileBrowserInfo struct { } type ServerData struct { - Id string - OwnerId string - Image string - VolumeId string - Nickname string - UserPermissions map[string]Permission - DefaultCommand string - DefaultPorts []Port + Id string `bson:"Id"` + OwnerId string `bson:"OwnerId"` + Image string `bson:"Image"` + VolumeId string `bson:"VolumeId"` + Nickname string `bson:"Nickname"` + UserPermissions map[string]Permission `bson:"UserPermissions"` + DefaultCommand string `bson:"DefaultCommand"` + DefaultPorts []Port `bson:"DefaultPorts"` } diff --git a/servers/servers.go b/servers/servers.go index 7ae0d39..21cd8ee 100644 --- a/servers/servers.go +++ b/servers/servers.go @@ -29,9 +29,13 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) +const FILE_BROWSER_IMAGE = "filebrowser/filebrowser:latest" + +var DOMAIN = os.Getenv("SERVER_DOMAIN") + type Connection struct { - connection *mongo.Client - apiClient *client.Client + databaseConnection *mongo.Client + dockerClient *client.Client } type ContainerLabels struct { @@ -63,7 +67,7 @@ func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, e var volumeLabels VolumeLabels var serverData models.ServerData - con.connection.Database("Backend").Collection("Servers").FindOne(context.TODO(), bson.D{{Key: "id", Value: volume.Name}}).Decode(&serverData) + con.databaseConnection.Database("Backend").Collection("Servers").FindOne(context.TODO(), bson.D{{Key: "Id", Value: volume.Name}}).Decode(&serverData) jsonData, err := json.Marshal(volume.Labels) if err != nil { @@ -78,7 +82,7 @@ func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, e if len(serverData.DefaultPorts) == 0 { - imageList, err := con.apiClient.ImageList(context.TODO(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("reference", volumeLabels.ImageId))}) + imageList, err := con.dockerClient.ImageList(context.TODO(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("reference", volumeLabels.ImageId), filters.Arg("label", "type=GAME"))}) if err != nil { return nil, err } @@ -86,7 +90,7 @@ func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, e return nil, fmt.Errorf("ImageId %s does not exist", volumeLabels.ImageId) } imageSummery := imageList[0] - imageInspect, _, err := con.apiClient.ImageInspectWithRaw(context.TODO(), imageSummery.ID) + imageInspect, _, err := con.dockerClient.ImageInspectWithRaw(context.TODO(), imageSummery.ID) if err != nil { return nil, err } @@ -105,7 +109,7 @@ func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, e imageName := imageNameAndVersion[0] imageVersion := imageNameAndVersion[1] - containers, err := con.apiClient.ContainerList(context.TODO(), container.ListOptions{ + containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{ All: true, Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("label", fmt.Sprintf("volume_id=%s", volume.Name))), }) @@ -138,7 +142,7 @@ func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, e } func (con Connection) getServerInfoFromId(ServerId string) (*models.ServerInfo, error) { - volume, err := con.apiClient.VolumeInspect(context.TODO(), ServerId) + volume, err := con.dockerClient.VolumeInspect(context.TODO(), ServerId) if err != nil { return nil, err } @@ -168,9 +172,10 @@ func convertLabelsToMap(v any) (map[string]string, error) { } type CreateServerRequest struct { - ImageId string `json:"image_id"` - DefaultPorts []models.Port `json:"default_ports"` - DefaultCommand string `json:"default_command"` + ImageId string `json:"ImageId"` + DefaultPorts []models.Port `json:"DefaultPorts"` + DefaultCommand string `json:"DefaultCommand"` + Nickname string `json:"Nickname"` } func (con Connection) CreateServer(ctx *gin.Context) { @@ -186,7 +191,7 @@ func (con Connection) CreateServer(ctx *gin.Context) { return } - imageList, err := con.apiClient.ImageList(context.TODO(), image.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("reference", request.ImageId))}) + imageList, err := con.dockerClient.ImageList(context.TODO(), image.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("reference", request.ImageId), filters.Arg("label", "type=GAME"))}) if err != nil { ctx.AbortWithError(400, err) return @@ -201,14 +206,14 @@ func (con Connection) CreateServer(ctx *gin.Context) { ctx.AbortWithError(400, err) return } - volumeResponse, err := con.apiClient.VolumeCreate(context.TODO(), volume.CreateOptions{ + volumeResponse, err := con.dockerClient.VolumeCreate(context.TODO(), volume.CreateOptions{ Labels: labels, }) if err != nil { ctx.AbortWithError(500, err) return } - con.connection.Database("Backend").Collection("Servers").InsertOne(context.TODO(), models.ServerData{ + con.databaseConnection.Database("Backend").Collection("Servers").InsertOne(context.TODO(), models.ServerData{ Id: volumeResponse.Name, OwnerId: claims.(*auth.AuthClaims).Username, Image: imageSummary.RepoTags[0], @@ -226,8 +231,8 @@ type PortMappingRequest struct { } type StartServerRequest struct { - Command string `json:"command"` - Ports []PortMappingRequest `json:"ports"` + Command string `json:"Command"` + Ports []PortMappingRequest `json:"Ports"` } func (con Connection) StartServer(ctx *gin.Context) { @@ -271,7 +276,7 @@ func (con Connection) StartServer(ctx *gin.Context) { volumes := make(map[string]struct{}) - image, _, err := con.apiClient.ImageInspectWithRaw(context.TODO(), imageId) + image, _, err := con.dockerClient.ImageInspectWithRaw(context.TODO(), imageId) if err != nil { ctx.AbortWithError(500, err) return @@ -302,11 +307,13 @@ func (con Connection) StartServer(ctx *gin.Context) { if len(words) == 0 { words = nil } + portSet := make(nat.PortSet) - for port, _ := range portMapping { + for port := range portMapping { portSet[port] = struct{}{} } - response, err := con.apiClient.ContainerCreate( + + response, err := con.dockerClient.ContainerCreate( context.TODO(), &container.Config{ AttachStdin: true, @@ -334,22 +341,25 @@ func (con Connection) StartServer(ctx *gin.Context) { ctx.AbortWithError(500, err) return } - if err := con.apiClient.ContainerStart(ctx, response.ID, container.StartOptions{}); err != nil { + if err := con.dockerClient.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.TODO(), response.ID) + containerData, err := con.dockerClient.ContainerInspect(context.TODO(), 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) + ctx.AbortWithError(503, err) + con.dockerClient.ContainerStop(context.TODO(), containerData.ID, container.StopOptions{}) + return } defer func() { if err := fo.Close(); err != nil { @@ -372,7 +382,7 @@ func (con Connection) StartServer(ctx *gin.Context) { } func (con Connection) GetServers(ctx *gin.Context) { - volumes, err := con.apiClient.VolumeList( + volumes, err := con.dockerClient.VolumeList( context.TODO(), volume.ListOptions{ Filters: filters.NewArgs(filters.Arg("label", "type=GAME")), @@ -398,7 +408,7 @@ func (con Connection) GetServers(ctx *gin.Context) { func (con Connection) StopServer(ctx *gin.Context) { serverId := ctx.Param("server_id") - containersList, err := con.apiClient.ContainerList(context.TODO(), container.ListOptions{ + containersList, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{ Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME")), }) if err != nil { @@ -411,36 +421,36 @@ func (con Connection) StopServer(ctx *gin.Context) { } for _, containerData := range containersList { - con.apiClient.ContainerStop(context.TODO(), containerData.ID, container.StopOptions{}) + con.dockerClient.ContainerStop(context.TODO(), 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.TODO(), container.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) + containers, err := con.dockerClient.ContainerList(context.TODO(), 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.TODO(), containerInstance.ID, container.StopOptions{}) - err := con.apiClient.ContainerRemove(context.TODO(), containerInstance.ID, container.RemoveOptions{Force: true, RemoveLinks: true}) + con.dockerClient.ContainerStop(context.TODO(), containerInstance.ID, container.StopOptions{}) + err := con.dockerClient.ContainerRemove(context.TODO(), containerInstance.ID, container.RemoveOptions{Force: true, RemoveLinks: true}) if err != nil { ctx.AbortWithError(500, err) return } } - con.apiClient.VolumeRemove(context.TODO(), serverId, false) - con.connection.Database("Backend").Collection("Servers").FindOneAndDelete(context.TODO(), bson.D{{Key: "id", Value: serverId}}) + con.dockerClient.VolumeRemove(context.TODO(), serverId, false) + con.databaseConnection.Database("Backend").Collection("Servers").FindOneAndDelete(context.TODO(), bson.D{{Key: "Id", Value: serverId}}) ctx.JSON(200, "ok") } type RunCommandRequest struct { - Command string `json:"command"` + Command string `json:"Command"` } func (con Connection) RunCommand(ctx *gin.Context) { @@ -452,14 +462,14 @@ func (con Connection) RunCommand(ctx *gin.Context) { serverId := ctx.Param("server_id") log.Print("Writing command \"", request.Command, "\"") - containers, err := con.apiClient.ContainerList(context.TODO(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) + containers, err := con.dockerClient.ContainerList(context.TODO(), 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.TODO(), containerData.ID, container.AttachOptions{Stream: true, Stdin: true}) + hijacked, err := con.dockerClient.ContainerAttach(context.TODO(), containerData.ID, container.AttachOptions{Stream: true, Stdin: true}) defer func() { hijacked.Close(); hijacked.CloseWrite() }() if err != nil { ctx.AbortWithError(500, err) @@ -477,8 +487,8 @@ func (con Connection) RunCommand(ctx *gin.Context) { } type Commands struct { - CommandType string `json:"command_type"` - Arguments []string `json:"arguments"` + CommandType string `json:"CommandType"` + Arguments []string `json:"Arguments"` } func (con Connection) AttachServer(ctx *gin.Context) { @@ -493,7 +503,7 @@ func (con Connection) AttachServer(ctx *gin.Context) { close(containerRead) }() - containers, err := con.apiClient.ContainerList(context.TODO(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) + containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))}) if err != nil { ctx.AbortWithError(500, err) return @@ -502,7 +512,7 @@ func (con Connection) AttachServer(ctx *gin.Context) { ctx.AbortWithStatus(404) return } - hijacked, err := con.apiClient.ContainerAttach(context.TODO(), containers[0].ID, container.AttachOptions{Stream: true, Stdin: true, Stdout: true, Stderr: true, Logs: true}) + hijacked, err := con.dockerClient.ContainerAttach(context.TODO(), containers[0].ID, container.AttachOptions{Stream: true, Stdin: true, Stdout: true, Stderr: true, Logs: true}) if err != nil { ctx.AbortWithError(500, err) return @@ -511,6 +521,7 @@ func (con Connection) AttachServer(ctx *gin.Context) { ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { + ctx.AbortWithError(500, err) return } defer ws.Close() @@ -552,11 +563,6 @@ func (con Connection) AttachServer(ctx *gin.Context) { } }() - if err != nil { - ctx.AbortWithError(500, err) - return - } - for { if stop { break @@ -583,7 +589,6 @@ func (con Connection) AttachServer(ctx *gin.Context) { } } } - } func (con Connection) serverAuthorized(permissions models.Permission) func(*gin.Context) bool { @@ -600,7 +605,7 @@ func (con Connection) serverAuthorized(permissions models.Permission) func(*gin. var serverData models.ServerData - con.connection.Database("Backend").Collection("Servers").FindOne(context.TODO(), bson.D{{Key: "id", Value: server_id}}).Decode(&serverData) + con.databaseConnection.Database("Backend").Collection("Servers").FindOne(context.TODO(), bson.D{{Key: "Id", Value: server_id}}).Decode(&serverData) if serverData.OwnerId == claims.(*auth.AuthClaims).Username { return true @@ -616,6 +621,130 @@ func (con Connection) serverAuthorized(permissions models.Permission) func(*gin. } } +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 Connection) 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) + } + + updateOperation := bson.M{} + if Request.DefaultCommand != "" { + updateOperation["DefaultCommand"] = Request.DefaultCommand + } + if Request.DefaultPorts != nil { + updateOperation["DefaultPorts"] = Request.DefaultPorts + } + if Request.Nickname != "" { + updateOperation["Nickname"] = Request.Nickname + } + + _, err = con.databaseConnection.Database("Backend").Collection("Servers").UpdateOne(context.TODO(), bson.D{{Key: "Id", Value: serverId}}, bson.D{{Key: "$set", Value: updateOperation}}) + if err != nil { + ctx.AbortWithError(500, err) + } + + ctx.JSON(200, "OK") + +} + +func (con Connection) BrowseServer(ctx *gin.Context) { + serverID := ctx.Param("server_id") + claims, exists := ctx.Get("claims") + if !exists { + ctx.AbortWithStatus(403) + return + } + serverInfo, err := con.getServerInfoFromId(serverID) + + if err != nil { + ctx.AbortWithError(500, err) + } + + browserLabels := make(map[string]string) + browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.domains[0].main", serverInfo.Id)] = fmt.Sprintf("%s.%s", "browsers", DOMAIN) + browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.domains[0].sans", serverInfo.Id)] = fmt.Sprintf("*.%s.%s", "browsers", DOMAIN) + browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", serverInfo.Id)] = "myresolver" + + containerLabels := ContainerLabels{ + OwnerId: claims.(*auth.AuthClaims).Username, + ImageId: FILE_BROWSER_IMAGE, + VolumeId: serverInfo.Id, + Type: "FILE-BROWSER", + } + jsonLabels, err := json.Marshal(containerLabels) + if err != nil { + ctx.AbortWithError(500, err) + return + } + + err = json.Unmarshal(jsonLabels, &browserLabels) + if err != nil { + ctx.AbortWithError(500, err) + return + } + command, err := shellwords.Split("-r /tmp/data --noauth") + if err != nil { + ctx.AbortWithError(500, err) + return + } + + imageRef := fmt.Sprintf("%s:%s", serverInfo.Image.Name, serverInfo.Image.Version) + images, err := con.dockerClient.ImageList(context.TODO(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("reference", imageRef))}) + if err != nil { + ctx.AbortWithError(500, err) + return + } + + if len(images) == 0 { + ctx.AbortWithError(500, fmt.Errorf("Image %s no longer exists", imageRef)) + return + } + + ContainerResponse, err := con.dockerClient.ContainerCreate( + context.TODO(), + &container.Config{ + Cmd: command, + Image: FILE_BROWSER_IMAGE, + Labels: browserLabels, + ExposedPorts: nat.PortSet{"80/tcp": struct{}{}}, + }, + &container.HostConfig{ + Mounts: []mount.Mount{{Source: serverInfo.Id, Target: "/tmp/data", Type: "volume"}}, + PublishAllPorts: true, + AutoRemove: true, + }, + &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{"test": { + NetworkID: "exposed", + }}, + }, + &v1.Platform{}, + "", + ) + if err != nil { + ctx.AbortWithError(500, err) + return + } + + err = con.dockerClient.ContainerStart(context.TODO(), ContainerResponse.ID, container.StartOptions{}) + if err != nil { + ctx.AbortWithError(500, err) + return + } + + ctx.JSON(200, "OK") +} + func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client) { apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { @@ -623,12 +752,14 @@ func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client) { } defer apiClient.Close() - connection := Connection{connection: mongo_client, apiClient: apiClient} + connection := Connection{databaseConnection: mongo_client, dockerClient: 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.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) }