diff --git a/go.mod b/go.mod index 2e15bf4..2451fcc 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect diff --git a/go.sum b/go.sum index c383a2f..8ecd1de 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/servers/servers.go b/servers/servers.go index db07000..53e66b4 100644 --- a/servers/servers.go +++ b/servers/servers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "net/http" "os" "strings" "time" @@ -22,6 +23,7 @@ import ( "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" @@ -49,6 +51,14 @@ 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) @@ -428,6 +438,113 @@ func (con Connection) RunCommand(ctx *gin.Context) { 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") @@ -472,4 +589,5 @@ func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client) { 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) }