client.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package localapi
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net"
  9. "time"
  10. "github.com/slackhq/nebula/commands"
  11. )
  12. // DialTimeout bounds the connect to a socket that exists but is not being served.
  13. const DialTimeout = 5 * time.Second
  14. // ResponseTimeout bounds the exchange once connected: sending the request and waiting
  15. // for the daemon's answer. The daemon drops a client that is slow to send in 10s and one
  16. // that is slow to read in 30s, so this is the other side of the same bargain, with room
  17. // for the slowest command (a hostmap dump on a large network) before a stalled daemon is
  18. // given up on. Client.Timeout overrides it.
  19. const ResponseTimeout = 60 * time.Second
  20. // MaxResponseBytes bounds what a client will read back. A response carries one command's
  21. // output, and a hostmap dump on a large network is the realistic worst case.
  22. const MaxResponseBytes = 64 << 20
  23. // Client talks to a nebula listening at Path.
  24. type Client struct {
  25. Path string
  26. // Timeout bounds the request and response once connected. Zero means
  27. // ResponseTimeout.
  28. Timeout time.Duration
  29. }
  30. // DaemonError is a refusal the daemon answered with: the connection worked, the request
  31. // was read, and the daemon said no. It is told apart from a failure to reach the daemon
  32. // so that a client does not send its user after a socket path, or a sudo, when the daemon
  33. // was right there and answered. A protocol version the daemon does not speak, a socket at
  34. // its connection limit and a peer the daemon will not serve all arrive as one of these.
  35. type DaemonError struct {
  36. Msg string
  37. }
  38. func (e *DaemonError) Error() string { return e.Msg }
  39. // Describe asks the daemon which commands it serves and what parameters they take, so a
  40. // client can offer them without having been built against this daemon.
  41. func (c *Client) Describe() ([]commands.CommandSchema, error) {
  42. res, err := c.do(&Request{Version: ProtocolVersion, Describe: true})
  43. if err != nil {
  44. return nil, err
  45. }
  46. if res.Error != "" {
  47. return nil, &DaemonError{Msg: res.Error}
  48. }
  49. schema := []commands.CommandSchema{}
  50. if err := json.Unmarshal(res.Result, &schema); err != nil {
  51. return nil, fmt.Errorf("could not parse the command schema: %w", err)
  52. }
  53. return schema, nil
  54. }
  55. // Invoke runs one command and returns what the daemon said. params are the command's
  56. // options by name, as the strings a command line parsed them from; a program speaking
  57. // the protocol directly may send JSON's own types instead, see Request.Params. An empty
  58. // format asks for JSON, which is the API's default; "text" asks the daemon to apply the
  59. // command's own human rendering.
  60. func (c *Client) Invoke(command string, params map[string]string, args []string, format string) (*Response, error) {
  61. var wire map[string]any
  62. if len(params) > 0 {
  63. wire = make(map[string]any, len(params))
  64. for k, v := range params {
  65. wire[k] = v
  66. }
  67. }
  68. return c.do(&Request{
  69. Version: ProtocolVersion,
  70. Command: command,
  71. Params: wire,
  72. Args: args,
  73. Format: format,
  74. })
  75. }
  76. // do sends one request and reads the one response.
  77. //
  78. // Nothing is checked about the socket before the connect: its mode is the kernel's to
  79. // enforce, and a socket that is not ours to connect to is refused there with a
  80. // permission error, which the caller turns into advice.
  81. func (c *Client) do(req *Request) (*Response, error) {
  82. conn, err := dial(c.Path, DialTimeout)
  83. if err != nil {
  84. return nil, err
  85. }
  86. defer conn.Close()
  87. // Without this a daemon that accepts and then stalls, blocked inside a command or
  88. // stopped with SIGSTOP, hangs the client for good, and a script wrapping it never
  89. // completes. One deadline covers the write and the read: the answer is due within
  90. // the budget, however the time is spent.
  91. timeout := c.Timeout
  92. if timeout <= 0 {
  93. timeout = ResponseTimeout
  94. }
  95. _ = conn.SetDeadline(time.Now().Add(timeout))
  96. // If we get a write error, save that fact but then look for a response from the server.
  97. // Some error conditions manifest as the server accepting our connection, writing an
  98. // error, and then closing the connection. Our write races against that close.
  99. writeErr := error(nil)
  100. if err := writeFrame(conn, req); err != nil {
  101. writeErr = fmt.Errorf("could not send request: %w", err)
  102. }
  103. res, err := readResponse(conn)
  104. if err != nil {
  105. if writeErr != nil {
  106. return nil, writeErr
  107. }
  108. var ne net.Error
  109. if errors.As(err, &ne) && ne.Timeout() {
  110. return nil, fmt.Errorf("the daemon did not answer within %s", timeout)
  111. }
  112. return nil, err
  113. }
  114. return res, nil
  115. }
  116. func readResponse(conn net.Conn) (*Response, error) {
  117. res := &Response{}
  118. if err := readFrame(conn, MaxResponseBytes, "response", res, false); err != nil {
  119. if errors.Is(err, io.EOF) {
  120. return nil, errors.New("the daemon closed the connection without answering")
  121. }
  122. return nil, err
  123. }
  124. return res, nil
  125. }
  126. // Pretty renders a JSON Result indented for a human reader, reporting false when the
  127. // result is empty or is not JSON and there is nothing to indent. A value that is a JSON
  128. // string has nothing to indent either, and comes back as it is, quotes and all: the
  129. // caller asked for JSON and that is what the value is.
  130. //
  131. // The bytes are re-indented rather than decoded and re-encoded: decoding into `any`
  132. // sorts object keys and routes every number through float64, so the layout would
  133. // differ from what the daemon wrote and from the ssh -pretty path.
  134. func Pretty(raw json.RawMessage) (string, bool) {
  135. trimmed := bytes.TrimSpace(raw)
  136. if len(trimmed) == 0 {
  137. return "", false
  138. }
  139. var buf bytes.Buffer
  140. if err := json.Indent(&buf, trimmed, "", " "); err != nil {
  141. return "", false
  142. }
  143. return buf.String(), true
  144. }