github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 "encoding/json" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "net" 29 "net/http" 30 "net/url" 31 "os" 32 "path" 33 "time" 34 35 "github.com/snapcore/snapd/dirs" 36 "github.com/snapcore/snapd/jsonutil" 37 ) 38 39 func unixDialer(socketPath string) func(string, string) (net.Conn, error) { 40 if socketPath == "" { 41 socketPath = dirs.SnapdSocket 42 } 43 return func(_, _ string) (net.Conn, error) { 44 return net.Dial("unix", socketPath) 45 } 46 } 47 48 type doer interface { 49 Do(*http.Request) (*http.Response, error) 50 } 51 52 // Config allows to customize client behavior. 53 type Config struct { 54 // BaseURL contains the base URL where snappy daemon is expected to be. 55 // It can be empty for a default behavior of talking over a unix socket. 56 BaseURL string 57 58 // DisableAuth controls whether the client should send an 59 // Authorization header from reading the auth.json data. 60 DisableAuth bool 61 62 // Interactive controls whether the client runs in interactive mode. 63 // At present, this only affects whether interactive polkit 64 // authorisation is requested. 65 Interactive bool 66 67 // Socket is the path to the unix socket to use 68 Socket string 69 70 // DisableKeepAlive indicates whether the connections should not be kept 71 // alive for later reuse 72 DisableKeepAlive bool 73 74 // User-Agent to sent to the snapd daemon 75 UserAgent string 76 } 77 78 // A Client knows how to talk to the snappy daemon. 79 type Client struct { 80 baseURL url.URL 81 doer doer 82 83 disableAuth bool 84 interactive bool 85 86 maintenance error 87 88 warningCount int 89 warningTimestamp time.Time 90 91 userAgent string 92 } 93 94 // New returns a new instance of Client 95 func New(config *Config) *Client { 96 if config == nil { 97 config = &Config{} 98 } 99 100 // By default talk over an UNIX socket. 101 if config.BaseURL == "" { 102 transport := &http.Transport{Dial: unixDialer(config.Socket), DisableKeepAlives: config.DisableKeepAlive} 103 return &Client{ 104 baseURL: url.URL{ 105 Scheme: "http", 106 Host: "localhost", 107 }, 108 doer: &http.Client{Transport: transport}, 109 disableAuth: config.DisableAuth, 110 interactive: config.Interactive, 111 userAgent: config.UserAgent, 112 } 113 } 114 115 baseURL, err := url.Parse(config.BaseURL) 116 if err != nil { 117 panic(fmt.Sprintf("cannot parse server base URL: %q (%v)", config.BaseURL, err)) 118 } 119 return &Client{ 120 baseURL: *baseURL, 121 doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, 122 disableAuth: config.DisableAuth, 123 interactive: config.Interactive, 124 userAgent: config.UserAgent, 125 } 126 } 127 128 // Maintenance returns an error reflecting the daemon maintenance status or nil. 129 func (client *Client) Maintenance() error { 130 return client.maintenance 131 } 132 133 // WarningsSummary returns the number of warnings that are ready to be shown to 134 // the user, and the timestamp of the most recently added warning (useful for 135 // silencing the warning alerts, and OKing the returned warnings). 136 func (client *Client) WarningsSummary() (count int, timestamp time.Time) { 137 return client.warningCount, client.warningTimestamp 138 } 139 140 func (client *Client) WhoAmI() (string, error) { 141 user, err := readAuthData() 142 if os.IsNotExist(err) { 143 return "", nil 144 } 145 if err != nil { 146 return "", err 147 } 148 149 return user.Email, nil 150 } 151 152 func (client *Client) setAuthorization(req *http.Request) error { 153 user, err := readAuthData() 154 if os.IsNotExist(err) { 155 return nil 156 } 157 if err != nil { 158 return err 159 } 160 161 var buf bytes.Buffer 162 fmt.Fprintf(&buf, `Macaroon root="%s"`, user.Macaroon) 163 for _, discharge := range user.Discharges { 164 fmt.Fprintf(&buf, `, discharge="%s"`, discharge) 165 } 166 req.Header.Set("Authorization", buf.String()) 167 return nil 168 } 169 170 type RequestError struct{ error } 171 172 func (e RequestError) Error() string { 173 return fmt.Sprintf("cannot build request: %v", e.error) 174 } 175 176 type AuthorizationError struct{ error } 177 178 func (e AuthorizationError) Error() string { 179 return fmt.Sprintf("cannot add authorization: %v", e.error) 180 } 181 182 type ConnectionError struct{ error } 183 184 func (e ConnectionError) Error() string { 185 return fmt.Sprintf("cannot communicate with server: %v", e.error) 186 } 187 188 // AllowInteractionHeader is the HTTP request header used to indicate 189 // that the client is willing to allow interaction. 190 const AllowInteractionHeader = "X-Allow-Interaction" 191 192 // raw performs a request and returns the resulting http.Response and 193 // error you usually only need to call this directly if you expect the 194 // response to not be JSON, otherwise you'd call Do(...) instead. 195 func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { 196 // fake a url to keep http.Client happy 197 u := client.baseURL 198 u.Path = path.Join(client.baseURL.Path, urlpath) 199 u.RawQuery = query.Encode() 200 req, err := http.NewRequest(method, u.String(), body) 201 if err != nil { 202 return nil, RequestError{err} 203 } 204 if client.userAgent != "" { 205 req.Header.Set("User-Agent", client.userAgent) 206 } 207 208 for key, value := range headers { 209 req.Header.Set(key, value) 210 } 211 212 if !client.disableAuth { 213 // set Authorization header if there are user's credentials 214 err = client.setAuthorization(req) 215 if err != nil { 216 return nil, AuthorizationError{err} 217 } 218 } 219 220 if client.interactive { 221 req.Header.Set(AllowInteractionHeader, "true") 222 } 223 224 rsp, err := client.doer.Do(req) 225 if err != nil { 226 return nil, ConnectionError{err} 227 } 228 229 return rsp, nil 230 } 231 232 var ( 233 doRetry = 250 * time.Millisecond 234 doTimeout = 5 * time.Second 235 ) 236 237 // MockDoRetry mocks the delays used by the do retry loop. 238 func MockDoRetry(retry, timeout time.Duration) (restore func()) { 239 oldRetry := doRetry 240 oldTimeout := doTimeout 241 doRetry = retry 242 doTimeout = timeout 243 return func() { 244 doRetry = oldRetry 245 doTimeout = oldTimeout 246 } 247 } 248 249 type hijacked struct { 250 do func(*http.Request) (*http.Response, error) 251 } 252 253 func (h hijacked) Do(req *http.Request) (*http.Response, error) { 254 return h.do(req) 255 } 256 257 // Hijack lets the caller take over the raw http request 258 func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) { 259 client.doer = hijacked{f} 260 } 261 262 // do performs a request and decodes the resulting json into the given 263 // value. It's low-level, for testing/experimenting only; you should 264 // usually use a higher level interface that builds on this. 265 func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (statusCode int, err error) { 266 retry := time.NewTicker(doRetry) 267 defer retry.Stop() 268 timeout := time.After(doTimeout) 269 var rsp *http.Response 270 for { 271 rsp, err = client.raw(method, path, query, headers, body) 272 if err == nil || method != "GET" { 273 break 274 } 275 select { 276 case <-retry.C: 277 continue 278 case <-timeout: 279 } 280 break 281 } 282 if err != nil { 283 return 0, err 284 } 285 defer rsp.Body.Close() 286 287 if v != nil { 288 if err := decodeInto(rsp.Body, v); err != nil { 289 return rsp.StatusCode, err 290 } 291 } 292 293 return rsp.StatusCode, nil 294 } 295 296 func decodeInto(reader io.Reader, v interface{}) error { 297 dec := json.NewDecoder(reader) 298 if err := dec.Decode(v); err != nil { 299 r := dec.Buffered() 300 buf, err1 := ioutil.ReadAll(r) 301 if err1 != nil { 302 buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1)) 303 } 304 return fmt.Errorf("cannot decode %q: %s", buf, err) 305 } 306 return nil 307 } 308 309 // doSync performs a request to the given path using the specified HTTP method. 310 // It expects a "sync" response from the API and on success decodes the JSON 311 // response payload into the given value using the "UseNumber" json decoding 312 // which produces json.Numbers instead of float64 types for numbers. 313 func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { 314 var rsp response 315 statusCode, err := client.do(method, path, query, headers, body, &rsp) 316 if err != nil { 317 return nil, err 318 } 319 if err := rsp.err(client, statusCode); err != nil { 320 return nil, err 321 } 322 if rsp.Type != "sync" { 323 return nil, fmt.Errorf("expected sync response, got %q", rsp.Type) 324 } 325 326 if v != nil { 327 if err := jsonutil.DecodeWithNumber(bytes.NewReader(rsp.Result), v); err != nil { 328 return nil, fmt.Errorf("cannot unmarshal: %v", err) 329 } 330 } 331 332 client.warningCount = rsp.WarningCount 333 client.warningTimestamp = rsp.WarningTimestamp 334 335 return &rsp.ResultInfo, nil 336 } 337 338 func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { 339 _, changeID, err = client.doAsyncFull(method, path, query, headers, body) 340 return 341 } 342 343 func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) { 344 var rsp response 345 statusCode, err := client.do(method, path, query, headers, body, &rsp) 346 if err != nil { 347 return nil, "", err 348 } 349 if err := rsp.err(client, statusCode); err != nil { 350 return nil, "", err 351 } 352 if rsp.Type != "async" { 353 return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type) 354 } 355 if statusCode != 202 { 356 return nil, "", fmt.Errorf("operation not accepted") 357 } 358 if rsp.Change == "" { 359 return nil, "", fmt.Errorf("async response without change reference") 360 } 361 362 return rsp.Result, rsp.Change, nil 363 } 364 365 type ServerVersion struct { 366 Version string 367 Series string 368 OSID string 369 OSVersionID string 370 OnClassic bool 371 372 KernelVersion string 373 } 374 375 func (client *Client) ServerVersion() (*ServerVersion, error) { 376 sysInfo, err := client.SysInfo() 377 if err != nil { 378 return nil, err 379 } 380 381 return &ServerVersion{ 382 Version: sysInfo.Version, 383 Series: sysInfo.Series, 384 OSID: sysInfo.OSRelease.ID, 385 OSVersionID: sysInfo.OSRelease.VersionID, 386 OnClassic: sysInfo.OnClassic, 387 388 KernelVersion: sysInfo.KernelVersion, 389 }, nil 390 } 391 392 // A response produced by the REST API will usually fit in this 393 // (exceptions are the icons/ endpoints obvs) 394 type response struct { 395 Result json.RawMessage `json:"result"` 396 Type string `json:"type"` 397 Change string `json:"change"` 398 399 WarningCount int `json:"warning-count"` 400 WarningTimestamp time.Time `json:"warning-timestamp"` 401 402 ResultInfo 403 404 Maintenance *Error `json:"maintenance"` 405 } 406 407 // Error is the real value of response.Result when an error occurs. 408 type Error struct { 409 Kind string `json:"kind"` 410 Value interface{} `json:"value"` 411 Message string `json:"message"` 412 413 StatusCode int 414 } 415 416 func (e *Error) Error() string { 417 return e.Message 418 } 419 420 const ( 421 ErrorKindTwoFactorRequired = "two-factor-required" 422 ErrorKindTwoFactorFailed = "two-factor-failed" 423 ErrorKindLoginRequired = "login-required" 424 ErrorKindInvalidAuthData = "invalid-auth-data" 425 ErrorKindTermsNotAccepted = "terms-not-accepted" 426 ErrorKindNoPaymentMethods = "no-payment-methods" 427 ErrorKindPaymentDeclined = "payment-declined" 428 ErrorKindPasswordPolicy = "password-policy" 429 430 ErrorKindSnapAlreadyInstalled = "snap-already-installed" 431 ErrorKindSnapNotInstalled = "snap-not-installed" 432 ErrorKindSnapNotFound = "snap-not-found" 433 ErrorKindAppNotFound = "app-not-found" 434 ErrorKindSnapLocal = "snap-local" 435 ErrorKindSnapNeedsDevMode = "snap-needs-devmode" 436 ErrorKindSnapNeedsClassic = "snap-needs-classic" 437 ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system" 438 ErrorKindSnapNotClassic = "snap-not-classic" 439 ErrorKindNoUpdateAvailable = "snap-no-update-available" 440 441 ErrorKindRevisionNotAvailable = "snap-revision-not-available" 442 ErrorKindChannelNotAvailable = "snap-channel-not-available" 443 ErrorKindArchitectureNotAvailable = "snap-architecture-not-available" 444 445 ErrorKindChangeConflict = "snap-change-conflict" 446 447 ErrorKindNotSnap = "snap-not-a-snap" 448 449 ErrorKindNetworkTimeout = "network-timeout" 450 ErrorKindDNSFailure = "dns-failure" 451 452 ErrorKindInterfacesUnchanged = "interfaces-unchanged" 453 454 ErrorKindBadQuery = "bad-query" 455 ErrorKindConfigNoSuchOption = "option-not-found" 456 457 ErrorKindSystemRestart = "system-restart" 458 ErrorKindDaemonRestart = "daemon-restart" 459 460 ErrorKindAssertionNotFound = "assertion-not-found" 461 ) 462 463 // IsRetryable returns true if the given error is an error 464 // that can be retried later. 465 func IsRetryable(err error) bool { 466 switch e := err.(type) { 467 case *Error: 468 return e.Kind == ErrorKindChangeConflict 469 } 470 return false 471 } 472 473 // IsTwoFactorError returns whether the given error is due to problems 474 // in two-factor authentication. 475 func IsTwoFactorError(err error) bool { 476 e, ok := err.(*Error) 477 if !ok || e == nil { 478 return false 479 } 480 481 return e.Kind == ErrorKindTwoFactorFailed || e.Kind == ErrorKindTwoFactorRequired 482 } 483 484 // IsInterfacesUnchangedError returns whether the given error means the requested 485 // change to interfaces was not made, because there was nothing to do. 486 func IsInterfacesUnchangedError(err error) bool { 487 e, ok := err.(*Error) 488 if !ok || e == nil { 489 return false 490 } 491 return e.Kind == ErrorKindInterfacesUnchanged 492 } 493 494 // IsAssertionNotFoundError returns whether the given error means that the 495 // assertion wasn't found and thus the device isn't ready/seeded. 496 func IsAssertionNotFoundError(err error) bool { 497 e, ok := err.(*Error) 498 if !ok || e == nil { 499 return false 500 } 501 502 return e.Kind == ErrorKindAssertionNotFound 503 } 504 505 // OSRelease contains information about the system extracted from /etc/os-release. 506 type OSRelease struct { 507 ID string `json:"id"` 508 VersionID string `json:"version-id,omitempty"` 509 } 510 511 // RefreshInfo contains information about refreshes. 512 type RefreshInfo struct { 513 // Timer contains the refresh.timer setting. 514 Timer string `json:"timer,omitempty"` 515 // Schedule contains the legacy refresh.schedule setting. 516 Schedule string `json:"schedule,omitempty"` 517 Last string `json:"last,omitempty"` 518 Hold string `json:"hold,omitempty"` 519 Next string `json:"next,omitempty"` 520 } 521 522 // SysInfo holds system information 523 type SysInfo struct { 524 Series string `json:"series,omitempty"` 525 Version string `json:"version,omitempty"` 526 BuildID string `json:"build-id"` 527 OSRelease OSRelease `json:"os-release"` 528 OnClassic bool `json:"on-classic"` 529 Managed bool `json:"managed"` 530 531 KernelVersion string `json:"kernel-version,omitempty"` 532 533 Refresh RefreshInfo `json:"refresh,omitempty"` 534 Confinement string `json:"confinement"` 535 SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"` 536 } 537 538 func (rsp *response) err(cli *Client, statusCode int) error { 539 if cli != nil { 540 maintErr := rsp.Maintenance 541 // avoid setting to (*client.Error)(nil) 542 if maintErr != nil { 543 cli.maintenance = maintErr 544 } else { 545 cli.maintenance = nil 546 } 547 } 548 if rsp.Type != "error" { 549 return nil 550 } 551 var resultErr Error 552 err := json.Unmarshal(rsp.Result, &resultErr) 553 if err != nil || resultErr.Message == "" { 554 return fmt.Errorf("server error: %q", http.StatusText(statusCode)) 555 } 556 resultErr.StatusCode = statusCode 557 558 return &resultErr 559 } 560 561 func parseError(r *http.Response) error { 562 var rsp response 563 if r.Header.Get("Content-Type") != "application/json" { 564 return fmt.Errorf("server error: %q", r.Status) 565 } 566 567 dec := json.NewDecoder(r.Body) 568 if err := dec.Decode(&rsp); err != nil { 569 return fmt.Errorf("cannot unmarshal error: %v", err) 570 } 571 572 err := rsp.err(nil, r.StatusCode) 573 if err == nil { 574 return fmt.Errorf("server error: %q", r.Status) 575 } 576 return err 577 } 578 579 // SysInfo gets system information from the REST API. 580 func (client *Client) SysInfo() (*SysInfo, error) { 581 var sysInfo SysInfo 582 583 if _, err := client.doSync("GET", "/v2/system-info", nil, nil, nil, &sysInfo); err != nil { 584 return nil, fmt.Errorf("cannot obtain system details: %v", err) 585 } 586 587 return &sysInfo, nil 588 } 589 590 // CreateUserResult holds the result of a user creation. 591 type CreateUserResult struct { 592 Username string `json:"username"` 593 SSHKeys []string `json:"ssh-keys"` 594 } 595 596 // CreateUserOptions holds options for creating a local system user. 597 // 598 // If Known is false, the provided email is used to query the store for 599 // username and SSH key details. 600 // 601 // If Known is true, the user will be created by looking through existing 602 // system-user assertions and looking for a matching email. If Email is 603 // empty then all such assertions are considered and multiple users may 604 // be created. 605 type CreateUserOptions struct { 606 Email string `json:"email,omitempty"` 607 Sudoer bool `json:"sudoer,omitempty"` 608 Known bool `json:"known,omitempty"` 609 ForceManaged bool `json:"force-managed,omitempty"` 610 } 611 612 // CreateUser creates a local system user. See CreateUserOptions for details. 613 func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) { 614 if options.Email == "" { 615 return nil, fmt.Errorf("cannot create a user without providing an email") 616 } 617 618 var result CreateUserResult 619 data, err := json.Marshal(options) 620 if err != nil { 621 return nil, err 622 } 623 624 if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { 625 return nil, fmt.Errorf("while creating user: %v", err) 626 } 627 return &result, nil 628 } 629 630 // CreateUsers creates multiple local system users. See CreateUserOptions for details. 631 // 632 // Results may be provided even if there are errors. 633 func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) { 634 for _, opts := range options { 635 if opts.Email == "" && !opts.Known { 636 return nil, fmt.Errorf("cannot create user from store details without an email to query for") 637 } 638 } 639 640 var results []*CreateUserResult 641 var errs []error 642 643 for _, opts := range options { 644 data, err := json.Marshal(opts) 645 if err != nil { 646 return nil, err 647 } 648 649 if opts.Email == "" { 650 var result []*CreateUserResult 651 if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { 652 errs = append(errs, err) 653 } else { 654 results = append(results, result...) 655 } 656 } else { 657 var result *CreateUserResult 658 if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { 659 errs = append(errs, err) 660 } else { 661 results = append(results, result) 662 } 663 } 664 } 665 666 if len(errs) == 1 { 667 return results, errs[0] 668 } 669 if len(errs) > 1 { 670 var buf bytes.Buffer 671 for _, err := range errs { 672 fmt.Fprintf(&buf, "\n- %s", err) 673 } 674 return results, fmt.Errorf("while creating users:%s", buf.Bytes()) 675 } 676 return results, nil 677 } 678 679 // Users returns the local users. 680 func (client *Client) Users() ([]*User, error) { 681 var result []*User 682 683 if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil { 684 return nil, fmt.Errorf("while getting users: %v", err) 685 } 686 return result, nil 687 } 688 689 type debugAction struct { 690 Action string `json:"action"` 691 Params interface{} `json:"params,omitempty"` 692 } 693 694 // Debug is only useful when writing test code, it will trigger 695 // an internal action with the given parameters. 696 func (client *Client) Debug(action string, params interface{}, result interface{}) error { 697 body, err := json.Marshal(debugAction{ 698 Action: action, 699 Params: params, 700 }) 701 if err != nil { 702 return err 703 } 704 705 _, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result) 706 return err 707 } 708 709 func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error { 710 urlParams := url.Values{"aspect": []string{aspect}} 711 for k, v := range params { 712 urlParams.Set(k, v) 713 } 714 _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result) 715 return err 716 }