github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/client/client.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package client 21 22 import ( 23 "bytes" 24 "context" 25 "encoding/json" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "net" 30 "net/http" 31 "net/url" 32 "os" 33 "path" 34 "time" 35 36 "github.com/snapcore/snapd/dirs" 37 "github.com/snapcore/snapd/jsonutil" 38 ) 39 40 func unixDialer(socketPath string) func(string, string) (net.Conn, error) { 41 if socketPath == "" { 42 socketPath = dirs.SnapdSocket 43 } 44 return func(_, _ string) (net.Conn, error) { 45 return net.Dial("unix", socketPath) 46 } 47 } 48 49 type doer interface { 50 Do(*http.Request) (*http.Response, error) 51 } 52 53 // Config allows to customize client behavior. 54 type Config struct { 55 // BaseURL contains the base URL where snappy daemon is expected to be. 56 // It can be empty for a default behavior of talking over a unix socket. 57 BaseURL string 58 59 // DisableAuth controls whether the client should send an 60 // Authorization header from reading the auth.json data. 61 DisableAuth bool 62 63 // Interactive controls whether the client runs in interactive mode. 64 // At present, this only affects whether interactive polkit 65 // authorisation is requested. 66 Interactive bool 67 68 // Socket is the path to the unix socket to use 69 Socket string 70 71 // DisableKeepAlive indicates whether the connections should not be kept 72 // alive for later reuse 73 DisableKeepAlive bool 74 75 // User-Agent to sent to the snapd daemon 76 UserAgent string 77 } 78 79 // A Client knows how to talk to the snappy daemon. 80 type Client struct { 81 baseURL url.URL 82 doer doer 83 84 disableAuth bool 85 interactive bool 86 87 maintenance error 88 89 warningCount int 90 warningTimestamp time.Time 91 92 userAgent string 93 } 94 95 // New returns a new instance of Client 96 func New(config *Config) *Client { 97 if config == nil { 98 config = &Config{} 99 } 100 101 // By default talk over an UNIX socket. 102 if config.BaseURL == "" { 103 transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive} 104 return &Client{ 105 baseURL: url.URL{ 106 Scheme: "http", 107 Host: "localhost", 108 }, 109 doer: &http.Client{Transport: transport}, 110 disableAuth: config.DisableAuth, 111 interactive: config.Interactive, 112 userAgent: config.UserAgent, 113 } 114 } 115 116 baseURL, err := url.Parse(config.BaseURL) 117 if err != nil { 118 panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err)) 119 } 120 return &Client{ 121 baseURL: *baseURL, 122 doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, 123 disableAuth: config.DisableAuth, 124 interactive: config.Interactive, 125 userAgent: config.UserAgent, 126 } 127 } 128 129 // Maintenance returns an error reflecting the daemon maintenance status or nil. 130 func (client *Client) Maintenance() error { 131 return client.maintenance 132 } 133 134 // WarningsSummary returns the number of warnings that are ready to be shown to 135 // the user, and the timestamp of the most recently added warning (useful for 136 // silencing the warning alerts, and OKing the returned warnings). 137 func (client *Client) WarningsSummary() (count int, timestamp time.Time) { 138 return client.warningCount, client.warningTimestamp 139 } 140 141 func (client *Client) WhoAmI() (string, error) { 142 user, err := readAuthData() 143 if os.IsNotExist(err) { 144 return "", nil 145 } 146 if err != nil { 147 return "", err 148 } 149 150 return user.Email, nil 151 } 152 153 func (client *Client) setAuthorization(req *http.Request) error { 154 user, err := readAuthData() 155 if os.IsNotExist(err) { 156 return nil 157 } 158 if err != nil { 159 return err 160 } 161 162 var buf bytes.Buffer 163 fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon) 164 for _, discharge := range user.Discharges { 165 fmt.Fprintf(&buf, `, discharge="%s"`, discharge) 166 } 167 req.Header.Set("Authorization", buf.String()) 168 return nil 169 } 170 171 type RequestError struct{ error } 172 173 func (e RequestError) Error() string { 174 return fmt.Sprintf("cannot build request: %v", e.error) 175 } 176 177 type AuthorizationError struct{ error } 178 179 func (e AuthorizationError) Error() string { 180 return fmt.Sprintf("cannot add authorization: %v", e.error) 181 } 182 183 type ConnectionError struct{ Err error } 184 185 func (e ConnectionError) Error() string { 186 var errStr string 187 switch e.Err { 188 case context.DeadlineExceeded: 189 errStr = "timeout exceeded while waiting for response" 190 case context.Canceled: 191 errStr = "request canceled" 192 default: 193 errStr = e.Err.Error() 194 } 195 return fmt.Sprintf("cannot communicate with server: %s", errStr) 196 } 197 198 func (e ConnectionError) Unwrap() error { 199 return e.Err 200 } 201 202 // AllowInteractionHeader is the HTTP request header used to indicate 203 // that the client is willing to allow interaction. 204 const AllowInteractionHeader = "X-Allow-Interaction" 205 206 // raw performs a request and returns the resulting http.Response and 207 // error you usually only need to call this directly if you expect the 208 // response to not be JSON, otherwise you'd call Do(...) instead. 209 func (client *Client) raw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { 210 // fake a url to keep http.Client happy 211 u := client.baseURL 212 u.Path = path.Join(client.baseURL.Path, urlpath) 213 u.RawQuery = query.Encode() 214 req, err := http.NewRequest(method, u.String(), body) 215 if err != nil { 216 return nil, RequestError{err} 217 } 218 if client.userAgent != "" { 219 req.Header.Set("User-Agent", client.userAgent) 220 } 221 222 for key, value := range headers { 223 req.Header.Set(key, value) 224 } 225 226 if !client.disableAuth { 227 // set Authorization header if there are user's credentials 228 err = client.setAuthorization(req) 229 if err != nil { 230 return nil, AuthorizationError{err} 231 } 232 } 233 234 if client.interactive { 235 req.Header.Set(AllowInteractionHeader, "true") 236 } 237 238 if ctx != nil { 239 req = req.WithContext(ctx) 240 } 241 242 rsp, err := client.doer.Do(req) 243 if err != nil { 244 return nil, ConnectionError{err} 245 } 246 247 return rsp, nil 248 } 249 250 // rawWithTimeout is like raw(), but sets a timeout for the whole of request and 251 // response (including rsp.Body() read) round trip. The caller is responsible 252 // for canceling the internal context to release the resources associated with 253 // the request by calling the returned cancel function. 254 func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, timeout time.Duration) (*http.Response, context.CancelFunc, error) { 255 if timeout == 0 { 256 return nil, nil, fmt.Errorf("internal error: timeout not set for rawWithTimeout") 257 } 258 259 ctx, cancel := context.WithTimeout(ctx, timeout) 260 rsp, err := client.raw(ctx, method, urlpath, query, headers, body) 261 if err != nil && ctx.Err() != nil { 262 cancel() 263 return nil, nil, ConnectionError{ctx.Err()} 264 } 265 266 return rsp, cancel, err 267 } 268 269 var ( 270 doRetry = 250 * time.Millisecond 271 // snapd may need to reach out to the store, where it uses a fixed 10s 272 // timeout for the whole of a single request to complete, requests are 273 // retried for up to 38s in total, make sure that the client timeout is 274 // not shorter than that 275 doTimeout = 50 * time.Second 276 ) 277 278 // MockDoTimings mocks the delay used by the do retry loop and request timeout. 279 func MockDoTimings(retry, timeout time.Duration) (restore func()) { 280 oldRetry := doRetry 281 oldTimeout := doTimeout 282 doRetry = retry 283 doTimeout = timeout 284 return func() { 285 doRetry = oldRetry 286 doTimeout = oldTimeout 287 } 288 } 289 290 type hijacked struct { 291 do func(*http.Request) (*http.Response, error) 292 } 293 294 func (h hijacked) Do(req *http.Request) (*http.Response, error) { 295 return h.do(req) 296 } 297 298 // Hijack lets the caller take over the raw http request 299 func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) { 300 client.doer = hijacked{f} 301 } 302 303 type doFlags struct { 304 NoTimeout bool 305 } 306 307 // do performs a request and decodes the resulting json into the given 308 // value. It's low-level, for testing/experimenting only; you should 309 // usually use a higher level interface that builds on this. 310 func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, flags doFlags) (statusCode int, err error) { 311 retry := time.NewTicker(doRetry) 312 defer retry.Stop() 313 timeout := time.NewTimer(doTimeout) 314 defer timeout.Stop() 315 316 var rsp *http.Response 317 var ctx context.Context = context.Background() 318 for { 319 if flags.NoTimeout { 320 rsp, err = client.raw(ctx, method, path, query, headers, body) 321 } else { 322 var cancel context.CancelFunc 323 // use the same timeout as for the whole of the retry 324 // loop to error out the whole do() call when a single 325 // request exceeds the deadline 326 rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, doTimeout) 327 if err == nil { 328 defer cancel() 329 } 330 } 331 if err == nil || method != "GET" { 332 break 333 } 334 select { 335 case <-retry.C: 336 continue 337 case <-timeout.C: 338 } 339 break 340 } 341 if err != nil { 342 return 0, err 343 } 344 defer rsp.Body.Close() 345 346 if v != nil { 347 if err := decodeInto(rsp.Body, v); err != nil { 348 return rsp.StatusCode, err 349 } 350 } 351 352 return rsp.StatusCode, nil 353 } 354 355 func decodeInto(reader io.Reader, v interface{}) error { 356 dec := json.NewDecoder(reader) 357 if err := dec.Decode(v); err != nil { 358 r := dec.Buffered() 359 buf, err1 := ioutil.ReadAll(r) 360 if err1 != nil { 361 buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) 362 } 363 return fmt.Errorf("cannot decode %q: %s", buf, err) 364 } 365 return nil 366 } 367 368 // doSync performs a request to the given path using the specified HTTP method. 369 // It expects a "sync" response from the API and on success decodes the JSON 370 // response payload into the given value using the "UseNumber" json decoding 371 // which produces json.Numbers instead of float64 types for numbers. 372 func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { 373 var rsp response 374 statusCode, err := client.do(method, path, query, headers, body, &rsp, doFlags{}) 375 if err != nil { 376 return nil, err 377 } 378 if err := rsp.err(client, statusCode); err != nil { 379 return nil, err 380 } 381 if rsp.Type != "sync" { 382 return nil, fmt.Errorf("expected sync response, got %q", rsp.Type) 383 } 384 385 if v != nil { 386 if err := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil { 387 return nil, fmt.Errorf("cannot unmarshal: %v", err) 388 } 389 } 390 391 client.warningCount = rsp.WarningCount 392 client.warningTimestamp = rsp.WarningTimestamp 393 394 return &rsp.ResultInfo, nil 395 } 396 397 func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { 398 _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{}) 399 return 400 } 401 402 func (client *Client) doAsyncNoTimeout(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { 403 _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{NoTimeout: true}) 404 return changeID, err 405 } 406 407 func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, flags doFlags) (result json.RawMessage, changeID string, err error) { 408 var rsp response 409 statusCode, err := client.do(method, path, query, headers, body, &rsp, flags) 410 if err != nil { 411 return nil, "", err 412 } 413 if err := rsp.err(client, statusCode); err != nil { 414 return nil, "", err 415 } 416 if rsp.Type != "async" { 417 return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) 418 } 419 if statusCode != 202 { 420 return nil, "", fmt.Errorf("operation not accepted") 421 } 422 if rsp.Change == "" { 423 return nil, "", fmt.Errorf("async response without change reference") 424 } 425 426 return rsp.Result, rsp.Change, nil 427 } 428 429 type ServerVersion struct { 430 Version string 431 Series string 432 OSID string 433 OSVersionID string 434 OnClassic bool 435 436 KernelVersion string 437 Architecture string 438 Virtualization string 439 } 440 441 func (client *Client) ServerVersion() (*ServerVersion, error) { 442 sysInfo, err := client.SysInfo() 443 if err != nil { 444 return nil, err 445 } 446 447 return &ServerVersion{ 448 Version: sysInfo.Version, 449 Series: sysInfo.Series, 450 OSID: sysInfo.OSRelease.ID, 451 OSVersionID: sysInfo.OSRelease.VersionID, 452 OnClassic: sysInfo.OnClassic, 453 454 KernelVersion: sysInfo.KernelVersion, 455 Architecture: sysInfo.Architecture, 456 Virtualization: sysInfo.Virtualization, 457 }, nil 458 } 459 460 // A response produced by the REST API will usually fit in this 461 // (exceptions are the icons/ endpoints obvs) 462 type response struct { 463 Result json.RawMessage `json:"result"` 464 Type string `json:"type"` 465 Change string `json:"change"` 466 467 WarningCount int `json:"warning-count"` 468 WarningTimestamp time.Time `json:"warning-timestamp"` 469 470 ResultInfo 471 472 Maintenance *Error `json:"maintenance"` 473 } 474 475 // Error is the real value of response.Result when an error occurs. 476 type Error struct { 477 Kind ErrorKind `json:"kind"` 478 Value interface{} `json:"value"` 479 Message string `json:"message"` 480 481 StatusCode int 482 } 483 484 func (e *Error) Error() string { 485 return e.Message 486 } 487 488 // IsRetryable returns true if the given error is an error 489 // that can be retried later. 490 func IsRetryable(err error) bool { 491 switch e := err.(type) { 492 case *Error: 493 return e.Kind == ErrorKindSnapChangeConflict 494 } 495 return false 496 } 497 498 // IsTwoFactorError returns whether the given error is due to problems 499 // in two-factor authentication. 500 func IsTwoFactorError(err error) bool { 501 e, ok := err.(*Error) 502 if !ok || e == nil { 503 return false 504 } 505 506 return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired 507 } 508 509 // IsInterfacesUnchangedError returns whether the given error means the requested 510 // change to interfaces was not made, because there was nothing to do. 511 func IsInterfacesUnchangedError(err error) bool { 512 e, ok := err.(*Error) 513 if !ok || e == nil { 514 return false 515 } 516 return e.Kind == ErrorKindInterfacesUnchanged 517 } 518 519 // IsAssertionNotFoundError returns whether the given error means that the 520 // assertion wasn't found and thus the device isn't ready/seeded. 521 func IsAssertionNotFoundError(err error) bool { 522 e, ok := err.(*Error) 523 if !ok || e == nil { 524 return false 525 } 526 527 return e.Kind == ErrorKindAssertionNotFound 528 } 529 530 // OSRelease contains information about the system extracted from /etc/os-release. 531 type OSRelease struct { 532 ID string `json:"id"` 533 VersionID string `json:"version-id,omitempty"` 534 } 535 536 // RefreshInfo contains information about refreshes. 537 type RefreshInfo struct { 538 // Timer contains the refresh.timer setting. 539 Timer string `json:"timer,omitempty"` 540 // Schedule contains the legacy refresh.schedule setting. 541 Schedule string `json:"schedule,omitempty"` 542 Last string `json:"last,omitempty"` 543 Hold string `json:"hold,omitempty"` 544 Next string `json:"next,omitempty"` 545 } 546 547 // SysInfo holds system information 548 type SysInfo struct { 549 Series string `json:"series,omitempty"` 550 Version string `json:"version,omitempty"` 551 BuildID string `json:"build-id"` 552 OSRelease OSRelease `json:"os-release"` 553 OnClassic bool `json:"on-classic"` 554 Managed bool `json:"managed"` 555 556 KernelVersion string `json:"kernel-version,omitempty"` 557 Architecture string `json:"architecture,omitempty"` 558 Virtualization string `json:"virtualization,omitempty"` 559 560 Refresh RefreshInfo `json:"refresh,omitempty"` 561 Confinement string `json:"confinement"` 562 SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"` 563 } 564 565 func (rsp *response) err(cli *Client, statusCode int) error { 566 if cli != nil { 567 maintErr := rsp.Maintenance 568 // avoid setting to (*client.Error)(nil) 569 if maintErr != nil { 570 cli.maintenance = maintErr 571 } else { 572 cli.maintenance = nil 573 } 574 } 575 if rsp.Type != "error" { 576 return nil 577 } 578 var resultErr Error 579 err := json.Unmarshal(rsp.Result, &resultErr) 580 if err != nil || resultErr.Message == "" { 581 return fmt.Errorf("server error: %q", http.StatusText(statusCode)) 582 } 583 resultErr.StatusCode = statusCode 584 585 return &resultErr 586 } 587 588 func parseError(r *http.Response) error { 589 var rsp response 590 if r.Header.Get("Content-Type") != "application/json" { 591 return fmt.Errorf("server error: %q", r.Status) 592 } 593 594 dec := json.NewDecoder(r.Body) 595 if err := dec.Decode(&rsp); err != nil { 596 return fmt.Errorf("cannot unmarshal error: %v", err) 597 } 598 599 err := rsp.err(nil, r.StatusCode) 600 if err == nil { 601 return fmt.Errorf("server error: %q", r.Status) 602 } 603 return err 604 } 605 606 // SysInfo gets system information from the REST API. 607 func (client *Client) SysInfo() (*SysInfo, error) { 608 var sysInfo SysInfo 609 610 if _, err := client.doSync("GET", "/v2/system-info", nil, nil, nil, &sysInfo); err != nil { 611 return nil, fmt.Errorf("cannot obtain system details: %v", err) 612 } 613 614 return &sysInfo, nil 615 } 616 617 type debugAction struct { 618 Action string `json:"action"` 619 Params interface{} `json:"params,omitempty"` 620 } 621 622 // Debug is only useful when writing test code, it will trigger 623 // an internal action with the given parameters. 624 func (client *Client) Debug(action string, params interface{}, result interface{}) error { 625 body, err := json.Marshal(debugAction{ 626 Action: action, 627 Params: params, 628 }) 629 if err != nil { 630 return err 631 } 632 633 _, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result) 634 return err 635 } 636 637 func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error { 638 urlParams := url.Values{"aspect": []string{aspect}} 639 for k, v := range params { 640 urlParams.Set(k, v) 641 } 642 _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result) 643 return err 644 }