add tcp daemon listener with token auth

This commit is contained in:
2026-05-27 14:19:14 +01:00
parent 5149224000
commit 63cb8a4388
6 changed files with 368 additions and 12 deletions

View File

@@ -3,6 +3,7 @@ package app
import (
"context"
"encoding/json"
"io"
"net"
"os"
"path/filepath"
@@ -85,6 +86,80 @@ func TestDaemonDetachReattachPreservesProcess(t *testing.T) {
}
}
func TestDaemonTCPTokenAuthAndUnixExemption(t *testing.T) {
root := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", filepath.Join(root, "config"))
t.Setenv("XDG_DATA_HOME", filepath.Join(root, "data"))
t.Setenv("XDG_RUNTIME_DIR", filepath.Join(root, "runtime"))
projectDir := filepath.Join(root, "project")
if err := os.MkdirAll(projectDir, 0o700); err != nil {
t.Fatalf("mkdir project: %v", err)
}
socket := filepath.Join(root, "runtime", "patterm", "daemon.sock")
pid := filepath.Join(root, "runtime", "patterm", "daemon.pid")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errCh := make(chan error, 1)
ready := make(chan string, 1)
go func() {
errCh <- RunDaemon(ctx, DaemonOptions{
ProjectDir: projectDir,
SocketPath: socket,
PidPath: pid,
ListenAddr: "127.0.0.1:0",
Token: "secret-token",
TokenOut: io.Discard,
ListenReady: ready,
Cols: 80,
Rows: 24,
})
}()
waitForSocket(t, socket, errCh)
tcpAddr := waitForTCPAddr(t, ready, errCh)
assertTCPAttachDenied(t, tcpAddr, "")
assertTCPAttachDenied(t, tcpAddr, "wrong-token")
tcpClient := dialTCPDaemon(t, tcpAddr)
defer tcpClient.Close()
sendFrame(t, tcpClient, protocol.FrameAttach, protocol.Attach{
Token: "secret-token",
ProjectPath: projectDir,
TermSize: protocol.Size{Cols: 80, Rows: 24},
})
expectFrame(t, tcpClient, protocol.FrameHello)
expectFrame(t, tcpClient, protocol.FrameProjectList)
expectFrame(t, tcpClient, protocol.FrameChrome)
data, _ := json.Marshal(map[string]any{
"argv": []string{"sh", "-c", "trap 'exit 0' TERM; echo TCP-SNAPSHOT; sleep 30"},
"name": "tcp-survivor",
})
sendFrame(t, tcpClient, protocol.FramePaletteCommand, protocol.PaletteCommand{
Kind: "spawn_command",
Data: data,
})
expectFrame(t, tcpClient, protocol.FramePaneSnapshot)
unixClient := dialDaemon(t, socket)
defer unixClient.Close()
sendFrame(t, unixClient, protocol.FrameAttach, protocol.Attach{
ProjectPath: projectDir,
TermSize: protocol.Size{Cols: 80, Rows: 24},
})
expectFrame(t, unixClient, protocol.FrameHello)
cancel()
select {
case err := <-errCh:
if err != nil {
t.Fatalf("daemon returned error: %v", err)
}
case <-time.After(3 * time.Second):
t.Fatalf("daemon did not stop")
}
}
func waitForSocket(t *testing.T, socket string, errCh <-chan error) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
@@ -114,6 +189,49 @@ func dialDaemon(t *testing.T, socket string) protocol.Transport {
return protocol.NewConnTransport(conn)
}
func dialTCPDaemon(t *testing.T, addr string) protocol.Transport {
t.Helper()
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Fatalf("dial tcp daemon: %v", err)
}
return protocol.NewConnTransport(conn)
}
func waitForTCPAddr(t *testing.T, ready <-chan string, errCh <-chan error) string {
t.Helper()
select {
case addr := <-ready:
return addr
case err := <-errCh:
if err != nil && strings.Contains(err.Error(), "operation not permitted") {
t.Skipf("tcp sockets unavailable in this sandbox: %v", err)
}
t.Fatalf("daemon exited before TCP listener was ready: %v", err)
case <-time.After(3 * time.Second):
t.Fatalf("tcp listener was not ready")
}
return ""
}
func assertTCPAttachDenied(t *testing.T, addr, token string) {
t.Helper()
client := dialTCPDaemon(t, addr)
defer client.Close()
sendFrame(t, client, protocol.FrameAttach, protocol.Attach{
Token: token,
TermSize: protocol.Size{Cols: 80, Rows: 24},
})
f := expectFrame(t, client, protocol.FrameError)
msg, err := protocol.Decode[protocol.Error](f)
if err != nil {
t.Fatalf("decode error frame: %v", err)
}
if !strings.Contains(msg.Message, "auth denied") {
t.Fatalf("error message = %q, want auth denied", msg.Message)
}
}
func sendFrame[T any](t *testing.T, tr protocol.Transport, typ protocol.FrameType, payload T) {
t.Helper()
f, err := protocol.NewFrame(typ, payload)