1
0

client.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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. // ReplyError is an answer the client could not read: the daemon was reached and wrote
  31. // something back, but not a response this client understands. The daemon is not
  32. // nebula, or not a version this client speaks to.
  33. type ReplyError struct {
  34. Err error
  35. }
  36. func (e *ReplyError) Error() string { return "the daemon's reply could not be read: " + e.Err.Error() }
  37. func (e *ReplyError) Unwrap() error { return e.Err }
  38. // DaemonError is a refusal the daemon answered with: the connection worked, the request
  39. // was read, and the daemon said no. It is told apart from a failure to reach the daemon
  40. // so that a client does not send its user after a socket path, or a sudo, when the daemon
  41. // was right there and answered. A protocol version the daemon does not speak, a socket at
  42. // its connection limit and a peer the daemon will not serve all arrive as one of these.
  43. type DaemonError struct {
  44. Msg string
  45. }
  46. func (e *DaemonError) Error() string { return e.Msg }
  47. // Describe asks the daemon which commands it serves and what parameters they take, so a
  48. // client can offer them without having been built against this daemon.
  49. func (c *Client) Describe() ([]commands.CommandSchema, error) {
  50. res, err := c.do(&Request{Version: ProtocolVersion, Describe: true})
  51. if err != nil {
  52. return nil, err
  53. }
  54. if res.Error != "" {
  55. return nil, &DaemonError{Msg: res.Error}
  56. }
  57. schema := []commands.CommandSchema{}
  58. if err := json.Unmarshal(res.Result, &schema); err != nil {
  59. return nil, &ReplyError{Err: fmt.Errorf("could not parse the command schema: %w", err)}
  60. }
  61. return schema, nil
  62. }
  63. // Invoke runs one command and returns what the daemon said. params are the command's
  64. // options by name, as the strings a command line parsed them from; a program speaking
  65. // the protocol directly may send JSON's own types instead, see Request.Params. An empty
  66. // format asks for JSON, which is the API's default; "text" asks the daemon to apply the
  67. // command's own human rendering.
  68. func (c *Client) Invoke(command string, params map[string]string, args []string, format string) (*Response, error) {
  69. var wire map[string]any
  70. if len(params) > 0 {
  71. wire = make(map[string]any, len(params))
  72. for k, v := range params {
  73. wire[k] = v
  74. }
  75. }
  76. return c.do(&Request{
  77. Version: ProtocolVersion,
  78. Command: command,
  79. Params: wire,
  80. Args: args,
  81. Format: format,
  82. })
  83. }
  84. // do sends one request and reads the one response.
  85. //
  86. // Nothing is checked about the socket before the connect: its mode is the kernel's to
  87. // enforce, and a socket that is not ours to connect to is refused there with a
  88. // permission error, which the caller turns into advice.
  89. func (c *Client) do(req *Request) (*Response, error) {
  90. conn, err := dial(c.Path, DialTimeout)
  91. if err != nil {
  92. return nil, err
  93. }
  94. defer conn.Close()
  95. // Without this a daemon that accepts and then stalls, blocked inside a command or
  96. // stopped with SIGSTOP, hangs the client for good, and a script wrapping it never
  97. // completes. One deadline covers the write and the read: the answer is due within
  98. // the budget, however the time is spent.
  99. timeout := c.Timeout
  100. if timeout <= 0 {
  101. timeout = ResponseTimeout
  102. }
  103. _ = conn.SetDeadline(time.Now().Add(timeout))
  104. // If we get a write error, save that fact but then look for a response from the server.
  105. // Some error conditions manifest as the server accepting our connection, writing an
  106. // error, and then closing the connection. Our write races against that close.
  107. writeErr := error(nil)
  108. if err := writeFrame(conn, req); err != nil {
  109. writeErr = fmt.Errorf("could not send request: %w", err)
  110. }
  111. res, err := readResponse(conn)
  112. if err != nil {
  113. // Something answered and it was not a response: the daemon was reached, and
  114. // that is the message even when it hung up before the request was written,
  115. // which a daemon that is not nebula is likely to do.
  116. var fe *frameError
  117. if errors.As(err, &fe) {
  118. return nil, &ReplyError{Err: err}
  119. }
  120. if writeErr != nil {
  121. return nil, writeErr
  122. }
  123. var ne net.Error
  124. if errors.As(err, &ne) && ne.Timeout() {
  125. return nil, fmt.Errorf("the daemon did not answer within %s", timeout)
  126. }
  127. return nil, err
  128. }
  129. return res, nil
  130. }
  131. func readResponse(conn net.Conn) (*Response, error) {
  132. res := &Response{}
  133. if err := readFrame(conn, MaxResponseBytes, "response", res); err != nil {
  134. if errors.Is(err, io.EOF) {
  135. return nil, errors.New("the daemon closed the connection without answering")
  136. }
  137. return nil, err
  138. }
  139. return res, nil
  140. }
  141. // Pretty renders a JSON Result indented for a human reader, reporting false when the
  142. // result is empty or is not JSON and there is nothing to indent. A value that is a JSON
  143. // string has nothing to indent either, and comes back as it is, quotes and all: the
  144. // caller asked for JSON and that is what the value is.
  145. //
  146. // The bytes are re-indented rather than decoded and re-encoded: decoding into `any`
  147. // sorts object keys and routes every number through float64, so the layout would
  148. // differ from what the daemon wrote and from the ssh -pretty path.
  149. func Pretty(raw json.RawMessage) (string, bool) {
  150. trimmed := bytes.TrimSpace(raw)
  151. if len(trimmed) == 0 {
  152. return "", false
  153. }
  154. var buf bytes.Buffer
  155. if err := json.Indent(&buf, trimmed, "", " "); err != nil {
  156. return "", false
  157. }
  158. return buf.String(), true
  159. }
  160. // Text renders the Result of a request that asked for text. The daemon carries a text
  161. // rendering as a JSON string, which is unquoted back into the bytes the command wrote.
  162. // It is the caller's job to know that text was asked for: a JSON result whose value is
  163. // a string looks the same on the wire and must not be unquoted, so the choice is made
  164. // from the request's format, never from the bytes.
  165. func Text(raw json.RawMessage) string {
  166. if len(raw) == 0 {
  167. return ""
  168. }
  169. // Only an actual JSON string is unquoted. Unmarshalling into a string also accepts
  170. // null, which would turn a `null` answer into no output at all.
  171. if raw[0] == '"' {
  172. var s string
  173. if err := json.Unmarshal(raw, &s); err == nil {
  174. return s
  175. }
  176. }
  177. return string(raw)
  178. }