blob: 3dbc085035179be1b0318d7c1e1fa98501152a45 [file] [edit]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !plan9
package main
import (
"errors"
"fmt"
"io"
"log"
"os/exec"
"github.com/gliderlabs/ssh"
"github.com/pkg/sftp"
)
// sshSubsystems are the subsystem handlers for the buildlet's SSH server.
var sshSubsystems = map[string]ssh.SubsystemHandler{
"sftp": sshHandlerSFTP,
}
// sshHandlerSFTP serves the sftp subsystem, which scp and sftp use to
// copy files to and from the buildlet (go.dev/issue/21140). Relative
// paths name files in the buildlet's work directory.
func sshHandlerSFTP(s ssh.Session) {
srv, err := sftp.NewServer(s, sftp.WithServerWorkingDirectory(*workDir))
if err != nil {
log.Printf("starting sftp server: %s", err)
fmt.Fprintf(s.Stderr(), "starting sftp server: %s\n", err)
s.Exit(255)
return
}
defer srv.Close()
if err := srv.Serve(); err != nil && !errors.Is(err, io.EOF) {
log.Printf("sftp server: %s", err)
fmt.Fprintf(s.Stderr(), "sftp server: %s\n", err)
s.Exit(255)
return
}
s.Exit(0)
}
// sshHandlerDirect handles a session that did not request a pty: an
// exec request ("gomote ssh instance cmd..."), a shell reading
// commands from piped standard input, or the legacy scp protocol
// (go.dev/issue/21140). It connects the command to the session's own
// streams instead of a pty, so data passes through byte for byte, and
// it propagates the command's exit status.
func sshHandlerDirect(s ssh.Session) {
fail := func(format string, args ...any) {
fmt.Fprintf(s.Stderr(), format, args...)
s.Exit(255)
}
cmd := shellCommand(s.Context(), s.RawCommand())
cmd.Dir = *workDir
stdin, err := cmd.StdinPipe()
if err != nil {
fail("%v\n", err)
return
}
cmd.Stdout = s
cmd.Stderr = s.Stderr()
if err := cmd.Start(); err != nil {
log.Printf("unable to start shell: %s", err)
fail("unable to start shell %q: %s\n", shell(), err)
return
}
// Copy session input on the side: the copy blocks until the client
// sends EOF or disconnects, which must not keep Wait from returning
// once the command exits.
go func() {
io.Copy(stdin, s)
stdin.Close()
}()
err = cmd.Wait()
code := 0
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if code = exitErr.ExitCode(); code < 0 {
code = 255
}
} else if err != nil {
fail("running shell %q: %s\n", shell(), err)
return
}
s.Exit(code)
}