diff --git a/go.mod b/go.mod index 12bfe50..2e15bf4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require github.com/gin-gonic/gin v1.9.1 require ( + github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/docker/docker v26.1.2+incompatible // indirect diff --git a/go.sum b/go.sum index 2385170..c383a2f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10 h1:XwHQ5xDtYPdtBbVPyRO6UZoWZe8/mbKUb076f8x7RvI= +github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10/go.mod h1:gv0DYOzHEsKgo31lTCDGauIg4DTTGn41Bzp+t3wSOlk= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/servers/servers.go b/servers/servers.go index ec2b461..db07000 100644 --- a/servers/servers.go +++ b/servers/servers.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "fmt" + "log" "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" @@ -181,16 +183,27 @@ func (con Connection) CreateServer(ctx *gin.Context) { 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 } - // command := ctx.Param("command") - serverInfo, err := con.getServerInfoFromId(serverId) if err != nil { @@ -227,14 +240,29 @@ func (con Connection) StartServer(ctx *gin.Context) { } var portMapping nat.PortMap = make(nat.PortMap) - - for _, port := range serverInfo.Image.Ports { - dockerPort, err := nat.NewPort(port.Protocol, fmt.Sprint(port.Number)) - if err != nil { - ctx.AbortWithError(500, err) - return + 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"}} } - 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( @@ -244,11 +272,12 @@ func (con Connection) StartServer(ctx *gin.Context) { AttachStdout: true, AttachStderr: true, Tty: true, - OpenStdin: false, + OpenStdin: true, StdinOnce: false, Image: imageId, Volumes: volumes, Labels: jsonLabels, + Cmd: words, }, &container.HostConfig{ AutoRemove: true, @@ -345,6 +374,60 @@ func (con Connection) StopServer(ctx *gin.Context) { 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") +} + func (con Connection) serverAuthorized(permissions models.Permission) func(*gin.Context) bool { return func(ctx *gin.Context) bool { claims, exists := ctx.Get("claims") @@ -387,4 +470,6 @@ func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client) { 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) }