server_test.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. //go:build !windows
  2. package localapi
  3. import (
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "flag"
  9. "fmt"
  10. "io"
  11. "math"
  12. "net"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. "sync"
  17. "testing"
  18. "time"
  19. "github.com/slackhq/nebula/commands"
  20. "github.com/slackhq/nebula/test"
  21. "github.com/stretchr/testify/assert"
  22. "github.com/stretchr/testify/require"
  23. "golang.org/x/sys/unix"
  24. )
  25. // testSocket returns a short socket path whose directory does not exist yet and whose
  26. // parent is private: one level below the test's own directory, so that listen has a
  27. // directory of its own to create.
  28. func testSocket(t *testing.T) string {
  29. t.Helper()
  30. return filepath.Join(test.ShortTempDir(t), "n", "s.sock")
  31. }
  32. func serve(t *testing.T, reg *commands.Registry) (*Server, string) {
  33. t.Helper()
  34. path := testSocket(t)
  35. s := NewServer(test.NewLogger(), reg)
  36. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  37. t.Cleanup(s.Stop)
  38. return s, path
  39. }
  40. // serveAt starts a server on an exact path, for tests that care where the socket is.
  41. func serveAt(t *testing.T, path string) (*Server, func()) {
  42. t.Helper()
  43. s := NewServer(test.NewLogger(), basicRegistry())
  44. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  45. return s, s.Stop
  46. }
  47. // echoCommand reports its operands, for tests on either surface.
  48. func echoCommand() *commands.Command {
  49. return &commands.Command{
  50. Name: "echo",
  51. ShortDescription: "echoes its arguments",
  52. Run: func(fs any, a []string) (commands.Result, error) {
  53. return commands.Value(fmt.Sprintf("%v", a)), nil
  54. },
  55. }
  56. }
  57. func basicRegistry() *commands.Registry {
  58. reg := commands.New()
  59. reg.Register(echoCommand())
  60. reg.Register(&commands.Command{
  61. Name: "boom",
  62. ShortDescription: "panics",
  63. Run: func(fs any, a []string) (commands.Result, error) {
  64. panic("this command is broken")
  65. },
  66. })
  67. return reg
  68. }
  69. // run invokes one command and returns its output and its error text. An empty error is
  70. // success: the protocol has no separate status field.
  71. func run(t *testing.T, path, command string, args ...string) (string, string) {
  72. t.Helper()
  73. res, err := (&Client{Path: path}).Invoke(command, nil, args, "")
  74. require.NoError(t, err)
  75. return textOf(res), res.Error
  76. }
  77. // textOf is what a test reads as a command's output: the text when text was asked for,
  78. // else the JSON result, unquoted when it is a JSON string so a test can compare words.
  79. func textOf(res *Response) string {
  80. if len(res.Text) > 0 {
  81. return string(res.Text)
  82. }
  83. if len(res.Result) > 0 && res.Result[0] == '"' {
  84. var s string
  85. if err := json.Unmarshal(res.Result, &s); err == nil {
  86. return s
  87. }
  88. }
  89. return string(res.Result)
  90. }
  91. func TestServeAllowedCommand(t *testing.T) {
  92. _, path := serve(t, basicRegistry())
  93. out, errOut := run(t, path, "echo", "a", "b")
  94. assert.Equal(t, "[a b]", out)
  95. assert.Empty(t, errOut, "a command that worked reports no error")
  96. }
  97. // TestPanicInCommandDoesNotKillTheDaemon is ADR 003's requirement: a broken command
  98. // costs a connection, not the overlay.
  99. func TestPanicInCommandDoesNotKillTheDaemon(t *testing.T) {
  100. _, path := serve(t, basicRegistry())
  101. out, errOut := run(t, path, "boom")
  102. assert.Empty(t, out)
  103. assert.Contains(t, errOut, "command failed")
  104. // The server is still serving, which is the point of the test.
  105. out, errOut = run(t, path, "echo", "still", "here")
  106. assert.Equal(t, "[still here]", out)
  107. assert.Empty(t, errOut)
  108. }
  109. // TestSocketPermissions: the modes are pinned, not left to the umask. A 077 umask would
  110. // make the directory 0700 and hide a root daemon's socket from an unprivileged nebula
  111. // ctl.
  112. func TestSocketPermissions(t *testing.T) {
  113. for _, umask := range []int{0, 0077} {
  114. t.Run(fmt.Sprintf("umask %04o", umask), func(t *testing.T) {
  115. old := unix.Umask(umask)
  116. defer unix.Umask(old)
  117. _, path := serve(t, basicRegistry())
  118. fi, err := os.Stat(path)
  119. require.NoError(t, err)
  120. assert.Equal(t, os.FileMode(0600), fi.Mode().Perm(), "socket must not be reachable by other users")
  121. di, err := os.Stat(filepath.Dir(path))
  122. require.NoError(t, err)
  123. assert.Equal(t, os.FileMode(0755), di.Mode().Perm(), "a directory we create ourselves is searchable, so ServedByRoot can see into it")
  124. })
  125. }
  126. }
  127. func TestLiveSocketIsNotStolen(t *testing.T) {
  128. first, path := serve(t, basicRegistry())
  129. require.NotNil(t, first)
  130. second := NewServer(test.NewLogger(), basicRegistry())
  131. err := second.Rebind(context.Background(), path, ownerOnly())
  132. assert.ErrorContains(t, err, "already in use", "a live endpoint must not be unlinked by a second instance")
  133. }
  134. // TestLiveSocketIsNotStolenBeforeItsChmod: between an instance's bind and its chmod the
  135. // socket is at whatever mode the umask left. A second instance starting in that window
  136. // must still see it as live, not stale: only a refused connect proves nothing is behind
  137. // the file.
  138. func TestLiveSocketIsNotStolenBeforeItsChmod(t *testing.T) {
  139. first, path := serve(t, basicRegistry())
  140. require.NotNil(t, first)
  141. require.NoError(t, os.Chmod(path, 0666))
  142. second := NewServer(test.NewLogger(), basicRegistry())
  143. err := second.Rebind(context.Background(), path, ownerOnly())
  144. require.ErrorContains(t, err, "already in use", "a live socket at any mode is not thereby dead")
  145. _, err = os.Stat(path)
  146. require.NoError(t, err, "the first instance's socket is still there")
  147. require.NoError(t, os.Chmod(path, 0600))
  148. out, _ := run(t, path, "echo", "ok")
  149. assert.Equal(t, "[ok]", out, "and still served")
  150. }
  151. func TestRebindMovesTheSocket(t *testing.T) {
  152. s, path := serve(t, basicRegistry())
  153. next := filepath.Join(filepath.Dir(path), "n.sock")
  154. require.NoError(t, s.Rebind(context.Background(), next, ownerOnly()))
  155. assert.Equal(t, next, s.Path())
  156. out, _ := run(t, next, "echo", "moved")
  157. assert.Equal(t, "[moved]", out)
  158. _, err := os.Stat(path)
  159. assert.True(t, os.IsNotExist(err), "the old socket should be unlinked")
  160. }
  161. func TestRebindFailureKeepsTheOldSocket(t *testing.T) {
  162. s, path := serve(t, basicRegistry())
  163. err := s.Rebind(context.Background(), "/nonexistent-dir-for-tests/x.sock", ownerOnly())
  164. require.Error(t, err)
  165. assert.Equal(t, path, s.Path(), "a bad path must leave the working endpoint alone")
  166. out, _ := run(t, path, "echo", "ok")
  167. assert.Equal(t, "[ok]", out)
  168. }
  169. // TestSemaphoreSurvivesRestart pins the rule that the connection limiter is made once. A
  170. // handler releases into the channel it acquired from, so if Rebind replaced it after a
  171. // Stop, a command still running across the restart would block forever on the new,
  172. // empty channel and leak its goroutine and connection.
  173. func TestSemaphoreSurvivesRestart(t *testing.T) {
  174. s := NewServer(test.NewLogger(), basicRegistry())
  175. path := testSocket(t)
  176. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  177. first := s.sem
  178. require.NotNil(t, first)
  179. s.Stop()
  180. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  181. defer s.Stop()
  182. assert.Equal(t, first, s.sem, "Rebind after Stop must keep the same semaphore")
  183. require.NoError(t, s.Rebind(context.Background(), testSocket(t), ownerOnly()))
  184. assert.Equal(t, first, s.sem, "Rebind must keep the same semaphore")
  185. }
  186. // TestRestartIsWatchedOnItsOwnContext: the ctx watcher is installed by the first bind of
  187. // a run and cleared by Stop, so a server stopped by one context's cancellation and
  188. // started again under another is stopped when that second context is cancelled, not
  189. // left serving until the process exits.
  190. func TestRestartIsWatchedOnItsOwnContext(t *testing.T) {
  191. s := NewServer(test.NewLogger(), basicRegistry())
  192. path := testSocket(t)
  193. ctxA, cancelA := context.WithCancel(context.Background())
  194. require.NoError(t, s.Rebind(ctxA, path, ownerOnly()))
  195. cancelA()
  196. require.Eventually(t, func() bool { return s.Path() == "" }, time.Second, 10*time.Millisecond, "the first context must stop the server")
  197. ctxB, cancelB := context.WithCancel(context.Background())
  198. require.NoError(t, s.Rebind(ctxB, path, ownerOnly()))
  199. defer s.Stop()
  200. assert.Equal(t, path, s.Path())
  201. cancelB()
  202. require.Eventually(t, func() bool { return s.Path() == "" }, time.Second, 10*time.Millisecond, "the second context must stop the server too")
  203. _, err := os.Stat(path)
  204. assert.True(t, os.IsNotExist(err), "the socket must be removed when the second context stops the server")
  205. }
  206. // TestStopDetachesTheWatcher: a server stopped by Stop and started again under another
  207. // context belongs to that context alone. The one that started the earlier run, ending
  208. // later, must not take the new run down with it.
  209. func TestStopDetachesTheWatcher(t *testing.T) {
  210. s := NewServer(test.NewLogger(), basicRegistry())
  211. path := testSocket(t)
  212. ctxA, cancelA := context.WithCancel(context.Background())
  213. t.Cleanup(cancelA)
  214. require.NoError(t, s.Rebind(ctxA, path, ownerOnly()))
  215. s.Stop()
  216. ctxB, cancelB := context.WithCancel(context.Background())
  217. t.Cleanup(cancelB)
  218. require.NoError(t, s.Rebind(ctxB, path, ownerOnly()))
  219. t.Cleanup(s.Stop)
  220. cancelA()
  221. time.Sleep(50 * time.Millisecond)
  222. assert.Equal(t, path, s.Path(), "the first run's context has nothing to do with the second run")
  223. _, err := (&Client{Path: path}).Describe()
  224. require.NoError(t, err)
  225. cancelB()
  226. require.Eventually(t, func() bool { return s.Path() == "" }, time.Second, 10*time.Millisecond, "the second run's context stops it")
  227. }
  228. func TestVersionMismatchIsExplained(t *testing.T) {
  229. _, path := serve(t, basicRegistry())
  230. conn, err := net.Dial("unix", path)
  231. require.NoError(t, err)
  232. defer conn.Close()
  233. body, _ := json.Marshal(Request{Version: ProtocolVersion + 1, Command: "echo"})
  234. _, err = conn.Write(append(body, '\n'))
  235. require.NoError(t, err)
  236. res, err := readResponse(conn)
  237. require.NoError(t, err)
  238. assert.Contains(t, res.Error, "different versions")
  239. }
  240. func TestOversizedRequestIsRejected(t *testing.T) {
  241. _, path := serve(t, basicRegistry())
  242. conn, err := net.Dial("unix", path)
  243. require.NoError(t, err)
  244. defer conn.Close()
  245. _, err = conn.Write(append(bytes.Repeat([]byte("a"), MaxRequestBytes+10), '\n'))
  246. if err == nil {
  247. res, rerr := readResponse(conn)
  248. require.NoError(t, rerr)
  249. assert.NotEmpty(t, res.Error)
  250. }
  251. }
  252. // TestConcurrentClientsAreBounded checks that going over the connection cap is answered
  253. // rather than queued, so a burst of clients cannot pile up against the packet path.
  254. // TestSlowCommandIsNamedInTheLog: a command that does not return holds its slot for
  255. // good, and enough of them leave the socket answering only "busy". Both are logged, the
  256. // command by name once it has run past slowAfter and each refused connection as it
  257. // happens, so a busy socket can be traced to what wedged it.
  258. func TestSlowCommandIsNamedInTheLog(t *testing.T) {
  259. reg := commands.New()
  260. release := make(chan struct{})
  261. reg.Register(&commands.Command{
  262. Name: "block",
  263. ShortDescription: "blocks until released",
  264. Run: func(fs any, a []string) (commands.Result, error) {
  265. <-release
  266. return commands.Value("done"), nil
  267. },
  268. })
  269. var logs lockedBuffer
  270. s := NewServer(test.NewLoggerWithOutput(&logs), reg)
  271. s.maxConns = 1
  272. s.slowAfter = 20 * time.Millisecond
  273. path := testSocket(t)
  274. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  275. defer s.Stop()
  276. var wg sync.WaitGroup
  277. wg.Add(1)
  278. go func() {
  279. defer wg.Done()
  280. _, _ = (&Client{Path: path}).Invoke("block", nil, []string{"x"}, "")
  281. }()
  282. require.Eventually(t, func() bool {
  283. return strings.Contains(logs.String(), "still running")
  284. }, 2*time.Second, 10*time.Millisecond)
  285. assert.Contains(t, logs.String(), "command=block")
  286. assert.Contains(t, logs.String(), "args=[x]")
  287. res, err := (&Client{Path: path}).Invoke("block", nil, nil, "")
  288. require.NoError(t, err)
  289. assert.Contains(t, res.Error, "busy")
  290. assert.Contains(t, logs.String(), "at capacity")
  291. close(release)
  292. wg.Wait()
  293. }
  294. // TestPanickedCommandIsNotReportedAsStillRunning: the slow command line is about a slot
  295. // that is still held. A command that panicked has given its slot back, and the client
  296. // its answer, so no line must claim otherwise after the fact.
  297. func TestPanickedCommandIsNotReportedAsStillRunning(t *testing.T) {
  298. reg := commands.New()
  299. reg.Register(&commands.Command{
  300. Name: "boom",
  301. ShortDescription: "panics",
  302. Run: func(fs any, a []string) (commands.Result, error) {
  303. panic("this command is broken")
  304. },
  305. })
  306. var logs lockedBuffer
  307. s := NewServer(test.NewLoggerWithOutput(&logs), reg)
  308. s.slowAfter = 20 * time.Millisecond
  309. path := testSocket(t)
  310. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  311. defer s.Stop()
  312. res, err := (&Client{Path: path}).Invoke("boom", nil, nil, "")
  313. require.NoError(t, err)
  314. assert.Equal(t, "command failed", res.Error)
  315. time.Sleep(100 * time.Millisecond)
  316. assert.Contains(t, logs.String(), "Recovered from panic")
  317. assert.NotContains(t, logs.String(), "still running")
  318. }
  319. // lockedBuffer is a strings.Builder the server's goroutines and the test can share.
  320. type lockedBuffer struct {
  321. mu sync.Mutex
  322. b strings.Builder
  323. }
  324. func (l *lockedBuffer) Write(p []byte) (int, error) {
  325. l.mu.Lock()
  326. defer l.mu.Unlock()
  327. return l.b.Write(p)
  328. }
  329. func (l *lockedBuffer) String() string {
  330. l.mu.Lock()
  331. defer l.mu.Unlock()
  332. return l.b.String()
  333. }
  334. // flakyListener fails its first few Accepts the way a process that is out of file
  335. // descriptors does, then behaves.
  336. type flakyListener struct {
  337. net.Listener
  338. failures int
  339. }
  340. func (f *flakyListener) Accept() (net.Conn, error) {
  341. if f.failures > 0 {
  342. f.failures--
  343. return nil, &net.OpError{Op: "accept", Net: "unix", Err: errors.New("too many open files")}
  344. }
  345. return f.Listener.Accept()
  346. }
  347. // TestAcceptSurvivesTransientErrors: an Accept that fails for a reason other than the
  348. // listener being closed is retried, so a moment at the descriptor limit does not take
  349. // the control socket down for the rest of the process. Closing the listener is still
  350. // what ends the loop.
  351. func TestAcceptSurvivesTransientErrors(t *testing.T) {
  352. path := testSocket(t)
  353. require.NoError(t, os.Mkdir(filepath.Dir(path), 0700))
  354. ln, err := listen(path, Access{Mode: ctlSocketMode})
  355. require.NoError(t, err)
  356. s := NewServer(test.NewLogger(), basicRegistry())
  357. done := make(chan struct{})
  358. go func() {
  359. defer close(done)
  360. s.accept(&flakyListener{Listener: ln, failures: 3}, make(chan struct{}, 1))
  361. }()
  362. out, errText := run(t, path, "echo", "still", "here")
  363. assert.Empty(t, errText)
  364. assert.Equal(t, "[still here]", out)
  365. require.NoError(t, ln.Close())
  366. select {
  367. case <-done:
  368. case <-time.After(5 * time.Second):
  369. t.Fatal("accept loop did not exit when the listener was closed")
  370. }
  371. }
  372. func TestStopRemovesTheSocket(t *testing.T) {
  373. s, path := serve(t, basicRegistry())
  374. s.Stop()
  375. _, err := os.Stat(path)
  376. assert.True(t, os.IsNotExist(err))
  377. }
  378. func TestContextCancellationStops(t *testing.T) {
  379. ctx, cancel := context.WithCancel(context.Background())
  380. path := testSocket(t)
  381. s := NewServer(test.NewLogger(), basicRegistry())
  382. require.NoError(t, s.Rebind(ctx, path, ownerOnly()))
  383. cancel()
  384. assert.Eventually(t, func() bool {
  385. _, err := os.Stat(path)
  386. return os.IsNotExist(err)
  387. }, 2*time.Second, 20*time.Millisecond)
  388. }
  389. // TestNamedParametersDriveFlags is the point of the protocol: a caller passes options by
  390. // name, with JSON's own types, and never assembles a command line.
  391. // TestStringParamsNumbers: JSON numbers arrive as float64. Whole ones render as integers
  392. // so int flags accept them, and a whole one outside int64 is refused rather than
  393. // converted, since Go's float-to-int conversion of an out-of-range value is unspecified
  394. // and would set a number the caller never sent.
  395. func TestStringParamsNumbers(t *testing.T) {
  396. cases := map[string]struct {
  397. in float64
  398. want string
  399. wantErr bool
  400. }{
  401. "whole": {in: 3, want: "3"},
  402. "negative whole": {in: -1, want: "-1"},
  403. "fraction": {in: 1.5, want: "1.5"},
  404. "min int64": {in: math.MinInt64, want: "-9223372036854775808"},
  405. "largest in range": {in: 9223372036854774784, want: "9223372036854774784"},
  406. "2^63": {in: 9223372036854775808, wantErr: true},
  407. "1e19": {in: 1e19, wantErr: true},
  408. "below min int64": {in: -9.3e18, wantErr: true},
  409. }
  410. for name, tc := range cases {
  411. t.Run(name, func(t *testing.T) {
  412. out, err := stringParams(map[string]any{"n": tc.in})
  413. if tc.wantErr {
  414. require.Error(t, err)
  415. require.ErrorContains(t, err, `parameter "n" is `)
  416. assert.ErrorContains(t, err, "outside the range of a 64-bit integer")
  417. return
  418. }
  419. require.NoError(t, err)
  420. assert.Equal(t, tc.want, out["n"])
  421. })
  422. }
  423. }
  424. func TestNamedParametersDriveFlags(t *testing.T) {
  425. reg := commands.New()
  426. reg.Register(&commands.Command{
  427. Name: "report",
  428. ShortDescription: "reports its flags",
  429. Flags: func() (*flag.FlagSet, any) {
  430. fl := flag.NewFlagSet("", flag.ContinueOnError)
  431. s := struct {
  432. Verbose bool
  433. Label string
  434. }{}
  435. fl.BoolVar(&s.Verbose, "verbose", false, "say more")
  436. fl.StringVar(&s.Label, "label", "none", "a label")
  437. return fl, &s
  438. },
  439. Run: func(fs any, a []string) (commands.Result, error) {
  440. s := fs.(*struct {
  441. Verbose bool
  442. Label string
  443. })
  444. return commands.Value(fmt.Sprintf("verbose=%v label=%s args=%v", s.Verbose, s.Label, a)), nil
  445. },
  446. })
  447. _, path := serve(t, reg)
  448. res, err := (&Client{Path: path}).Invoke("report",
  449. map[string]string{"verbose": "true", "label": "here"},
  450. []string{"an-operand"},
  451. "",
  452. )
  453. require.NoError(t, err)
  454. assert.Empty(t, res.Error)
  455. assert.Equal(t, "verbose=true label=here args=[an-operand]", textOf(res))
  456. }
  457. // TestUnknownParameterIsReported: a misspelled parameter is an error rather than a
  458. // silently applied default, which is the difference between an API and a guess.
  459. func TestUnknownParameterIsReported(t *testing.T) {
  460. reg := commands.New()
  461. reg.Register(&commands.Command{
  462. Name: "report",
  463. ShortDescription: "takes a flag",
  464. Flags: func() (*flag.FlagSet, any) {
  465. fl := flag.NewFlagSet("", flag.ContinueOnError)
  466. fl.Bool("verbose", false, "say more")
  467. return fl, nil
  468. },
  469. Run: func(fs any, a []string) (commands.Result, error) { return commands.Nothing(), nil },
  470. })
  471. _, path := serve(t, reg)
  472. res, err := (&Client{Path: path}).Invoke("report", map[string]string{"verbos": "true"}, nil, "")
  473. require.NoError(t, err)
  474. assert.Contains(t, res.Error, `report has no parameter "verbos"`)
  475. }
  476. func TestParametersOnACommandThatTakesNoneAreReported(t *testing.T) {
  477. _, path := serve(t, basicRegistry())
  478. res, err := (&Client{Path: path}).Invoke("echo", map[string]string{"nope": "true"}, nil, "")
  479. require.NoError(t, err)
  480. assert.Contains(t, res.Error, "echo takes no parameters")
  481. }
  482. // TestJSONOutputIsAJSONValue: a command that emits JSON lands in the response as JSON,
  483. // not as a string a caller has to parse a second time.
  484. func TestJSONOutputIsAJSONValue(t *testing.T) {
  485. reg := commands.New()
  486. reg.Register(&commands.Command{
  487. Name: "emit",
  488. ShortDescription: "emits json",
  489. Run: func(fs any, a []string) (commands.Result, error) {
  490. return commands.Value(map[string]any{"hosts": []int{1, 2, 3}}), nil
  491. },
  492. })
  493. _, path := serve(t, reg)
  494. res, err := (&Client{Path: path}).Invoke("emit", nil, nil, "")
  495. require.NoError(t, err)
  496. var decoded struct {
  497. Hosts []int `json:"hosts"`
  498. }
  499. require.NoError(t, json.Unmarshal(res.Result, &decoded))
  500. assert.Equal(t, []int{1, 2, 3}, decoded.Hosts)
  501. }
  502. // TestStringValueIsAJSONString: a command whose value is a string answers a JSON request
  503. // with a JSON string, quotes and all.
  504. func TestStringValueIsAJSONString(t *testing.T) {
  505. _, path := serve(t, basicRegistry())
  506. res, err := (&Client{Path: path}).Invoke("echo", nil, []string{"hi"}, "")
  507. require.NoError(t, err)
  508. var s string
  509. require.NoError(t, json.Unmarshal(res.Result, &s))
  510. assert.Equal(t, "[hi]", s)
  511. }
  512. func TestDescribeListsTheCommands(t *testing.T) {
  513. _, path := serve(t, basicRegistry())
  514. schema, err := (&Client{Path: path}).Describe()
  515. require.NoError(t, err)
  516. names := make([]string, 0, len(schema))
  517. for _, c := range schema {
  518. names = append(names, string(c.Name))
  519. }
  520. assert.ElementsMatch(t, []string{"help", "echo", "boom"}, names)
  521. }
  522. // TestPresentationParametersAreRefused: the schema lists json and pretty on every
  523. // command, because the command lines take them, but a request has one way to choose its
  524. // rendering, the format field. A client that sends them as parameters is pointed there
  525. // rather than given a second, weaker way to say the same thing.
  526. func TestPresentationParametersAreRefused(t *testing.T) {
  527. reg := commands.New()
  528. reg.Register(&commands.Command{
  529. Name: "pair",
  530. ShortDescription: "returns a pair",
  531. Run: func(fs any, a []string) (commands.Result, error) {
  532. return commands.Value(map[string]int{"a": 1}), nil
  533. },
  534. Text: func(v any, w io.Writer) error {
  535. _, err := fmt.Fprintln(w, "a is one")
  536. return err
  537. },
  538. })
  539. _, path := serve(t, reg)
  540. c := &Client{Path: path}
  541. for _, name := range []string{"json", "pretty"} {
  542. res, err := c.Invoke("pair", map[string]string{name: "true"}, nil, "text")
  543. require.NoError(t, err)
  544. assert.Contains(t, res.Error, name+" is a command line flag, not a parameter")
  545. assert.Contains(t, res.Error, "format")
  546. assert.Empty(t, res.Result, "a refused request runs nothing")
  547. }
  548. // The format field is the one that counts.
  549. res, err := c.Invoke("pair", nil, nil, "text")
  550. require.NoError(t, err)
  551. require.Empty(t, res.Error)
  552. assert.Equal(t, "a is one\n", textOf(res))
  553. res, err = c.Invoke("pair", nil, nil, "")
  554. require.NoError(t, err)
  555. require.Empty(t, res.Error)
  556. assert.JSONEq(t, `{"a":1}`, string(res.Result), "the API default is JSON")
  557. }
  558. // TestTextRequestToATextlessCommand: a command with no human rendering still answers a
  559. // text request when the answer is a refusal, since a refusal always has one. Only a
  560. // successful result has nothing to say in text, and that is reported then, by the daemon.
  561. func TestTextRequestToATextlessCommand(t *testing.T) {
  562. reg := commands.New()
  563. reg.Register(&commands.Command{
  564. Name: "tunnel",
  565. ShortDescription: "data on success, a refusal without an operand",
  566. Run: func(fs any, a []string) (commands.Result, error) {
  567. if len(a) == 0 {
  568. return commands.Failed("No vpn address was provided"), nil
  569. }
  570. return commands.Value(map[string]int{"index": 7}), nil
  571. },
  572. JSONOnly: true,
  573. })
  574. _, path := serve(t, reg)
  575. c := &Client{Path: path}
  576. res, err := c.Invoke("tunnel", nil, nil, "text")
  577. require.NoError(t, err)
  578. assert.Equal(t, "No vpn address was provided\n", res.Error, "the refusal is rendered, not the missing text form")
  579. res, err = c.Invoke("tunnel", nil, []string{"10.0.0.1"}, "text")
  580. require.NoError(t, err)
  581. assert.Contains(t, res.Error, "tunnel has no human readable output yet; pass -json or -pretty")
  582. assert.Empty(t, res.Result)
  583. res, err = c.Invoke("tunnel", nil, []string{"10.0.0.1"}, "json")
  584. require.NoError(t, err)
  585. require.Empty(t, res.Error)
  586. assert.JSONEq(t, `{"index":7}`, string(res.Result))
  587. // A refusal asked for as JSON carries the outcome for a program and the reason in
  588. // Error, so a client exits non-zero on the envelope alone, never by sniffing the
  589. // payload for an ok field some unrelated result might also have.
  590. res, err = c.Invoke("tunnel", nil, nil, "json")
  591. require.NoError(t, err)
  592. assert.Equal(t, "No vpn address was provided", res.Error)
  593. assert.JSONEq(t, `{"ok":false,"reason":"No vpn address was provided"}`, string(res.Result))
  594. }
  595. // TestRebindAfterFailedBindStillStopsOnCancel covers a daemon whose initial listen
  596. // failed and which later Rebinds onto a good path from a reloaded config. The context
  597. // watcher has to be installed by that Rebind, or the socket outlives the daemon.
  598. func TestRebindAfterFailedBindStillStopsOnCancel(t *testing.T) {
  599. ctx, cancel := context.WithCancel(context.Background())
  600. defer cancel()
  601. base := test.ShortTempDir(t)
  602. // listen creates one level only, so a socket directory under a parent that is not
  603. // there yet fails to bind, the shape of a runtime directory that does not exist.
  604. s := NewServer(test.NewLogger(), basicRegistry())
  605. require.Error(t, s.Rebind(ctx, filepath.Join(base, "missing", "n", "s.sock"), ownerOnly()))
  606. good := filepath.Join(base, "n", "s.sock")
  607. require.NoError(t, s.Rebind(ctx, good, ownerOnly()))
  608. t.Cleanup(s.Stop)
  609. _, err := (&Client{Path: good}).Describe()
  610. require.NoError(t, err)
  611. cancel()
  612. assert.Eventually(t, func() bool {
  613. _, err := os.Stat(good)
  614. return os.IsNotExist(err)
  615. }, 2*time.Second, 20*time.Millisecond)
  616. }
  617. // TestCommandRegisteredAfterStartIsServed: the server filters the registry per request,
  618. // so a command an embedder registers after wiring the socket is served, and described,
  619. // just as it would be over ssh.
  620. func TestCommandRegisteredAfterStartIsServed(t *testing.T) {
  621. reg := basicRegistry()
  622. _, path := serve(t, reg)
  623. reg.Register(&commands.Command{
  624. Name: "late",
  625. ShortDescription: "registered after the server started",
  626. Run: func(fs any, a []string) (commands.Result, error) {
  627. return commands.Value("late"), nil
  628. },
  629. })
  630. c := &Client{Path: path}
  631. schema, err := c.Describe()
  632. require.NoError(t, err)
  633. names := make([]string, 0, len(schema))
  634. for _, cs := range schema {
  635. names = append(names, string(cs.Name))
  636. }
  637. assert.Contains(t, names, "late")
  638. res, err := c.Invoke("late", nil, nil, "")
  639. require.NoError(t, err)
  640. require.Empty(t, res.Error)
  641. assert.JSONEq(t, `"late"`, string(res.Result))
  642. }
  643. // TestThenIsAnsweredInTheOneReply: this surface answers in one piece, so a command that
  644. // acts does so before the reply, and the reply carries Run's line and Then's outcome in
  645. // text, and the outcome alone in JSON.
  646. func TestThenIsAnsweredInTheOneReply(t *testing.T) {
  647. reg := commands.New()
  648. acted := false
  649. reg.Register(&commands.Command{
  650. Name: "act",
  651. ShortDescription: "replies, then acts",
  652. Run: func(fs any, a []string) (commands.Result, error) {
  653. return commands.Did("about to act"), nil
  654. },
  655. Then: func() commands.Result {
  656. acted = true
  657. return commands.Did("acted")
  658. },
  659. })
  660. _, path := serve(t, reg)
  661. res, err := (&Client{Path: path}).Invoke("act", nil, nil, "text")
  662. require.NoError(t, err)
  663. assert.True(t, acted)
  664. assert.Empty(t, res.Error)
  665. assert.Equal(t, "about to act\nacted\n", string(res.Text))
  666. res, err = (&Client{Path: path}).Invoke("act", nil, nil, "")
  667. require.NoError(t, err)
  668. assert.JSONEq(t, `{"ok":true,"reason":"acted"}`, string(res.Result))
  669. // A Run that refused is the outcome, and Then does not run.
  670. acted = false
  671. reg.Register(&commands.Command{
  672. Name: "refuse",
  673. ShortDescription: "refuses, so never acts",
  674. Run: func(fs any, a []string) (commands.Result, error) {
  675. return commands.Failed("not like that"), nil
  676. },
  677. Then: func() commands.Result {
  678. acted = true
  679. return commands.Did("acted anyway")
  680. },
  681. })
  682. res, err = (&Client{Path: path}).Invoke("refuse", nil, nil, "text")
  683. require.NoError(t, err)
  684. assert.Equal(t, "not like that\n", res.Error)
  685. assert.False(t, acted)
  686. }
  687. // TestRebindRefusesDuringShutdown: a reload that lands after the daemon's context is
  688. // cancelled must not bring the endpoint back. Stop is not terminal, so Rebind has to
  689. // take the answer from the context rather than from the running flag.
  690. func TestRebindRefusesDuringShutdown(t *testing.T) {
  691. ctx, cancel := context.WithCancel(context.Background())
  692. defer cancel()
  693. // Built by hand rather than through serve: the context watcher is installed once,
  694. // by the first bind, and this test needs it on a context it can cancel.
  695. s := NewServer(test.NewLogger(), basicRegistry())
  696. require.NoError(t, s.Rebind(ctx, testSocket(t), ownerOnly()))
  697. t.Cleanup(s.Stop)
  698. cancel()
  699. assert.Eventually(t, func() bool { return s.Path() == "" }, 2*time.Second, 20*time.Millisecond, "the watcher stops the server")
  700. next := testSocket(t)
  701. err := s.Rebind(ctx, next, ownerOnly())
  702. require.Error(t, err)
  703. require.ErrorContains(t, err, "shutting down")
  704. assert.Empty(t, s.Path(), "the server stays stopped")
  705. _, statErr := os.Stat(next)
  706. assert.True(t, os.IsNotExist(statErr), "and the refused socket is not left on disk")
  707. }
  708. // TestRebindAndStopSerialize hammers Rebind against Stop. Whichever runs last decides
  709. // the final state, so after the dust settles a Stop must leave nothing listening and no
  710. // socket file behind, on either path.
  711. func TestRebindAndStopSerialize(t *testing.T) {
  712. a := testSocket(t)
  713. b := testSocket(t)
  714. for i := 0; i < 50; i++ {
  715. s := NewServer(test.NewLogger(), basicRegistry())
  716. require.NoError(t, s.Rebind(context.Background(), a, ownerOnly()))
  717. var wg sync.WaitGroup
  718. wg.Add(2)
  719. go func() {
  720. defer wg.Done()
  721. _ = s.Rebind(context.Background(), b, ownerOnly())
  722. }()
  723. go func() {
  724. defer wg.Done()
  725. s.Stop()
  726. }()
  727. wg.Wait()
  728. // The Rebind may have run after the Stop, which restarts the server by design.
  729. // A final Stop then has to take everything down.
  730. s.Stop()
  731. assert.Empty(t, s.Path())
  732. for _, p := range []string{a, b} {
  733. _, err := os.Stat(p)
  734. assert.True(t, os.IsNotExist(err), "iteration %d: %s left behind", i, p)
  735. }
  736. }
  737. }
  738. // TestUnknownRequestFieldIsRefused: a request with a field the protocol does not define
  739. // is answered with the field named, the way an unknown parameter is, and not run.
  740. func TestUnknownRequestFieldIsRefused(t *testing.T) {
  741. _, path := serve(t, basicRegistry())
  742. conn, err := dial(path, time.Second)
  743. require.NoError(t, err)
  744. defer conn.Close()
  745. require.NoError(t, writeFrame(conn, map[string]any{"version": ProtocolVersion, "command": "echo", "arguments": []string{"x"}}))
  746. var res Response
  747. require.NoError(t, readFrame(conn, MaxResponseBytes, "response", &res, false))
  748. assert.Contains(t, res.Error, `unknown field "arguments"`)
  749. assert.Empty(t, res.Result)
  750. }
  751. // TestTextOutputKeepsTheBytes: a text rendering travels as the bytes the command wrote.
  752. // A JSON string would have had to be UTF-8, and a byte that is not would have come out
  753. // as U+FFFD, where the ssh server passes it through.
  754. func TestTextOutputKeepsTheBytes(t *testing.T) {
  755. reg := commands.New()
  756. raw := []byte{'n', 'a', 'm', 'e', ' ', 0xff, 0xfe, '\n'}
  757. reg.Register(&commands.Command{
  758. Name: "raw",
  759. ShortDescription: "writes what a certificate held",
  760. Run: func(fs any, a []string) (commands.Result, error) { return commands.Value("raw"), nil },
  761. Text: func(v any, w io.Writer) error {
  762. _, err := w.Write(raw)
  763. return err
  764. },
  765. })
  766. _, path := serve(t, reg)
  767. res, err := (&Client{Path: path}).Invoke("raw", nil, nil, "text")
  768. require.NoError(t, err)
  769. assert.Empty(t, res.Error)
  770. assert.Equal(t, raw, res.Text)
  771. assert.Empty(t, res.Result, "text is not also carried as JSON")
  772. }