- Implement SSH dial via stdio for remote connections - Add sources API and storage layer for managing connection sources - Add SourcePanel and SshFields web components for SSH configuration - Update app structure to support source-based connections - Update handlers and server for new sources endpoint Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
80 lines
2.6 KiB
Go
80 lines
2.6 KiB
Go
package sshx
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestDialAPICommand pins the command the tunnel runs on the source host: it is
|
|
// the same one `docker -H ssh://…` uses, and the sudo / custom binary settings
|
|
// have to reach it.
|
|
func TestDialAPICommand(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
rd *RemoteDocker
|
|
want string
|
|
}{
|
|
{"plain", &RemoteDocker{binary: "docker"}, "docker system dial-stdio"},
|
|
{"sudo", &RemoteDocker{binary: "docker", sudo: true}, "sudo -n docker system dial-stdio"},
|
|
{"podman", &RemoteDocker{binary: "podman"}, "podman system dial-stdio"},
|
|
{"path with a space", &RemoteDocker{binary: "/opt/my docker/bin/docker"}, "'/opt/my docker/bin/docker' system dial-stdio"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := tc.rd.Cmd("system", "dial-stdio"); got != tc.want {
|
|
t.Fatalf("command = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAPIConnSurfacesRemoteStderr covers the failure that would otherwise reach
|
|
// the Docker client as a bare EOF: the remote docker printing a reason and
|
|
// exiting.
|
|
func TestAPIConnSurfacesRemoteStderr(t *testing.T) {
|
|
errBuf := &syncBuffer{}
|
|
errBuf.Write([]byte("docker: 'system dial-stdio' is not a docker command\n"))
|
|
c := &apiConn{
|
|
stdout: strings.NewReader(""),
|
|
stdin: nopWriteCloser{io.Discard},
|
|
stderr: errBuf,
|
|
}
|
|
_, err := c.Read(make([]byte, 8))
|
|
if err == nil {
|
|
t.Fatal("a closed stream with remote stderr should be an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "is not a docker command") {
|
|
t.Fatalf("error = %v, want the remote stderr in it", err)
|
|
}
|
|
// Without stderr the plain EOF must survive, or the HTTP transport cannot
|
|
// tell a finished response from a broken one.
|
|
quiet := &apiConn{stdout: strings.NewReader(""), stdin: nopWriteCloser{io.Discard}, stderr: &syncBuffer{}}
|
|
if _, err := quiet.Read(make([]byte, 8)); !errors.Is(err, io.EOF) {
|
|
t.Fatalf("error = %v, want io.EOF", err)
|
|
}
|
|
}
|
|
|
|
func TestAPIConnDeadlinesAreNoops(t *testing.T) {
|
|
var c net.Conn = &apiConn{stdout: strings.NewReader(""), stdin: nopWriteCloser{io.Discard}, stderr: &syncBuffer{}}
|
|
now := time.Now()
|
|
if err := c.SetDeadline(now); err != nil {
|
|
t.Fatalf("SetDeadline: %v", err)
|
|
}
|
|
if err := c.SetReadDeadline(now); err != nil {
|
|
t.Fatalf("SetReadDeadline: %v", err)
|
|
}
|
|
if err := c.SetWriteDeadline(now); err != nil {
|
|
t.Fatalf("SetWriteDeadline: %v", err)
|
|
}
|
|
if c.RemoteAddr().Network() != "ssh" {
|
|
t.Fatalf("network = %q, want ssh", c.RemoteAddr().Network())
|
|
}
|
|
}
|
|
|
|
type nopWriteCloser struct{ io.Writer }
|
|
|
|
func (nopWriteCloser) Close() error { return nil }
|