diff --git a/internal/api/api.go b/internal/api/api.go index 490f00c..11cf033 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -52,6 +52,7 @@ func (manager *ApiManagerCtx) Mount(r chi.Router) { r.Get("/settings", manager.roomGetSettings) r.Get("/stats", manager.roomGetStats) + r.Get("/screenshot", manager.roomGetScreenshot) r.Post("/start", manager.roomGenericAction(manager.rooms.Start)) r.Post("/stop", manager.roomGenericAction(manager.rooms.Stop)) diff --git a/internal/api/rooms.go b/internal/api/rooms.go index bac2b84..28a5a26 100644 --- a/internal/api/rooms.go +++ b/internal/api/rooms.go @@ -137,6 +137,21 @@ func (manager *ApiManagerCtx) roomGetStats(w http.ResponseWriter, r *http.Reques json.NewEncoder(w).Encode(response) } +func (manager *ApiManagerCtx) roomGetScreenshot(w http.ResponseWriter, r *http.Request) { + roomId := chi.URLParam(r, "roomId") + + response, err := manager.rooms.GetScreenshot(roomId) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + w.Header().Set("Content-Type", "image/jpeg") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + + w.Write(response) +} + func (manager *ApiManagerCtx) roomGenericAction(action func(id string) error) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { roomId := chi.URLParam(r, "roomId") diff --git a/internal/room/cmddialer.go b/internal/room/cmddialer.go new file mode 100644 index 0000000..145b1d8 --- /dev/null +++ b/internal/room/cmddialer.go @@ -0,0 +1,93 @@ +package room + +import ( + "fmt" + "io" + "net" + "time" +) + +type cmdDialer struct { + stdin io.WriteCloser + stdout io.ReadCloser +} + +// Read reads data from the connection. +// Read can be made to time out and return an error after a fixed +// time limit; see SetDeadline and SetReadDeadline. +func (c *cmdDialer) Read(b []byte) (n int, err error) { + n, err = c.stdout.Read(b) + fmt.Printf("Read: %s\n", b) + return +} + +// Write writes data to the connection. +// Write can be made to time out and return an error after a fixed +// time limit; see SetDeadline and SetWriteDeadline. +func (c *cmdDialer) Write(b []byte) (n int, err error) { + fmt.Printf("Write: %s\n", b) + return c.stdin.Write(b) +} + +// Close closes the connection. +// Any blocked Read or Write operations will be unblocked and return errors. +func (c *cmdDialer) Close() error { + if err := c.stdin.Close(); err != nil { + return err + } + return c.stdout.Close() +} + +// LocalAddr returns the local network address, if known. +func (c *cmdDialer) LocalAddr() net.Addr { + return nil +} + +// RemoteAddr returns the remote network address, if known. +func (c *cmdDialer) RemoteAddr() net.Addr { + return nil +} + +// SetDeadline sets the read and write deadlines associated +// with the connection. It is equivalent to calling both +// SetReadDeadline and SetWriteDeadline. +// +// A deadline is an absolute time after which I/O operations +// fail instead of blocking. The deadline applies to all future +// and pending I/O, not just the immediately following call to +// Read or Write. After a deadline has been exceeded, the +// connection can be refreshed by setting a deadline in the future. +// +// If the deadline is exceeded a call to Read or Write or to other +// I/O methods will return an error that wraps os.ErrDeadlineExceeded. +// This can be tested using errors.Is(err, os.ErrDeadlineExceeded). +// The error's Timeout method will return true, but note that there +// are other possible errors for which the Timeout method will +// return true even if the deadline has not been exceeded. +// +// An idle timeout can be implemented by repeatedly extending +// the deadline after successful Read or Write calls. +// +// A zero value for t means I/O operations will not time out. +func (c *cmdDialer) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +// SetReadDeadline sets the deadline for future Read calls +// and any currently-blocked Read call. +// A zero value for t means Read will not time out. +func (c *cmdDialer) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline sets the deadline for future Write calls +// and any currently-blocked Write call. +// Even if write times out, it may return n > 0, indicating that +// some of the data was successfully written. +// A zero value for t means Write will not time out. +func (c *cmdDialer) SetWriteDeadline(t time.Time) error { + return nil +} diff --git a/internal/room/containers.go b/internal/room/containers.go index bbf41db..683699a 100644 --- a/internal/room/containers.go +++ b/internal/room/containers.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "io" + "net" "time" dockerTypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/stdcopy" "github.com/m1k1o/neko-rooms/internal/types" ) @@ -116,7 +118,7 @@ func (manager *RoomManagerCtx) inspectContainer(id string) (*dockerTypes.Contain return &container, nil } -func (manager *RoomManagerCtx) containerExec(id string, cmd []string) (string, error) { +func (manager *RoomManagerCtx) containerExec(id string, cmd []string) ([]byte, error) { exec, err := manager.client.ContainerExecCreate(context.Background(), id, dockerTypes.ExecConfig{ AttachStderr: true, AttachStdin: true, @@ -126,7 +128,7 @@ func (manager *RoomManagerCtx) containerExec(id string, cmd []string) (string, e Detach: false, }) if err != nil { - return "", err + return nil, err } conn, err := manager.client.ContainerExecAttach(context.Background(), exec.ID, dockerTypes.ExecStartCheck{ @@ -134,10 +136,59 @@ func (manager *RoomManagerCtx) containerExec(id string, cmd []string) (string, e Tty: true, }) if err != nil { - return "", err + return nil, err } defer conn.Close() - data, err := io.ReadAll(conn.Reader) - return string(data), err + return io.ReadAll(conn.Reader) +} + +func (manager *RoomManagerCtx) containerExecDialer(id string) func(ctx context.Context, network, addr string) (net.Conn, error) { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + fmt.Println("dialer", network, addr) + + // split addr + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + exec, err := manager.client.ContainerExecCreate(ctx, id, dockerTypes.ExecConfig{ + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Cmd: []string{"wget", "-O-", "http://" + host + ":" + port + ""}, + Tty: false, + Detach: false, + }) + if err != nil { + return nil, err + } + + c, err := manager.client.ContainerExecAttach(ctx, exec.ID, dockerTypes.ExecStartCheck{ + Detach: false, + Tty: false, + }) + if err != nil { + return nil, err + } + + // Read all + // io.ReadAll(c.Reader) + dat, err := io.ReadAll(c.Reader) + if err != nil { + return nil, err + } + + fmt.Println("dialer", string(dat)) + + // new pipe + stderr := stdcopy.NewStdWriter(c.Conn, stdcopy.Stderr) + stdout := stdcopy.NewStdWriter(c.Conn, stdcopy.Stdout) + + return &cmdDialer{ + stdin: c.Conn, + stdout: c.Conn, + }, nil + } } diff --git a/internal/room/manager.go b/internal/room/manager.go index 8ca36b4..bab79d8 100644 --- a/internal/room/manager.go +++ b/internal/room/manager.go @@ -4,10 +4,13 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "net/http" "os" "path" "path/filepath" "strings" + "time" dockerTypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -593,21 +596,59 @@ func (manager *RoomManagerCtx) GetStats(id string) (*types.RoomStats, error) { return nil, err } - output, err := manager.containerExec(id, []string{ - "wget", "-q", "-O-", "http://127.0.0.1:8080/stats?pwd=" + settings.AdminPass, - }) + transport := http.Transport{ + DialContext: manager.containerExecDialer(id), + TLSHandshakeTimeout: 10 * time.Second, + } + + client := http.Client{ + Transport: &transport, + Timeout: 4 * time.Second, + } + + // http request to get the stats + req, err := http.NewRequest("GET", "http://127.0.0.1:8080/stats?pwd="+settings.AdminPass, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + output, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var stats types.RoomStats - if err := json.Unmarshal([]byte(output), &stats); err != nil { + if err := json.Unmarshal(output, &stats); err != nil { return nil, err } return &stats, nil } +func (manager *RoomManagerCtx) GetScreenshot(id string) ([]byte, error) { + container, err := manager.inspectContainer(id) + if err != nil { + return nil, err + } + + settings := types.RoomSettings{} + err = settings.FromEnv(container.Config.Env) + if err != nil { + return nil, err + } + + return manager.containerExec(id, []string{ + "wget", "-q", "-O-", "http://127.0.0.1:8080/screenshot.jpg?pwd=" + settings.AdminPass, + }) +} + func (manager *RoomManagerCtx) Start(id string) error { _, err := manager.inspectContainer(id) if err != nil { diff --git a/internal/types/room.go b/internal/types/room.go index 2e7ab5a..4ab4eca 100644 --- a/internal/types/room.go +++ b/internal/types/room.go @@ -258,6 +258,7 @@ type RoomManager interface { GetEntry(id string) (*RoomEntry, error) GetSettings(id string) (*RoomSettings, error) GetStats(id string) (*RoomStats, error) + GetScreenshot(id string) ([]byte, error) Remove(id string) error Start(id string) error