github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/client/client.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2020 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 "strconv" 35 "time" 36 37 "github.com/snapcore/snapd/dirs" 38 "github.com/snapcore/snapd/jsonutil" 39 ) 40 41 func unixDialer(socketPath string) func(string, string) (net.Conn, error) { 42 if socketPath == "" { 43 socketPath = dirs.SnapdSocket 44 } 45 return func(_, _ string) (net.Conn, error) { 46 return net.Dial("unix", socketPath) 47 } 48 } 49 50 type doer interface { 51 Do(*http.Request) (*http.Response, error) 52 } 53 54 // Config allows to customize client behavior. 55 type Config struct { 56 // BaseURL contains the base URL where snappy daemon is expected to be. 57 // It can be empty for a default behavior of talking over a unix socket. 58 BaseURL string 59 60 // DisableAuth controls whether the client should send an 61 // Authorization header from reading the auth.json data. 62 DisableAuth bool 63 64 // Interactive controls whether the client runs in interactive mode. 65 // At present, this only affects whether interactive polkit 66 // authorisation is requested. 67 Interactive bool 68 69 // Socket is the path to the unix socket to use 70 Socket string 71 72 // DisableKeepAlive indicates whether the connections should not be kept 73 // alive for later reuse 74 DisableKeepAlive bool 75 76 // User-Agent to sent to the snapd daemon 77 UserAgent string 78 } 79 80 // A Client knows how to talk to the snappy daemon. 81 type Client struct { 82 baseURL url.URL 83 doer doer 84 85 disableAuth bool 86 interactive bool 87 88 maintenance error 89 90 warningCount int 91 warningTimestamp time.Time 92 93 userAgent string 94 } 95 96 // New returns a new instance of Client 97 func New(config *Config) *Client { 98 if config == nil { 99 config = &Config{} 100 } 101 102 // By default talk over an UNIX socket. 103 if config.BaseURL == "" { 104 transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive} 105 return &Client{ 106 baseURL: url.URL{ 107 Scheme: "http", 108 Host: "localhost", 109 }, 110 doer: &http.Client{Transport: transport}, 111 disableAuth: config.DisableAuth, 112 interactive: config.Interactive, 113 userAgent: config.UserAgent, 114 } 115 } 116 117 baseURL, err := url.Parse(config.BaseURL) 118 if err != nil { 119 panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err)) 120 } 121 return &Client{ 122 baseURL: *baseURL, 123 doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, 124 disableAuth: config.DisableAuth, 125 interactive: config.Interactive, 126 userAgent: config.UserAgent, 127 } 128 } 129 130 // Maintenance returns an error reflecting the daemon maintenance status or nil. 131 func (client *Client) Maintenance() error { 132 return client.maintenance 133 } 134 135 // WarningsSummary returns the number of warnings that are ready to be shown to 136 // the user, and the timestamp of the most recently added warning (useful for 137 // silencing the warning alerts, and OKing the returned warnings). 138 func (client *Client) WarningsSummary() (count int, timestamp time.Time) { 139 return client.warningCount, client.warningTimestamp 140 } 141 142 func (client *Client) WhoAmI() (string, error) { 143 user, err := readAuthData() 144 if os.IsNotExist(err) { 145 return "", nil 146 } 147 if err != nil { 148 return "", err 149 } 150 151 return user.Email, nil 152 } 153 154 func (client *Client) setAuthorization(req *http.Request) error { 155 user, err := readAuthData() 156 if os.IsNotExist(err) { 157 return nil 158 } 159 if err != nil { 160 return err 161 } 162 163 var buf bytes.Buffer 164 fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon) 165 for _, discharge := range user.Discharges { 166 fmt.Fprintf(&buf, `, discharge="%s"`, discharge) 167 } 168 req.Header.Set("Authorization", buf.String()) 169 return nil 170 } 171 172 type RequestError struct{ error } 173 174 func (e RequestError) Error() string { 175 return fmt.Sprintf("cannot build request: %v", e.error) 176 } 177 178 type AuthorizationError struct{ error } 179 180 func (e AuthorizationError) Error() string { 181 return fmt.Sprintf("cannot add authorization: %v", e.error) 182 } 183 184 type ConnectionError struct{ Err error } 185 186 func (e ConnectionError) Error() string { 187 var errStr string 188 switch e.Err { 189 case context.DeadlineExceeded: 190 errStr = "timeout exceeded while waiting for response" 191 case context.Canceled: 192 errStr = "request canceled" 193 default: 194 errStr = e.Err.Error() 195 } 196 return fmt.Sprintf("cannot communicate with server: %s", errStr) 197 } 198 199 func (e ConnectionError) Unwrap() error { 200 return e.Err 201 } 202 203 // AllowInteractionHeader is the HTTP request header used to indicate 204 // that the client is willing to allow interaction. 205 const AllowInteractionHeader = "X-Allow-Interaction" 206 207 // raw performs a request and returns the resulting http.Response and 208 // error. You usually only need to call this directly if you expect the 209 // response to not be JSON, otherwise you'd call Do(...) instead. 210 func (client *Client) raw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { 211 // fake a url to keep http.Client happy 212 u := client.baseURL 213 u.Path = path.Join(client.baseURL.Path, urlpath) 214 u.RawQuery = query.Encode() 215 req, err := http.NewRequest(method, u.String(), body) 216 if err != nil { 217 return nil, RequestError{err} 218 } 219 if client.userAgent != "" { 220 req.Header.Set("User-Agent", client.userAgent) 221 } 222 223 for key, value := range headers { 224 req.Header.Set(key, value) 225 } 226 // Content-length headers are special and need to be set 227 // directly to the request. Just setting it to the header 228 // will be ignored by go http. 229 if clStr := req.Header.Get("Content-Length"); clStr != "" { 230 cl, err := strconv.ParseInt(clStr, 10, 64) 231 if err != nil { 232 return nil, err 233 } 234 req.ContentLength = cl 235 } 236 237 if !client.disableAuth { 238 // set Authorization header if there are user's credentials 239 err = client.setAuthorization(req) 240 if err != nil { 241 return nil, AuthorizationError{err} 242 } 243 } 244 245 if client.interactive { 246 req.Header.Set(AllowInteractionHeader, "true") 247 } 248 249 if ctx != nil { 250 req = req.WithContext(ctx) 251 } 252 253 rsp, err := client.doer.Do(req) 254 if err != nil { 255 return nil, ConnectionError{err} 256 } 257 258 return rsp, nil 259 } 260 261 // rawWithTimeout is like raw(), but sets a timeout based on opts for 262 // the whole of request and response (including rsp.Body() read) round 263 // trip. If opts is nil the default doTimeout is used. 264 // The caller is responsible for canceling the internal context 265 // to release the resources associated with the request by calling the 266 // returned cancel function. 267 func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (*http.Response, context.CancelFunc, error) { 268 opts = ensureDoOpts(opts) 269 if opts.Timeout <= 0 { 270 return nil, nil, fmt.Errorf("internal error: timeout not set in options for rawWithTimeout") 271 } 272 273 ctx, cancel := context.WithTimeout(ctx, opts.Timeout) 274 rsp, err := client.raw(ctx, method, urlpath, query, headers, body) 275 if err != nil && ctx.Err() != nil { 276 cancel() 277 return nil, nil, ConnectionError{ctx.Err()} 278 } 279 280 return rsp, cancel, err 281 } 282 283 var ( 284 doRetry = 250 * time.Millisecond 285 // snapd may need to reach out to the store, where it uses a fixed 10s 286 // timeout for the whole of a single request to complete, requests are 287 // retried for up to 38s in total, make sure that the client timeout is 288 // not shorter than that 289 doTimeout = 120 * time.Second 290 ) 291 292 // MockDoTimings mocks the delay used by the do retry loop and request timeout. 293 func MockDoTimings(retry, timeout time.Duration) (restore func()) { 294 oldRetry := doRetry 295 oldTimeout := doTimeout 296 doRetry = retry 297 doTimeout = timeout 298 return func() { 299 doRetry = oldRetry 300 doTimeout = oldTimeout 301 } 302 } 303 304 type hijacked struct { 305 do func(*http.Request) (*http.Response, error) 306 } 307 308 func (h hijacked) Do(req *http.Request) (*http.Response, error) { 309 return h.do(req) 310 } 311 312 // Hijack lets the caller take over the raw http request 313 func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) { 314 client.doer = hijacked{f} 315 } 316 317 type doOptions struct { 318 // Timeout is the overall request timeout 319 Timeout time.Duration 320 // Retry interval. 321 // Note for a request with a Timeout but without a retry, Retry should just 322 // be set to something larger than the Timeout. 323 Retry time.Duration 324 } 325 326 func ensureDoOpts(opts *doOptions) *doOptions { 327 if opts == nil { 328 // defaults 329 opts = &doOptions{ 330 Timeout: doTimeout, 331 Retry: doRetry, 332 } 333 } 334 return opts 335 } 336 337 // doNoTimeoutAndRetry can be passed to the do family to not have timeout 338 // nor retries. 339 var doNoTimeoutAndRetry = &doOptions{ 340 Timeout: time.Duration(-1), 341 } 342 343 // do performs a request and decodes the resulting json into the given 344 // value. It's low-level, for testing/experimenting only; you should 345 // usually use a higher level interface that builds on this. 346 func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (statusCode int, err error) { 347 opts = ensureDoOpts(opts) 348 349 client.checkMaintenanceJSON() 350 351 var rsp *http.Response 352 var ctx context.Context = context.Background() 353 if opts.Timeout <= 0 { 354 // no timeout and retries 355 rsp, err = client.raw(ctx, method, path, query, headers, body) 356 } else { 357 if opts.Retry <= 0 { 358 return 0, fmt.Errorf("internal error: retry setting %s invalid", opts.Retry) 359 } 360 retry := time.NewTicker(opts.Retry) 361 defer retry.Stop() 362 timeout := time.NewTimer(opts.Timeout) 363 defer timeout.Stop() 364 365 for { 366 var cancel context.CancelFunc 367 // use the same timeout as for the whole of the retry 368 // loop to error out the whole do() call when a single 369 // request exceeds the deadline 370 rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, opts) 371 if err == nil { 372 defer cancel() 373 } 374 if err == nil || method != "GET" { 375 break 376 } 377 select { 378 case <-retry.C: 379 continue 380 case <-timeout.C: 381 } 382 break 383 } 384 } 385 if err != nil { 386 return 0, err 387 } 388 defer rsp.Body.Close() 389 390 if v != nil { 391 if err := decodeInto(rsp.Body, v); err != nil { 392 return rsp.StatusCode, err 393 } 394 } 395 396 return rsp.StatusCode, nil 397 } 398 399 func decodeInto(reader io.Reader, v interface{}) error { 400 dec := json.NewDecoder(reader) 401 if err := dec.Decode(v); err != nil { 402 r := dec.Buffered() 403 buf, err1 := ioutil.ReadAll(r) 404 if err1 != nil { 405 buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) 406 } 407 return fmt.Errorf("cannot decode %q: %s", buf, err) 408 } 409 return nil 410 } 411 412 // doSync performs a request to the given path using the specified HTTP method. 413 // It expects a "sync" response from the API and on success decodes the JSON 414 // response payload into the given value using the "UseNumber" json decoding 415 // which produces json.Numbers instead of float64 types for numbers. 416 func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { 417 return client.doSyncWithOpts(method, path, query, headers, body, v, nil) 418 } 419 420 // checkMaintenanceJSON checks if there is a maintenance.json file written by 421 // snapd the daemon that positively identifies snapd as being unavailable due to 422 // maintenance, either for snapd restarting itself to update, or rebooting the 423 // system to update the kernel or base snap, etc. If there is ongoing 424 // maintenance, then the maintenance object on the client is set appropriately. 425 // note that currently checkMaintenanceJSON does not return errors, such that 426 // if the file is missing or corrupt or empty, nothing will happen and it will 427 // be silently ignored 428 func (client *Client) checkMaintenanceJSON() { 429 f, err := os.Open(dirs.SnapdMaintenanceFile) 430 // just continue if we can't read the maintenance file 431 if err != nil { 432 return 433 } 434 defer f.Close() 435 436 // we have a maintenance file, try to read it 437 maintenance := &Error{} 438 439 if err := json.NewDecoder(f).Decode(&maintenance); err != nil { 440 // if the json is malformed, just ignore it for now, we only use it for 441 // positive identification of snapd down for maintenance 442 return 443 } 444 445 if maintenance != nil { 446 switch maintenance.Kind { 447 case ErrorKindDaemonRestart: 448 client.maintenance = maintenance 449 case ErrorKindSystemRestart: 450 client.maintenance = maintenance 451 } 452 // don't set maintenance for other kinds, as we don't know what it 453 // is yet 454 455 // this also means an empty json object in maintenance.json doesn't get 456 // treated as a real maintenance downtime for example 457 } 458 } 459 460 func (client *Client) doSyncWithOpts(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (*ResultInfo, error) { 461 // first check maintenance.json to see if snapd is down for a restart, and 462 // set cli.maintenance as appropriate, then perform the request 463 // TODO: it would be a nice thing to skip the request if we know that snapd 464 // won't respond and return a specific error, but that's a big behavior 465 // change we probably shouldn't make right now, not to mention it probably 466 // requires adjustments in other areas too 467 client.checkMaintenanceJSON() 468 469 var rsp response 470 statusCode, err := client.do(method, path, query, headers, body, &rsp, opts) 471 if err != nil { 472 return nil, err 473 } 474 if err := rsp.err(client, statusCode); err != nil { 475 return nil, err 476 } 477 if rsp.Type != "sync" { 478 return nil, fmt.Errorf("expected sync response, got %q", rsp.Type) 479 } 480 481 if v != nil { 482 if err := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil { 483 return nil, fmt.Errorf("cannot unmarshal: %v", err) 484 } 485 } 486 487 client.warningCount = rsp.WarningCount 488 client.warningTimestamp = rsp.WarningTimestamp 489 490 return &rsp.ResultInfo, nil 491 } 492 493 func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { 494 _, changeID, err = client.doAsyncFull(method, path, query, headers, body, nil) 495 return 496 } 497 498 func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (result json.RawMessage, changeID string, err error) { 499 var rsp response 500 statusCode, err := client.do(method, path, query, headers, body, &rsp, opts) 501 if err != nil { 502 return nil, "", err 503 } 504 if err := rsp.err(client, statusCode); err != nil { 505 return nil, "", err 506 } 507 if rsp.Type != "async" { 508 return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) 509 } 510 if statusCode != 202 { 511 return nil, "", fmt.Errorf("operation not accepted") 512 } 513 if rsp.Change == "" { 514 return nil, "", fmt.Errorf("async response without change reference") 515 } 516 517 return rsp.Result, rsp.Change, nil 518 } 519 520 type ServerVersion struct { 521 Version string 522 Series string 523 OSID string 524 OSVersionID string 525 OnClassic bool 526 527 KernelVersion string 528 Architecture string 529 Virtualization string 530 } 531 532 func (client *Client) ServerVersion() (*ServerVersion, error) { 533 sysInfo, err := client.SysInfo() 534 if err != nil { 535 return nil, err 536 } 537 538 return &ServerVersion{ 539 Version: sysInfo.Version, 540 Series: sysInfo.Series, 541 OSID: sysInfo.OSRelease.ID, 542 OSVersionID: sysInfo.OSRelease.VersionID, 543 OnClassic: sysInfo.OnClassic, 544 545 KernelVersion: sysInfo.KernelVersion, 546 Architecture: sysInfo.Architecture, 547 Virtualization: sysInfo.Virtualization, 548 }, nil 549 } 550 551 // A response produced by the REST API will usually fit in this 552 // (exceptions are the icons/ endpoints obvs) 553 type response struct { 554 Result json.RawMessage `json:"result"` 555 Type string `json:"type"` 556 Change string `json:"change"` 557 558 WarningCount int `json:"warning-count"` 559 WarningTimestamp time.Time `json:"warning-timestamp"` 560 561 ResultInfo 562 563 Maintenance *Error `json:"maintenance"` 564 } 565 566 // Error is the real value of response.Result when an error occurs. 567 type Error struct { 568 Kind ErrorKind `json:"kind"` 569 Value interface{} `json:"value"` 570 Message string `json:"message"` 571 572 StatusCode int 573 } 574 575 func (e *Error) Error() string { 576 return e.Message 577 } 578 579 // IsRetryable returns true if the given error is an error 580 // that can be retried later. 581 func IsRetryable(err error) bool { 582 switch e := err.(type) { 583 case *Error: 584 return e.Kind == ErrorKindSnapChangeConflict 585 } 586 return false 587 } 588 589 // IsTwoFactorError returns whether the given error is due to problems 590 // in two-factor authentication. 591 func IsTwoFactorError(err error) bool { 592 e, ok := err.(*Error) 593 if !ok || e == nil { 594 return false 595 } 596 597 return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired 598 } 599 600 // IsInterfacesUnchangedError returns whether the given error means the requested 601 // change to interfaces was not made, because there was nothing to do. 602 func IsInterfacesUnchangedError(err error) bool { 603 e, ok := err.(*Error) 604 if !ok || e == nil { 605 return false 606 } 607 return e.Kind == ErrorKindInterfacesUnchanged 608 } 609 610 // IsAssertionNotFoundError returns whether the given error means that the 611 // assertion wasn't found and thus the device isn't ready/seeded. 612 func IsAssertionNotFoundError(err error) bool { 613 e, ok := err.(*Error) 614 if !ok || e == nil { 615 return false 616 } 617 618 return e.Kind == ErrorKindAssertionNotFound 619 } 620 621 // OSRelease contains information about the system extracted from /etc/os-release. 622 type OSRelease struct { 623 ID string `json:"id"` 624 VersionID string `json:"version-id,omitempty"` 625 } 626 627 // RefreshInfo contains information about refreshes. 628 type RefreshInfo struct { 629 // Timer contains the refresh.timer setting. 630 Timer string `json:"timer,omitempty"` 631 // Schedule contains the legacy refresh.schedule setting. 632 Schedule string `json:"schedule,omitempty"` 633 Last string `json:"last,omitempty"` 634 Hold string `json:"hold,omitempty"` 635 Next string `json:"next,omitempty"` 636 } 637 638 // SysInfo holds system information 639 type SysInfo struct { 640 Series string `json:"series,omitempty"` 641 Version string `json:"version,omitempty"` 642 BuildID string `json:"build-id"` 643 OSRelease OSRelease `json:"os-release"` 644 OnClassic bool `json:"on-classic"` 645 Managed bool `json:"managed"` 646 647 KernelVersion string `json:"kernel-version,omitempty"` 648 Architecture string `json:"architecture,omitempty"` 649 Virtualization string `json:"virtualization,omitempty"` 650 651 Refresh RefreshInfo `json:"refresh,omitempty"` 652 Confinement string `json:"confinement"` 653 SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"` 654 } 655 656 func (rsp *response) err(cli *Client, statusCode int) error { 657 if cli != nil { 658 maintErr := rsp.Maintenance 659 // avoid setting to (*client.Error)(nil) 660 if maintErr != nil { 661 cli.maintenance = maintErr 662 } else { 663 cli.maintenance = nil 664 } 665 } 666 if rsp.Type != "error" { 667 return nil 668 } 669 var resultErr Error 670 err := json.Unmarshal(rsp.Result, &resultErr) 671 if err != nil || resultErr.Message == "" { 672 return fmt.Errorf("server error: %q", http.StatusText(statusCode)) 673 } 674 resultErr.StatusCode = statusCode 675 676 return &resultErr 677 } 678 679 func parseError(r *http.Response) error { 680 var rsp response 681 if r.Header.Get("Content-Type") != "application/json" { 682 return fmt.Errorf("server error: %q", r.Status) 683 } 684 685 dec := json.NewDecoder(r.Body) 686 if err := dec.Decode(&rsp); err != nil { 687 return fmt.Errorf("cannot unmarshal error: %v", err) 688 } 689 690 err := rsp.err(nil, r.StatusCode) 691 if err == nil { 692 return fmt.Errorf("server error: %q", r.Status) 693 } 694 return err 695 } 696 697 // SysInfo gets system information from the REST API. 698 func (client *Client) SysInfo() (*SysInfo, error) { 699 var sysInfo SysInfo 700 701 opts := &doOptions{ 702 Timeout: 25 * time.Second, 703 Retry: doRetry, 704 } 705 if _, err := client.doSyncWithOpts("GET", "/v2/system-info", nil, nil, nil, &sysInfo, opts); err != nil { 706 return nil, fmt.Errorf("cannot obtain system details: %v", err) 707 } 708 709 return &sysInfo, nil 710 } 711 712 type debugAction struct { 713 Action string `json:"action"` 714 Params interface{} `json:"params,omitempty"` 715 } 716 717 // Debug is only useful when writing test code, it will trigger 718 // an internal action with the given parameters. 719 func (client *Client) Debug(action string, params interface{}, result interface{}) error { 720 body, err := json.Marshal(debugAction{ 721 Action: action, 722 Params: params, 723 }) 724 if err != nil { 725 return err 726 } 727 728 _, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result) 729 return err 730 } 731 732 func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error { 733 urlParams := url.Values{"aspect": []string{aspect}} 734 for k, v := range params { 735 urlParams.Set(k, v) 736 } 737 _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result) 738 return err 739 } 740 741 type SystemRecoveryKeysResponse struct { 742 RecoveryKey string `json:"recovery-key"` 743 ReinstallKey string `json:"reinstall-key"` 744 } 745 746 func (client *Client) SystemRecoveryKeys(result interface{}) error { 747 _, err := client.doSync("GET", "/v2/system-recovery-keys", nil, nil, nil, &result) 748 return err 749 }