1
0

server_test.go 30 KB

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