diff --git a/images/chromium-headful/wrapper.sh b/images/chromium-headful/wrapper.sh index 6fd55ad2..dd15f099 100755 --- a/images/chromium-headful/wrapper.sh +++ b/images/chromium-headful/wrapper.sh @@ -33,6 +33,22 @@ if [[ -z "${WITHDOCKER:-}" ]]; then disable_scale_to_zero fi +# ----------------------------------------------------------------------------- +# Ensure a sensible hostname --------------------------------------------------- +# ----------------------------------------------------------------------------- +# Some environments boot with an empty or \"(none)\" hostname which shows up in +# prompts. Best-effort set a friendly hostname early so services inherit it. +if h=$(cat /proc/sys/kernel/hostname 2>/dev/null); then + if [ -z "$h" ] || [ "$h" = "(none)" ]; then + if command -v hostname >/dev/null 2>&1; then + hostname kernel-vm 2>/dev/null || true + fi + echo -n "kernel-vm" > /proc/sys/kernel/hostname 2>/dev/null || true + fi +fi +# Also export HOSTNAME so shells pick it up immediately. +export HOSTNAME="${HOSTNAME:-kernel-vm}" + # ----------------------------------------------------------------------------- # House-keeping for the unprivileged "kernel" user -------------------------------- # Some Chromium subsystems want to create files under $HOME (NSS cert DB, dconf diff --git a/images/chromium-headless/image/wrapper.sh b/images/chromium-headless/image/wrapper.sh index f2daafa0..d156cf73 100755 --- a/images/chromium-headless/image/wrapper.sh +++ b/images/chromium-headless/image/wrapper.sh @@ -30,6 +30,19 @@ if [[ -z "${WITHDOCKER:-}" ]]; then disable_scale_to_zero fi +# ----------------------------------------------------------------------------- +# Ensure a sensible hostname --------------------------------------------------- +# ----------------------------------------------------------------------------- +if h=$(cat /proc/sys/kernel/hostname 2>/dev/null); then + if [ -z "$h" ] || [ "$h" = "(none)" ]; then + if command -v hostname >/dev/null 2>&1; then + hostname kernel-vm 2>/dev/null || true + fi + echo -n "kernel-vm" > /proc/sys/kernel/hostname 2>/dev/null || true + fi +fi +export HOSTNAME="${HOSTNAME:-kernel-vm}" + # if CHROMIUM_FLAGS is not set, default to the flags used in playwright_stealth if [ -z "${CHROMIUM_FLAGS:-}" ]; then CHROMIUM_FLAGS="--accept-lang=en-US,en \ diff --git a/server/cmd/api/api/process.go b/server/cmd/api/api/process.go index 16fc6280..51e68ed4 100644 --- a/server/cmd/api/api/process.go +++ b/server/cmd/api/api/process.go @@ -9,6 +9,8 @@ import ( "errors" "fmt" "io" + "net" + "net/http" "os" "os/exec" "os/user" @@ -19,10 +21,12 @@ import ( "syscall" "time" + "github.com/creack/pty" "github.com/google/uuid" openapi_types "github.com/oapi-codegen/runtime/types" "github.com/onkernel/kernel-images/server/lib/logger" oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "golang.org/x/sys/unix" ) type processHandle struct { @@ -34,9 +38,13 @@ type processHandle struct { stdin io.WriteCloser stdout io.ReadCloser stderr io.ReadCloser + ptyFile *os.File + isTTY bool outCh chan oapi.ProcessStreamEvent doneCh chan struct{} mu sync.RWMutex + // attachActive guards PTY attach sessions; only one client may be attached at a time. + attachActive bool } func (h *processHandle) state() string { @@ -223,21 +231,72 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil } - stdout, err := cmd.StdoutPipe() - if err != nil { - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdout"}}, nil - } - stderr, err := cmd.StderrPipe() - if err != nil { - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stderr"}}, nil - } - stdin, err := cmd.StdinPipe() - if err != nil { - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdin"}}, nil - } - if err := cmd.Start(); err != nil { - log.Error("failed to start process", "err", err) - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil + var ( + stdout io.ReadCloser + stderr io.ReadCloser + stdin io.WriteCloser + ptyFile *os.File + isTTY bool + ) + // PTY mode when requested + if request.Body.AllocateTty != nil && *request.Body.AllocateTty { + // Validate rows/cols before starting the process + const maxUint16 = 65535 + if request.Body.Rows != nil && *request.Body.Rows > maxUint16 { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "rows must be <= 65535"}}, nil + } + if request.Body.Cols != nil && *request.Body.Cols > maxUint16 { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "cols must be <= 65535"}}, nil + } + // Ensure TERM and initial size env + hasTerm := false + for _, kv := range cmd.Env { + if strings.HasPrefix(kv, "TERM=") { + hasTerm = true + break + } + } + if !hasTerm { + cmd.Env = append(cmd.Env, "TERM=xterm-256color") + } + // Start with PTY + var errStart error + ptyFile, errStart = pty.Start(cmd) + if errStart != nil { + log.Error("failed to start PTY process", "err", errStart) + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil + } + // Set initial size if provided + var rows, cols uint16 + if request.Body.Rows != nil && *request.Body.Rows > 0 { + rows = uint16(*request.Body.Rows) + } + if request.Body.Cols != nil && *request.Body.Cols > 0 { + cols = uint16(*request.Body.Cols) + } + if rows > 0 && cols > 0 { + _ = pty.Setsize(ptyFile, &pty.Winsize{Rows: rows, Cols: cols}) + } + stdout = ptyFile + stdin = ptyFile + isTTY = true + } else { + stdout, err = cmd.StdoutPipe() + if err != nil { + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdout"}}, nil + } + stderr, err = cmd.StderrPipe() + if err != nil { + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stderr"}}, nil + } + stdin, err = cmd.StdinPipe() + if err != nil { + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdin"}}, nil + } + if err := cmd.Start(); err != nil { + log.Error("failed to start process", "err", err) + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil + } } // Disable scale-to-zero while the process is running. @@ -258,6 +317,8 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn stdin: stdin, stdout: stdout, stderr: stderr, + ptyFile: ptyFile, + isTTY: isTTY, outCh: make(chan oapi.ProcessStreamEvent, 256), doneCh: make(chan struct{}), } @@ -271,37 +332,40 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn s.procMu.Unlock() // Reader goroutines - go func() { - reader := bufio.NewReader(stdout) - buf := make([]byte, 4096) - for { - n, err := reader.Read(buf) - if n > 0 { - data := base64.StdEncoding.EncodeToString(buf[:n]) - stream := oapi.ProcessStreamEventStream("stdout") - h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} - } - if err != nil { - break - } - } - }() - - go func() { - reader := bufio.NewReader(stderr) - buf := make([]byte, 4096) - for { - n, err := reader.Read(buf) - if n > 0 { - data := base64.StdEncoding.EncodeToString(buf[:n]) - stream := oapi.ProcessStreamEventStream("stderr") - h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} + // In PTY mode, do NOT read from the PTY here to avoid competing with the /attach endpoint. + // In non‑PTY mode, stdout and stderr are separate pipes, so we run two readers and tag chunks accordingly. + if !isTTY { + go func() { + reader := bufio.NewReader(stdout) + buf := make([]byte, 4096) + for { + n, err := reader.Read(buf) + if n > 0 { + data := base64.StdEncoding.EncodeToString(buf[:n]) + stream := oapi.ProcessStreamEventStream("stdout") + h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} + } + if err != nil { + break + } } - if err != nil { - break + }() + go func() { + reader := bufio.NewReader(stderr) + buf := make([]byte, 4096) + for { + n, err := reader.Read(buf) + if n > 0 { + data := base64.StdEncoding.EncodeToString(buf[:n]) + stream := oapi.ProcessStreamEventStream("stderr") + h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} + } + if err != nil { + break + } } - } - }() + }() + } // Waiter goroutine - use context without cancel since HTTP request may complete // before the process exits @@ -321,6 +385,23 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn } } h.setExited(code) + // Ensure all related FDs are closed to avoid leaking descriptors. + // In PTY mode, close the PTY master; in non-PTY mode, close individual pipes. + if h.isTTY { + if h.ptyFile != nil { + _ = h.ptyFile.Close() + } + } else { + if h.stdin != nil { + _ = h.stdin.Close() + } + if h.stdout != nil { + _ = h.stdout.Close() + } + if h.stderr != nil { + _ = h.stderr.Close() + } + } // Re-enable scale-to-zero now that the process has exited, // but only if we successfully disabled it earlier @@ -510,3 +591,163 @@ func (s *ApiService) ProcessStdoutStream(ctx context.Context, request oapi.Proce } func ptrOf[T any](v T) *T { return &v } + +// Resize PTY-backed process +// (POST /process/{process_id}/resize) +func (s *ApiService) ProcessResize(ctx context.Context, request oapi.ProcessResizeRequestObject) (oapi.ProcessResizeResponseObject, error) { + id := request.ProcessId.String() + if request.Body == nil { + return oapi.ProcessResize400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil + } + rows := request.Body.Rows + cols := request.Body.Cols + if rows <= 0 || cols <= 0 { + return oapi.ProcessResize400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "rows and cols must be > 0"}}, nil + } + const maxUint16 = 65535 + if rows > maxUint16 || cols > maxUint16 { + return oapi.ProcessResize400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "rows and cols must be <= 65535"}}, nil + } + s.procMu.RLock() + h, ok := s.procs[id] + s.procMu.RUnlock() + if !ok { + return oapi.ProcessResize404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil + } + if !h.isTTY || h.ptyFile == nil { + return oapi.ProcessResize400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "process is not PTY-backed"}}, nil + } + ws := &pty.Winsize{Rows: uint16(rows), Cols: uint16(cols)} + if err := pty.Setsize(h.ptyFile, ws); err != nil { + return oapi.ProcessResize500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to resize PTY"}}, nil + } + return oapi.ProcessResize200JSONResponse(oapi.OkResponse{Ok: true}), nil +} + +// HandleProcessAttach performs a raw HTTP hijack and shuttles bytes between the client and the PTY. +// This endpoint is intentionally not defined in OpenAPI. +func (s *ApiService) HandleProcessAttach(w http.ResponseWriter, r *http.Request, id string) { + s.procMu.RLock() + h, ok := s.procs[id] + s.procMu.RUnlock() + if !ok { + http.Error(w, "process not found", http.StatusNotFound) + return + } + if !h.isTTY || h.ptyFile == nil { + http.Error(w, "process is not PTY-backed", http.StatusBadRequest) + return + } + // Enforce single concurrent attach per PTY-backed process to avoid I/O corruption. + h.mu.Lock() + if h.attachActive { + h.mu.Unlock() + http.Error(w, "process already has an active attach session", http.StatusConflict) + return + } + h.attachActive = true + h.mu.Unlock() + // Ensure the flag is cleared when this handler exits (client disconnects or process ends). + defer func() { + h.mu.Lock() + h.attachActive = false + h.mu.Unlock() + }() + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "hijacking not supported", http.StatusInternalServerError) + return + } + conn, buf, err := hj.Hijack() + if err != nil { + http.Error(w, "failed to hijack connection", http.StatusInternalServerError) + return + } + // Write minimal HTTP response and switch to raw I/O + _, _ = buf.WriteString("HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n") + _ = buf.Flush() + + processRW := h.ptyFile + // Coordinate shutdown so that both pumps exit when either side closes. + done := make(chan struct{}) + var once sync.Once + shutdown := func() { + once.Do(func() { + _ = conn.Close() + close(done) + }) + } + + // Pipe: client -> process + // Use buf.Reader to consume any buffered data that was read ahead before the hijack, + // then continue reading from the underlying connection. + go func() { + _, _ = io.Copy(processRW, buf.Reader) + shutdown() + }() + // Pipe: process -> client (non-blocking reads to allow early shutdown) + go func() { + copyPTYToConn(processRW, conn, done) + shutdown() + }() + + // Close when process exits + go func() { + <-h.doneCh + shutdown() + }() + + // Keep handler alive until shutdown triggered + <-done +} + +// copyPTYToConn copies from a PTY (os.File) to a net.Conn without mutating the +// PTY's file status flags. It uses readiness polling so we can wake up and exit +// when stop is closed, avoiding goroutine leaks and preserving blocking mode. +func copyPTYToConn(ptyFile *os.File, conn net.Conn, stop <-chan struct{}) { + fd := int(ptyFile.Fd()) + buf := make([]byte, 32*1024) + // Poll in short intervals so we can react quickly to stop signal. + for { + // Check for stop first to avoid extra reads after shutdown. + select { + case <-stop: + return + default: + } + pfds := []unix.PollFd{ + {Fd: int32(fd), Events: unix.POLLIN}, + } + _, perr := unix.Poll(pfds, 100) // 100ms + if perr != nil && perr != syscall.EINTR { + return + } + // If readable (or hangup/err), attempt a read. + if pfds[0].Revents&(unix.POLLIN|unix.POLLHUP|unix.POLLERR) == 0 { + // Not ready; loop around and re-check stop. + continue + } + n, rerr := ptyFile.Read(buf) + if n > 0 { + if _, werr := conn.Write(buf[:n]); werr != nil { + return + } + } + if rerr != nil { + if rerr == io.EOF { + return + } + if errno, ok := rerr.(syscall.Errno); ok { + // EIO is observed on PTY when the slave closes; treat as EOF. + if errno == syscall.EIO { + return + } + // Spurious would-block after poll; just continue. + if errno == syscall.EAGAIN || errno == syscall.EWOULDBLOCK { + continue + } + } + return + } + } +} diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index e25f5496..6acdcad9 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -118,6 +118,11 @@ func main() { w.Header().Set("Content-Type", "application/json") w.Write(jsonData) }) + // Raw attach endpoint (HTTP hijack) - not part of OpenAPI spec + r.Get("/process/{process_id}/attach", func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "process_id") + apiService.HandleProcessAttach(w, r, id) + }) srv := &http.Server{ Addr: fmt.Sprintf(":%d", config.Port), diff --git a/server/cmd/shell/main.go b/server/cmd/shell/main.go new file mode 100644 index 00000000..5f0bcb8b --- /dev/null +++ b/server/cmd/shell/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "flag" + "fmt" + "io" + "log" + "net" + "net/url" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/google/uuid" + openapi_types "github.com/oapi-codegen/runtime/types" + oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "golang.org/x/term" +) + +func main() { + var serverURL string + flag.StringVar(&serverURL, "server", "http://localhost:444", "Base URL to API server (e.g., http://localhost:444)") + flag.Parse() + + u, err := ensureHTTPURL(serverURL) + if err != nil { + log.Fatalf("invalid server URL: %v", err) + } + + // Determine terminal size (cols, rows) + cols, rows := 80, 24 + if term.IsTerminal(int(os.Stdin.Fd())) { + if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil { + cols, rows = w, h + } + } + + // Prepare client + client, err := oapi.NewClientWithResponses(u.String()) + if err != nil { + log.Fatalf("failed to init client: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Spawn bash with TTY + body := oapi.ProcessSpawnJSONRequestBody{ + Command: "/bin/bash", + } + body.AllocateTty = boolPtr(true) + body.Rows = &rows + body.Cols = &cols + args := []string{} + body.Args = &args + + resp, err := client.ProcessSpawnWithResponse(ctx, body) + if err != nil { + log.Fatalf("spawn request failed: %v", err) + } + if resp.JSON200 == nil || resp.JSON200.ProcessId == nil { + log.Fatalf("unexpected response: %+v", resp) + } + procID := resp.JSON200.ProcessId.String() + + // Attach via HTTP hijack + var ( + rawConn net.Conn + ) + { + addr := u.Host + if addr == "" { + // Fallback for URLs like http://localhost + addr = u.Hostname() + if port := u.Port(); port != "" { + addr = net.JoinHostPort(addr, port) + } else { + // Default ports by scheme + if u.Scheme == "https" { + addr = net.JoinHostPort(addr, "443") + } else { + addr = net.JoinHostPort(addr, "80") + } + } + } + // Dial based on scheme + switch u.Scheme { + case "https": + tlsConf := &tls.Config{ + ServerName: u.Hostname(), + MinVersion: tls.VersionTLS12, + } + rc, err := tls.Dial("tcp", addr, tlsConf) + if err != nil { + log.Fatalf("failed to tls dial %s: %v", addr, err) + } + rawConn = rc + default: + rc, err := net.Dial("tcp", addr) + if err != nil { + log.Fatalf("failed to dial %s: %v", addr, err) + } + rawConn = rc + } + } + defer rawConn.Close() + + pathPrefix := strings.TrimRight(u.Path, "/") + if pathPrefix == "/" { + pathPrefix = "" + } + path := fmt.Sprintf("%s/%s/%s/%s", pathPrefix, "process", procID, "attach") + req := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", path, u.Host) + // For TLS, ensure we speak HTTP/1.1 and not attempt an HTTP/2 preface + // by writing the raw bytes directly over the established connection. + if _, err := rawConn.Write([]byte(req)); err != nil { + log.Fatalf("failed to write attach request: %v", err) + } + + // Read and consume HTTP response headers (until \r\n\r\n) + br := bufio.NewReader(rawConn) + if err := readHTTPHeaders(br); err != nil { + log.Fatalf("failed to read attach response headers: %v", err) + } + + // Put local terminal into raw mode + var oldState *term.State + if term.IsTerminal(int(os.Stdin.Fd())) { + s, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + log.Fatalf("failed to set raw mode: %v", err) + } + oldState = s + defer func() { + _ = term.Restore(int(os.Stdin.Fd()), oldState) + fmt.Println() + }() + } + + // Handle window resize (SIGWINCH) + winch := make(chan os.Signal, 1) + signal.Notify(winch, syscall.SIGWINCH) + go func() { + for range winch { + if term.IsTerminal(int(os.Stdin.Fd())) { + if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil { + // rows=h, cols=w + rows := h + cols := w + // best-effort resize; do not cancel main ctx + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + uid, _ := uuid.Parse(procID) + _, _ = client.ProcessResizeWithResponse(ctx, openapi_types.UUID(uid), oapi.ProcessResizeJSONRequestBody{ + Rows: rows, + Cols: cols, + }) + }() + } + } + } + }() + + // Bi-directional piping + errCh := make(chan error, 2) + go func() { + _, err := io.Copy(rawConn, os.Stdin) + errCh <- err + }() + go func() { + // Use the buffered reader to include any bytes already read beyond headers + _, err := io.Copy(os.Stdout, br) + errCh <- err + }() + + // Wait for either side to close/error + <-errCh +} + +func ensureHTTPURL(s string) (*url.URL, error) { + if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") { + s = "http://" + s + } + u, err := url.Parse(s) + if err != nil { + return nil, err + } + if u.Scheme == "" { + u.Scheme = "http" + } + if u.Host == "" && u.Path != "" { + // Allow bare host:port without scheme + u.Host = u.Path + u.Path = "" + } + return u, nil +} + +func readHTTPHeaders(r *bufio.Reader) error { + for { + line, err := r.ReadString('\n') + if err != nil { + return err + } + if line == "\r\n" { + return nil + } + // continue until empty line + } +} + +func boolPtr(b bool) *bool { return &b } diff --git a/server/go.mod b/server/go.mod index 2cbf0cc1..3a7ea256 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/avast/retry-go/v5 v5.0.0 github.com/coder/websocket v1.8.14 + github.com/creack/pty v1.1.24 github.com/fsnotify/fsnotify v1.9.0 github.com/getkin/kin-openapi v0.132.0 github.com/ghodss/yaml v1.0.0 @@ -18,6 +19,8 @@ require ( github.com/samber/lo v1.52.0 github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.17.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 ) require ( @@ -39,7 +42,6 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/crypto v0.40.0 // indirect - golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/server/go.sum b/server/go.sum index 976131d0..ecefca2e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -6,6 +6,8 @@ github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9 github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -86,8 +88,10 @@ golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+Zdx golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 6814ccdf..595f89f4 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -1,6 +1,6 @@ // Package oapi provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. package oapi import ( @@ -373,8 +373,47 @@ type ProcessKillRequest struct { // ProcessKillRequestSignal Signal to send. type ProcessKillRequestSignal string -// ProcessSpawnRequest Request to execute a command synchronously. -type ProcessSpawnRequest = ProcessExecRequest +// ProcessResizeRequest Resize a PTY-backed process. +type ProcessResizeRequest struct { + // Cols New terminal columns. + Cols int `json:"cols"` + + // Rows New terminal rows. + Rows int `json:"rows"` +} + +// ProcessSpawnRequest defines model for ProcessSpawnRequest. +type ProcessSpawnRequest struct { + // AllocateTty Allocate a pseudo-terminal (PTY) for the process to enable interactive shells. + AllocateTty *bool `json:"allocate_tty,omitempty"` + + // Args Command arguments. + Args *[]string `json:"args,omitempty"` + + // AsRoot Run the process with root privileges. + AsRoot *bool `json:"as_root,omitempty"` + + // AsUser Run the process as this user. + AsUser *string `json:"as_user"` + + // Cols Initial terminal columns when allocate_tty is true. + Cols *int `json:"cols,omitempty"` + + // Command Executable or shell command to run. + Command string `json:"command"` + + // Cwd Working directory (absolute path) to run the command in. + Cwd *string `json:"cwd"` + + // Env Environment variables to set for the process. + Env *map[string]string `json:"env,omitempty"` + + // Rows Initial terminal rows when allocate_tty is true. + Rows *int `json:"rows,omitempty"` + + // TimeoutSec Maximum execution time in seconds. + TimeoutSec *int `json:"timeout_sec"` +} // ProcessSpawnResult Information about a spawned process. type ProcessSpawnResult struct { @@ -720,6 +759,9 @@ type ProcessSpawnJSONRequestBody = ProcessSpawnRequest // ProcessKillJSONRequestBody defines body for ProcessKill for application/json ContentType. type ProcessKillJSONRequestBody = ProcessKillRequest +// ProcessResizeJSONRequestBody defines body for ProcessResize for application/json ContentType. +type ProcessResizeJSONRequestBody = ProcessResizeRequest + // ProcessStdinJSONRequestBody defines body for ProcessStdin for application/json ContentType. type ProcessStdinJSONRequestBody = ProcessStdinRequest @@ -938,6 +980,11 @@ type ClientInterface interface { ProcessKill(ctx context.Context, processId openapi_types.UUID, body ProcessKillJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ProcessResizeWithBody request with any body + ProcessResizeWithBody(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ProcessResize(ctx context.Context, processId openapi_types.UUID, body ProcessResizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ProcessStatus request ProcessStatus(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1583,6 +1630,30 @@ func (c *Client) ProcessKill(ctx context.Context, processId openapi_types.UUID, return c.Client.Do(req) } +func (c *Client) ProcessResizeWithBody(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewProcessResizeRequestWithBody(c.Server, processId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ProcessResize(ctx context.Context, processId openapi_types.UUID, body ProcessResizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewProcessResizeRequest(c.Server, processId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ProcessStatus(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewProcessStatusRequest(c.Server, processId) if err != nil { @@ -3025,6 +3096,53 @@ func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, return req, nil } +// NewProcessResizeRequest calls the generic ProcessResize builder with application/json body +func NewProcessResizeRequest(server string, processId openapi_types.UUID, body ProcessResizeJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewProcessResizeRequestWithBody(server, processId, "application/json", bodyReader) +} + +// NewProcessResizeRequestWithBody generates requests for ProcessResize with any type of body +func NewProcessResizeRequestWithBody(server string, processId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/process/%s/resize", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewProcessStatusRequest generates requests for ProcessStatus func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http.Request, error) { var err error @@ -3512,6 +3630,11 @@ type ClientWithResponsesInterface interface { ProcessKillWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessKillJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessKillResponse, error) + // ProcessResizeWithBodyWithResponse request with any body + ProcessResizeWithBodyWithResponse(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessResizeResponse, error) + + ProcessResizeWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessResizeJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessResizeResponse, error) + // ProcessStatusWithResponse request ProcessStatusWithResponse(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*ProcessStatusResponse, error) @@ -4284,6 +4407,31 @@ func (r ProcessKillResponse) StatusCode() int { return 0 } +type ProcessResizeResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON404 *NotFoundError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ProcessResizeResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ProcessResizeResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ProcessStatusResponse struct { Body []byte HTTPResponse *http.Response @@ -4916,6 +5064,23 @@ func (c *ClientWithResponses) ProcessKillWithResponse(ctx context.Context, proce return ParseProcessKillResponse(rsp) } +// ProcessResizeWithBodyWithResponse request with arbitrary body returning *ProcessResizeResponse +func (c *ClientWithResponses) ProcessResizeWithBodyWithResponse(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessResizeResponse, error) { + rsp, err := c.ProcessResizeWithBody(ctx, processId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessResizeResponse(rsp) +} + +func (c *ClientWithResponses) ProcessResizeWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessResizeJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessResizeResponse, error) { + rsp, err := c.ProcessResize(ctx, processId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessResizeResponse(rsp) +} + // ProcessStatusWithResponse request returning *ProcessStatusResponse func (c *ClientWithResponses) ProcessStatusWithResponse(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*ProcessStatusResponse, error) { rsp, err := c.ProcessStatus(ctx, processId, reqEditors...) @@ -6204,6 +6369,53 @@ func ParseProcessKillResponse(rsp *http.Response) (*ProcessKillResponse, error) return response, nil } +// ParseProcessResizeResponse parses an HTTP response from a ProcessResizeWithResponse call +func ParseProcessResizeResponse(rsp *http.Response) (*ProcessResizeResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ProcessResizeResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest OkResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseProcessStatusResponse parses an HTTP response from a ProcessStatusWithResponse call func ParseProcessStatusResponse(rsp *http.Response) (*ProcessStatusResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -6626,6 +6838,9 @@ type ServerInterface interface { // Send signal to process // (POST /process/{process_id}/kill) ProcessKill(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) + // Resize a PTY-backed process + // (POST /process/{process_id}/resize) + ProcessResize(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) // Get process status // (GET /process/{process_id}/status) ProcessStatus(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) @@ -6842,6 +7057,12 @@ func (_ Unimplemented) ProcessKill(w http.ResponseWriter, r *http.Request, proce w.WriteHeader(http.StatusNotImplemented) } +// Resize a PTY-backed process +// (POST /process/{process_id}/resize) +func (_ Unimplemented) ProcessResize(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) { + w.WriteHeader(http.StatusNotImplemented) +} + // Get process status // (GET /process/{process_id}/status) func (_ Unimplemented) ProcessStatus(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) { @@ -7518,6 +7739,31 @@ func (siw *ServerInterfaceWrapper) ProcessKill(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// ProcessResize operation middleware +func (siw *ServerInterfaceWrapper) ProcessResize(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "process_id" ------------- + var processId openapi_types.UUID + + err = runtime.BindStyledParameterWithOptions("simple", "process_id", chi.URLParam(r, "process_id"), &processId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "process_id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ProcessResize(w, r, processId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ProcessStatus operation middleware func (siw *ServerInterfaceWrapper) ProcessStatus(w http.ResponseWriter, r *http.Request) { @@ -7882,6 +8128,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/process/{process_id}/kill", wrapper.ProcessKill) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/process/{process_id}/resize", wrapper.ProcessResize) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/process/{process_id}/status", wrapper.ProcessStatus) }) @@ -9223,6 +9472,51 @@ func (response ProcessKill500JSONResponse) VisitProcessKillResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type ProcessResizeRequestObject struct { + ProcessId openapi_types.UUID `json:"process_id"` + Body *ProcessResizeJSONRequestBody +} + +type ProcessResizeResponseObject interface { + VisitProcessResizeResponse(w http.ResponseWriter) error +} + +type ProcessResize200JSONResponse OkResponse + +func (response ProcessResize200JSONResponse) VisitProcessResizeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ProcessResize400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response ProcessResize400JSONResponse) VisitProcessResizeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ProcessResize404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response ProcessResize404JSONResponse) VisitProcessResizeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type ProcessResize500JSONResponse struct{ InternalErrorJSONResponse } + +func (response ProcessResize500JSONResponse) VisitProcessResizeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type ProcessStatusRequestObject struct { ProcessId openapi_types.UUID `json:"process_id"` } @@ -9719,6 +10013,9 @@ type StrictServerInterface interface { // Send signal to process // (POST /process/{process_id}/kill) ProcessKill(ctx context.Context, request ProcessKillRequestObject) (ProcessKillResponseObject, error) + // Resize a PTY-backed process + // (POST /process/{process_id}/resize) + ProcessResize(ctx context.Context, request ProcessResizeRequestObject) (ProcessResizeResponseObject, error) // Get process status // (GET /process/{process_id}/status) ProcessStatus(ctx context.Context, request ProcessStatusRequestObject) (ProcessStatusResponseObject, error) @@ -10699,6 +10996,39 @@ func (sh *strictHandler) ProcessKill(w http.ResponseWriter, r *http.Request, pro } } +// ProcessResize operation middleware +func (sh *strictHandler) ProcessResize(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) { + var request ProcessResizeRequestObject + + request.ProcessId = processId + + var body ProcessResizeJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ProcessResize(ctx, request.(ProcessResizeRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ProcessResize") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ProcessResizeResponseObject); ok { + if err := validResponse.VisitProcessResizeResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ProcessStatus operation middleware func (sh *strictHandler) ProcessStatus(w http.ResponseWriter, r *http.Request, processId openapi_types.UUID) { var request ProcessStatusRequestObject @@ -10930,123 +11260,126 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 6529c84a..f2b8d3ac 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -179,6 +179,36 @@ paths: $ref: "#/components/responses/NotFoundError" "500": $ref: "#/components/responses/InternalError" + /process/{process_id}/resize: + post: + summary: Resize a PTY-backed process + operationId: processResize + parameters: + - name: process_id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessResizeRequest" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/OkResponse" + "400": + $ref: "#/components/responses/BadRequestError" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalError" /recording/stop: post: summary: Stop the recording @@ -1405,6 +1435,20 @@ components: ProcessSpawnRequest: allOf: - $ref: "#/components/schemas/ProcessExecRequest" + - type: object + properties: + allocate_tty: + type: boolean + description: Allocate a pseudo-terminal (PTY) for the process to enable interactive shells. + default: false + rows: + type: integer + description: Initial terminal rows when allocate_tty is true. + minimum: 1 + cols: + type: integer + description: Initial terminal columns when allocate_tty is true. + minimum: 1 ProcessSpawnResult: type: object description: Information about a spawned process. @@ -1421,6 +1465,20 @@ components: format: date-time description: Timestamp when the process started. additionalProperties: false + ProcessResizeRequest: + type: object + description: Resize a PTY-backed process. + required: [rows, cols] + properties: + rows: + type: integer + minimum: 1 + description: New terminal rows. + cols: + type: integer + minimum: 1 + description: New terminal columns. + additionalProperties: false ProcessStatus: type: object description: Current status of a process.