1package e2e23import (4 "bufio"5 "os"6 "os/exec"7 "path/filepath"8 "regexp"9 "strings"10 "testing"11 "time"12)1314// serveSSH starts 012 serve on a random loopback port for served, with15// flags, authorizing a new key, and returns the system's ssh client and16// the arguments that reach the server with it, checking the host key.17func serveSSH(t *testing.T, top, served string, flags ...string) (string, []string) {18 t.Helper()19 sshPath, err1 := exec.LookPath("ssh")20 keygen, err2 := exec.LookPath("ssh-keygen")21 if err1 != nil || err2 != nil {22 t.Skip("needs ssh and ssh-keygen")23 }24 id := filepath.Join(top, "id_ed25519")25 if out, err := exec.Command(keygen, "-q", "-t", "ed25519", "-N", "", "-C", "e2e", "-f", id).CombinedOutput(); err != nil {26 t.Fatalf("ssh-keygen: %v\n%s", err, out)27 }28 pub, err := os.ReadFile(id + ".pub")29 if err != nil {30 t.Fatal(err)31 }32 ak := filepath.Join(top, "authorized_keys")33 if err := os.WriteFile(ak, pub, 0o600); err != nil {34 t.Fatal(err)35 }36 hostKey := filepath.Join(top, "config", "host_key")37 srv := exec.Command(binPath, append(append([]string{"serve", "--listen", "127.0.0.1:0", "--authorized-keys", ak, "--host-key", hostKey}, flags...), served)...)38 srv.Env = append(os.Environ(), "TYPESAFE_API_KEY=", "TYPESAFE_BASE_URL=")39 stderr, err := srv.StderrPipe()40 if err != nil {41 t.Fatal(err)42 }43 if err := srv.Start(); err != nil {44 t.Fatal(err)45 }46 t.Cleanup(func() {47 srv.Process.Kill()48 srv.Wait()49 })50 // "012 serving DIR on 127.0.0.1:PORT"51 port := make(chan string, 1)52 go func() {53 re := regexp.MustCompile(` on 127\.0\.0\.1:(\d+)$`)54 sc := bufio.NewScanner(stderr)55 for sc.Scan() {56 if m := re.FindStringSubmatch(sc.Text()); m != nil {57 port <- m[1]58 }59 }60 }()61 var p string62 select {63 case p = <-port:64 case <-time.After(waitTimeout):65 t.Fatal("012 serve didn't start")66 }67 hostPub, err := os.ReadFile(hostKey + ".pub")68 if err != nil {69 t.Fatal(err)70 }71 knownHosts := filepath.Join(top, "known_hosts")72 os.WriteFile(knownHosts, []byte("[127.0.0.1]:"+p+" "+string(hostPub)), 0o600)73 return sshPath, []string{"-F", "/dev/null", "-i", id, "-e", "none", "-p", p,74 "-o", "IdentitiesOnly=yes", "-o", "IdentityAgent=none", "-o", "BatchMode=yes",75 "-o", "UserKnownHostsFile=" + knownHosts, "-o", "StrictHostKeyChecking=yes", "-o", "LogLevel=ERROR",76 "-t", "127.0.0.1"}77}7879// 012 serve through a real ssh client in a real terminal: the first80// screen, typing, resizing, saving inside the served directory, a81// refused escape, and quitting closing the connection.82func TestServeOverSSH(t *testing.T) {83 t.Parallel()84 top := t.TempDir()85 served := filepath.Join(top, "served")86 os.Mkdir(served, 0o755)87 sshPath, args := serveSSH(t, top, served)8889 s := startWith(t, options{dir: top, program: sshPath}, args...)90 s.waitFor("Sheet1")91 s.keys("hello", "<enter>")92 s.waitForLine(gridRow1, " 1 hello")9394 // The window follows the client's terminal.95 s.resize(120, 40)96 s.eventually("column K after resizing", func() bool { return strings.Contains(s.line(3), " K") })9798 s.keys("<ctrl+s>", "greeting", "<enter>")99 s.eventually("greeting.012 in the served directory", func() bool {100 _, err := os.Stat(filepath.Join(served, "greeting.012"))101 return err == nil102 })103 s.waitFor("greeting.012")104105 s.keys("<ctrl+o>", "../id_ed25519", "<enter>")106 s.waitFor("outside the served directory")107 s.keys("<esc>")108109 s.keys("<ctrl+q>")110 s.waitExit()111}112113// ssh -t host book.012 opens the file from the served directory; a name114// outside it is refused before 012 starts.115func TestServeOpensFileFromCommandLine(t *testing.T) {116 t.Parallel()117 top := t.TempDir()118 served := filepath.Join(top, "served")119 os.Mkdir(served, 0o755)120 book := `{"version": 2, "cells": {"A1": "opened over ssh"}}`121 if err := os.WriteFile(filepath.Join(served, "book.012"), []byte(book), 0o644); err != nil {122 t.Fatal(err)123 }124 sshPath, args := serveSSH(t, top, served)125126 s := startWith(t, options{dir: top, program: sshPath}, append(args, "book.012")...)127 s.waitForLine(gridRow1, " 1 opened over ssh")128 s.keys("<ctrl+q>")129 s.waitExit()130131 s = startWith(t, options{dir: top, program: sshPath, startsOn: "outside the served directory"}, append(args, "../id_ed25519")...)132 s.waitExit()133}