github.com/vmware/govmomi@v0.51.0/vim25/soap/client.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package soap 6 7 import ( 8 "bufio" 9 "bytes" 10 "context" 11 "crypto/sha1" 12 "crypto/sha256" 13 "crypto/tls" 14 "crypto/x509" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "io" 19 "log" 20 "net" 21 "net/http" 22 "net/http/cookiejar" 23 "net/url" 24 "os" 25 "path" 26 "path/filepath" 27 "reflect" 28 "regexp" 29 "runtime" 30 "strings" 31 "sync" 32 33 "github.com/vmware/govmomi/internal/version" 34 "github.com/vmware/govmomi/vim25/progress" 35 "github.com/vmware/govmomi/vim25/types" 36 "github.com/vmware/govmomi/vim25/xml" 37 ) 38 39 type HasFault interface { 40 Fault() *Fault 41 } 42 43 type RoundTripper interface { 44 RoundTrip(ctx context.Context, req, res HasFault) error 45 } 46 47 const ( 48 SessionCookieName = "vmware_soap_session" 49 ) 50 51 // defaultUserAgent is the default user agent string, e.g. 52 // "govc govmomi/0.28.0 (go1.18.3;linux;amd64)" 53 var defaultUserAgent = fmt.Sprintf( 54 "%s %s/%s (%s)", 55 execName(), 56 version.ClientName, 57 version.ClientVersion, 58 strings.Join([]string{runtime.Version(), runtime.GOOS, runtime.GOARCH}, ";"), 59 ) 60 61 type Client struct { 62 http.Client 63 64 u *url.URL 65 k bool // Named after curl's -k flag 66 d *debugContainer 67 t *http.Transport 68 69 hostsMu sync.Mutex 70 hosts map[string]string 71 72 Namespace string `json:"namespace"` // Vim namespace 73 Version string `json:"version"` // Vim version 74 Types types.Func `json:"types"` 75 UserAgent string `json:"userAgent"` 76 77 // Cookie returns a value for the SOAP Header.Cookie. 78 // This SOAP request header is used for authentication by 79 // API endpoints such as pbm, vslm and sms. 80 // When nil, no SOAP Header.Cookie is set. 81 Cookie func() *HeaderElement 82 insecureCookies bool 83 84 useJSON bool 85 } 86 87 var schemeMatch = regexp.MustCompile(`^\w+://`) 88 89 type errInvalidCACertificate struct { 90 File string 91 } 92 93 func (e errInvalidCACertificate) Error() string { 94 return fmt.Sprintf( 95 "invalid certificate '%s', cannot be used as a trusted CA certificate", 96 e.File, 97 ) 98 } 99 100 // ParseURL is wrapper around url.Parse, where Scheme defaults to "https" and Path defaults to "/sdk" 101 func ParseURL(s string) (*url.URL, error) { 102 var err error 103 var u *url.URL 104 105 if s != "" { 106 // Default the scheme to https 107 if !schemeMatch.MatchString(s) { 108 s = "https://" + s 109 } 110 111 s := strings.TrimSuffix(s, "/") 112 u, err = url.Parse(s) 113 if err != nil { 114 return nil, err 115 } 116 117 // Default the path to /sdk 118 if u.Path == "" { 119 u.Path = "/sdk" 120 } 121 122 if u.User == nil { 123 u.User = url.UserPassword("", "") 124 } 125 } 126 127 return u, nil 128 } 129 130 // Go's ForceAttemptHTTP2 default is true, we disable by default. 131 // This undocumented env var can be used to enable. 132 var http2 = os.Getenv("GOVMOMI_HTTP2") == "true" 133 134 func NewClient(u *url.URL, insecure bool) *Client { 135 var t *http.Transport 136 137 if d, ok := http.DefaultTransport.(*http.Transport); ok { 138 // Inherit the same defaults explicitly set in http.DefaultTransport, 139 // unless otherwise noted. 140 t = &http.Transport{ 141 Proxy: d.Proxy, 142 DialContext: d.DialContext, 143 ForceAttemptHTTP2: http2, // false by default in govmomi 144 MaxIdleConns: d.MaxIdleConns, 145 IdleConnTimeout: d.IdleConnTimeout, 146 TLSHandshakeTimeout: d.TLSHandshakeTimeout, 147 ExpectContinueTimeout: d.ExpectContinueTimeout, 148 } 149 } else { 150 t = new(http.Transport) 151 } 152 153 t.TLSClientConfig = &tls.Config{ 154 InsecureSkipVerify: insecure, 155 } 156 157 c := newClientWithTransport(u, insecure, t) 158 159 // Always set DialTLS and DialTLSContext, even if InsecureSkipVerify=true, 160 // because of how certificate verification has been delegated to the host's 161 // PKI framework in Go 1.18. Please see the following links for more info: 162 // 163 // * https://tip.golang.org/doc/go1.18 (search for "Certificate.Verify") 164 // * https://github.com/square/certigo/issues/264 165 t.DialTLSContext = c.dialTLSContext 166 167 return c 168 } 169 170 func newClientWithTransport(u *url.URL, insecure bool, t *http.Transport) *Client { 171 c := Client{ 172 u: u, 173 k: insecure, 174 d: newDebug(), 175 t: t, 176 177 Types: types.TypeFunc(), 178 } 179 180 c.hosts = make(map[string]string) 181 182 c.Client.Transport = c.t 183 c.Client.Jar, _ = cookiejar.New(nil) 184 185 // Remove user information from a copy of the URL 186 c.u = c.URL() 187 c.u.User = nil 188 189 if c.u.Scheme == "http" { 190 c.insecureCookies = os.Getenv("GOVMOMI_INSECURE_COOKIES") == "true" 191 } 192 193 return &c 194 } 195 196 func (c *Client) DefaultTransport() *http.Transport { 197 return c.t 198 } 199 200 // NewServiceClient creates a NewClient with the given URL.Path and namespace. 201 func (c *Client) NewServiceClient(path string, namespace string) *Client { 202 return c.newServiceClientWithTransport(path, namespace, c.t) 203 } 204 205 func sessionCookie(jar http.CookieJar, u *url.URL) *HeaderElement { 206 for _, cookie := range jar.Cookies(u) { 207 if cookie.Name == SessionCookieName { 208 return &HeaderElement{Value: cookie.Value} 209 } 210 } 211 return nil 212 } 213 214 // SessionCookie returns a SessionCookie with value of the vmware_soap_session http.Cookie. 215 func (c *Client) SessionCookie() *HeaderElement { 216 u := c.URL() 217 218 if cookie := sessionCookie(c.Jar, u); cookie != nil { 219 return cookie 220 } 221 222 // Default "/sdk" Path would match above, 223 // but saw a case of Path == "sdk", where above returns nil. 224 // The jar entry Path is normally "/", so fallback to that. 225 u.Path = "/" 226 227 return sessionCookie(c.Jar, u) 228 } 229 230 func (c *Client) newServiceClientWithTransport(path string, namespace string, t *http.Transport) *Client { 231 vc := c.URL() 232 u, err := url.Parse(path) 233 if err != nil { 234 log.Panicf("url.Parse(%q): %s", path, err) 235 } 236 if u.Host == "" { 237 u.Scheme = vc.Scheme 238 u.Host = vc.Host 239 } 240 241 client := newClientWithTransport(u, c.k, t) 242 client.Namespace = "urn:" + namespace 243 244 // Copy the trusted thumbprints 245 c.hostsMu.Lock() 246 for k, v := range c.hosts { 247 client.hosts[k] = v 248 } 249 c.hostsMu.Unlock() 250 251 // Copy the cookies 252 client.Client.Jar.SetCookies(u, c.Client.Jar.Cookies(u)) 253 254 // Copy any query params (e.g. GOVMOMI_TUNNEL_PROXY_PORT used in testing) 255 client.u.RawQuery = vc.RawQuery 256 257 client.UserAgent = c.UserAgent 258 259 vimTypes := c.Types 260 client.Types = func(name string) (reflect.Type, bool) { 261 kind, ok := vimTypes(name) 262 if ok { 263 return kind, ok 264 } 265 // vim25/xml typeToString() does not have an option to include namespace prefix. 266 // Workaround this by re-trying the lookup with the namespace prefix. 267 return vimTypes(namespace + ":" + name) 268 } 269 270 return client 271 } 272 273 // UseJSON changes the protocol between SOAP and JSON. Starting with vCenter 274 // 8.0.1 JSON over HTTP can be used. Note this method has no locking and clients 275 // should be careful to not interfere with concurrent use of the client 276 // instance. 277 func (c *Client) UseJSON(useJSON bool) { 278 c.useJSON = useJSON 279 } 280 281 // SetRootCAs defines the set of PEM-encoded file locations of root certificate 282 // authorities the client uses when verifying server certificates instead of the 283 // TLS defaults which uses the host's root CA set. Multiple PEM file locations 284 // can be specified using the OS-specific PathListSeparator. 285 // 286 // See: http.Client.Transport.TLSClientConfig.RootCAs and 287 // https://pkg.go.dev/os#PathListSeparator 288 func (c *Client) SetRootCAs(pemPaths string) error { 289 pool := x509.NewCertPool() 290 291 for _, name := range filepath.SplitList(pemPaths) { 292 pem, err := os.ReadFile(filepath.Clean(name)) 293 if err != nil { 294 return err 295 } 296 297 if ok := pool.AppendCertsFromPEM(pem); !ok { 298 return errInvalidCACertificate{ 299 File: name, 300 } 301 } 302 } 303 304 c.t.TLSClientConfig.RootCAs = pool 305 306 return nil 307 } 308 309 // Add default https port if missing 310 func hostAddr(addr string) string { 311 _, port := splitHostPort(addr) 312 if port == "" { 313 return addr + ":443" 314 } 315 return addr 316 } 317 318 // SetThumbprint sets the known certificate thumbprint for the given host. 319 // A custom DialTLS function is used to support thumbprint based verification. 320 // We first try tls.Dial with the default tls.Config, only falling back to thumbprint verification 321 // if it fails with an x509.UnknownAuthorityError or x509.HostnameError 322 // 323 // See: http.Client.Transport.DialTLS 324 func (c *Client) SetThumbprint(host string, thumbprint string) { 325 host = hostAddr(host) 326 327 c.hostsMu.Lock() 328 if thumbprint == "" { 329 delete(c.hosts, host) 330 } else { 331 c.hosts[host] = thumbprint 332 } 333 c.hostsMu.Unlock() 334 } 335 336 // Thumbprint returns the certificate thumbprint for the given host if known to this client. 337 func (c *Client) Thumbprint(host string) string { 338 host = hostAddr(host) 339 c.hostsMu.Lock() 340 defer c.hostsMu.Unlock() 341 return c.hosts[host] 342 } 343 344 // KnownThumbprint checks whether the provided thumbprint is known to this client. 345 func (c *Client) KnownThumbprint(tp string) bool { 346 c.hostsMu.Lock() 347 defer c.hostsMu.Unlock() 348 349 for _, v := range c.hosts { 350 if v == tp { 351 return true 352 } 353 } 354 355 return false 356 } 357 358 // LoadThumbprints from file with the give name. 359 // If name is empty or name does not exist this function will return nil. 360 func (c *Client) LoadThumbprints(file string) error { 361 if file == "" { 362 return nil 363 } 364 365 for _, name := range filepath.SplitList(file) { 366 err := c.loadThumbprints(name) 367 if err != nil { 368 return err 369 } 370 } 371 372 return nil 373 } 374 375 func (c *Client) loadThumbprints(name string) error { 376 f, err := os.Open(filepath.Clean(name)) 377 if err != nil { 378 if os.IsNotExist(err) { 379 return nil 380 } 381 return err 382 } 383 384 scanner := bufio.NewScanner(f) 385 386 for scanner.Scan() { 387 e := strings.SplitN(scanner.Text(), " ", 2) 388 if len(e) != 2 { 389 continue 390 } 391 392 c.SetThumbprint(e[0], e[1]) 393 } 394 395 _ = f.Close() 396 397 return scanner.Err() 398 } 399 400 var fips140 = strings.Contains(os.Getenv("GODEBUG"), "fips140=only") 401 402 // ThumbprintSHA1 returns the thumbprint of the given cert in the same format used by the SDK and Client.SetThumbprint. 403 // 404 // See: SSLVerifyFault.Thumbprint, SessionManagerGenericServiceTicket.Thumbprint, HostConnectSpec.SslThumbprint 405 // When GODEBUG contains "fips140=only", this function returns an empty string. 406 func ThumbprintSHA1(cert *x509.Certificate) string { 407 if fips140 { 408 return "" 409 } 410 sum := sha1.Sum(cert.Raw) 411 hex := make([]string, len(sum)) 412 for i, b := range sum { 413 hex[i] = fmt.Sprintf("%02X", b) 414 } 415 return strings.Join(hex, ":") 416 } 417 418 // ThumbprintSHA256 returns the sha256 thumbprint of the given cert. 419 func ThumbprintSHA256(cert *x509.Certificate) string { 420 sum := sha256.Sum256(cert.Raw) 421 hex := make([]string, len(sum)) 422 for i, b := range sum { 423 hex[i] = fmt.Sprintf("%02X", b) 424 } 425 return strings.Join(hex, ":") 426 } 427 428 func thumbprintMatches(thumbprint string, cert *x509.Certificate) bool { 429 return thumbprint == ThumbprintSHA256(cert) || thumbprint == ThumbprintSHA1(cert) 430 } 431 432 func (c *Client) dialTLSContext( 433 ctx context.Context, 434 network, addr string) (net.Conn, error) { 435 436 // Would be nice if there was a tls.Config.Verify func, 437 // see tls.clientHandshakeState.doFullHandshake 438 439 conn, err := tls.Dial(network, addr, c.t.TLSClientConfig) 440 441 if err == nil { 442 return conn, nil 443 } 444 445 // Allow a thumbprint verification attempt if the error indicates 446 // the failure was due to lack of trust. 447 if !IsCertificateUntrusted(err) { 448 return nil, err 449 } 450 451 thumbprint := c.Thumbprint(addr) 452 if thumbprint == "" { 453 return nil, err 454 } 455 456 config := &tls.Config{InsecureSkipVerify: true} 457 conn, err = tls.Dial(network, addr, config) 458 if err != nil { 459 return nil, err 460 } 461 462 cert := conn.ConnectionState().PeerCertificates[0] 463 if thumbprintMatches(thumbprint, cert) { 464 return conn, nil 465 } 466 467 _ = conn.Close() 468 469 return nil, fmt.Errorf("host %q thumbprint does not match %q", addr, thumbprint) 470 } 471 472 // splitHostPort is similar to net.SplitHostPort, 473 // but rather than return error if there isn't a ':port', 474 // return an empty string for the port. 475 func splitHostPort(host string) (string, string) { 476 ix := strings.LastIndex(host, ":") 477 478 if ix <= strings.LastIndex(host, "]") { 479 return host, "" 480 } 481 482 name := host[:ix] 483 port := host[ix+1:] 484 485 return name, port 486 } 487 488 const sdkTunnel = "sdkTunnel:8089" 489 490 // Certificate returns the current TLS certificate. 491 func (c *Client) Certificate() *tls.Certificate { 492 certs := c.t.TLSClientConfig.Certificates 493 if len(certs) == 0 { 494 return nil 495 } 496 return &certs[0] 497 } 498 499 // SetCertificate st a certificate for TLS use. 500 func (c *Client) SetCertificate(cert tls.Certificate) { 501 t := c.Client.Transport.(*http.Transport) 502 503 // Extension or HoK certificate 504 t.TLSClientConfig.Certificates = []tls.Certificate{cert} 505 } 506 507 // UseServiceVersion sets Client.Version to the current version of the service endpoint via /sdk/vimServiceVersions.xml 508 func (c *Client) UseServiceVersion(kind ...string) error { 509 ns := "vim" 510 if len(kind) != 0 { 511 ns = kind[0] 512 } 513 514 u := c.URL() 515 u.Path = path.Join("/sdk", ns+"ServiceVersions.xml") 516 517 res, err := c.Get(u.String()) 518 if err != nil { 519 return err 520 } 521 522 if res.StatusCode != http.StatusOK { 523 return fmt.Errorf("http.Get(%s): %s", u.Path, res.Status) 524 } 525 526 v := struct { 527 Namespace *string `xml:"namespace>name"` 528 Version *string `xml:"namespace>version"` 529 }{ 530 &c.Namespace, 531 &c.Version, 532 } 533 534 err = xml.NewDecoder(res.Body).Decode(&v) 535 _ = res.Body.Close() 536 if err != nil { 537 return fmt.Errorf("xml.Decode(%s): %s", u.Path, err) 538 } 539 540 return nil 541 } 542 543 // Tunnel returns a Client configured to proxy requests through vCenter's http port 80, 544 // to the SDK tunnel virtual host. Use of the SDK tunnel is required by LoginExtensionByCertificate() 545 // and optional for other methods. 546 func (c *Client) Tunnel() *Client { 547 tunnel := c.newServiceClientWithTransport(c.u.Path, c.Namespace, c.DefaultTransport().Clone()) 548 549 t := tunnel.Client.Transport.(*http.Transport) 550 // Proxy to vCenter host on port 80 551 host := tunnel.u.Hostname() 552 // Should be no reason to change the default port other than testing 553 key := "GOVMOMI_TUNNEL_PROXY_PORT" 554 555 port := tunnel.URL().Query().Get(key) 556 if port == "" { 557 port = os.Getenv(key) 558 } 559 560 if port != "" { 561 host += ":" + port 562 } 563 564 t.Proxy = http.ProxyURL(&url.URL{ 565 Scheme: "http", 566 Host: host, 567 }) 568 569 // Rewrite url Host to use the sdk tunnel, required for a certificate request. 570 tunnel.u.Host = sdkTunnel 571 return tunnel 572 } 573 574 // URL returns the URL to which the client is configured 575 func (c *Client) URL() *url.URL { 576 urlCopy := *c.u 577 return &urlCopy 578 } 579 580 type marshaledClient struct { 581 Cookies []*http.Cookie `json:"cookies"` 582 URL *url.URL `json:"url"` 583 Insecure bool `json:"insecure"` 584 Version string `json:"version"` 585 UseJSON bool `json:"useJSON"` 586 } 587 588 // MarshalJSON writes the Client configuration to JSON. 589 func (c *Client) MarshalJSON() ([]byte, error) { 590 m := marshaledClient{ 591 Cookies: c.Jar.Cookies(c.u), 592 URL: c.u, 593 Insecure: c.k, 594 Version: c.Version, 595 UseJSON: c.useJSON, 596 } 597 598 return json.Marshal(m) 599 } 600 601 // UnmarshalJSON rads Client configuration from JSON. 602 func (c *Client) UnmarshalJSON(b []byte) error { 603 var m marshaledClient 604 605 err := json.Unmarshal(b, &m) 606 if err != nil { 607 return err 608 } 609 610 *c = *NewClient(m.URL, m.Insecure) 611 c.Version = m.Version 612 c.Jar.SetCookies(m.URL, m.Cookies) 613 c.useJSON = m.UseJSON 614 615 return nil 616 } 617 618 func (c *Client) setInsecureCookies(res *http.Response) { 619 cookies := res.Cookies() 620 if len(cookies) != 0 { 621 for _, cookie := range cookies { 622 cookie.Secure = false 623 } 624 c.Jar.SetCookies(c.u, cookies) 625 } 626 } 627 628 // Do is equivalent to http.Client.Do and takes care of API specifics including 629 // logging, user-agent header, handling cookies, measuring responsiveness of the 630 // API 631 func (c *Client) Do(ctx context.Context, req *http.Request, f func(*http.Response) error) error { 632 if ctx == nil { 633 ctx = context.Background() 634 } 635 // Create debugging context for this round trip 636 d := c.d.newRoundTrip() 637 if d.enabled() { 638 defer d.done() 639 } 640 641 // use default 642 if c.UserAgent == "" { 643 c.UserAgent = defaultUserAgent 644 } 645 646 req.Header.Set(`User-Agent`, c.UserAgent) 647 648 ext := "" 649 if d.enabled() { 650 ext = d.debugRequest(req) 651 } 652 653 res, err := c.Client.Do(req.WithContext(ctx)) 654 if err != nil { 655 return err 656 } 657 658 if d.enabled() { 659 d.debugResponse(res, ext) 660 } 661 662 if c.insecureCookies { 663 c.setInsecureCookies(res) 664 } 665 666 defer res.Body.Close() 667 668 return f(res) 669 } 670 671 // Signer can be implemented by soap.Header.Security to sign requests. 672 // If the soap.Header.Security field is set to an implementation of Signer via WithHeader(), 673 // then Client.RoundTrip will call Sign() to marshal the SOAP request. 674 type Signer interface { 675 Sign(Envelope) ([]byte, error) 676 } 677 678 type headerContext struct{} 679 680 // WithHeader can be used to modify the outgoing request soap.Header fields. 681 func (c *Client) WithHeader(ctx context.Context, header Header) context.Context { 682 return context.WithValue(ctx, headerContext{}, header) 683 } 684 685 type statusError struct { 686 res *http.Response 687 } 688 689 // Temporary returns true for HTTP response codes that can be retried 690 // See vim25.IsTemporaryNetworkError 691 func (e *statusError) Temporary() bool { 692 switch e.res.StatusCode { 693 case http.StatusBadGateway: 694 return true 695 } 696 return false 697 } 698 699 func (e *statusError) Error() string { 700 return e.res.Status 701 } 702 703 func newStatusError(res *http.Response) error { 704 return &url.Error{ 705 Op: res.Request.Method, 706 URL: res.Request.URL.Path, 707 Err: &statusError{res}, 708 } 709 } 710 711 // RoundTrip executes an API request to VMOMI server. 712 func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error { 713 if !c.useJSON { 714 return c.soapRoundTrip(ctx, reqBody, resBody) 715 } 716 return c.jsonRoundTrip(ctx, reqBody, resBody) 717 } 718 719 func (c *Client) soapRoundTrip(ctx context.Context, reqBody, resBody HasFault) error { 720 var err error 721 var b []byte 722 723 reqEnv := Envelope{Body: reqBody} 724 resEnv := Envelope{Body: resBody} 725 726 h, ok := ctx.Value(headerContext{}).(Header) 727 if !ok { 728 h = Header{} 729 } 730 731 // We added support for OperationID before soap.Header was exported. 732 if id, ok := ctx.Value(types.ID{}).(string); ok { 733 h.ID = id 734 } 735 736 if c.Cookie != nil { 737 h.Cookie = c.Cookie() 738 } 739 if h.Cookie != nil || h.ID != "" || h.Security != nil { 740 reqEnv.Header = &h // XML marshal header only if a field is set 741 } 742 743 if signer, ok := h.Security.(Signer); ok { 744 b, err = signer.Sign(reqEnv) 745 if err != nil { 746 return err 747 } 748 } else { 749 b, err = xml.Marshal(reqEnv) 750 if err != nil { 751 panic(err) 752 } 753 } 754 755 rawReqBody := io.MultiReader(strings.NewReader(xml.Header), bytes.NewReader(b)) 756 req, err := http.NewRequest("POST", c.u.String(), rawReqBody) 757 if err != nil { 758 panic(err) 759 } 760 761 req.Header.Set(`Content-Type`, `text/xml; charset="utf-8"`) 762 763 action := h.Action 764 if action == "" { 765 action = fmt.Sprintf("%s/%s", c.Namespace, c.Version) 766 } 767 req.Header.Set(`SOAPAction`, action) 768 769 return c.Do(ctx, req, func(res *http.Response) error { 770 switch res.StatusCode { 771 case http.StatusOK: 772 // OK 773 case http.StatusInternalServerError: 774 // Error, but typically includes a body explaining the error 775 default: 776 return newStatusError(res) 777 } 778 779 dec := xml.NewDecoder(res.Body) 780 dec.TypeFunc = c.Types 781 err = dec.Decode(&resEnv) 782 if err != nil { 783 return err 784 } 785 786 if f := resBody.Fault(); f != nil { 787 return WrapSoapFault(f) 788 } 789 790 return err 791 }) 792 } 793 794 func (c *Client) CloseIdleConnections() { 795 c.t.CloseIdleConnections() 796 } 797 798 // ParseURL wraps url.Parse to rewrite the URL.Host field 799 // In the case of VM guest uploads or NFC lease URLs, a Host 800 // field with a value of "*" is rewritten to the Client's URL.Host. 801 func (c *Client) ParseURL(urlStr string) (*url.URL, error) { 802 u, err := url.Parse(urlStr) 803 if err != nil { 804 return nil, err 805 } 806 807 host, _ := splitHostPort(u.Host) 808 if host == "*" { 809 // Also use Client's port, to support port forwarding 810 u.Host = c.URL().Host 811 } 812 813 return u, nil 814 } 815 816 type Upload struct { 817 Type string 818 Method string 819 ContentLength int64 820 Headers map[string]string 821 Ticket *http.Cookie 822 Progress progress.Sinker 823 Close bool 824 } 825 826 var DefaultUpload = Upload{ 827 Type: "application/octet-stream", 828 Method: "PUT", 829 } 830 831 // Upload PUTs the local file to the given URL 832 func (c *Client) Upload(ctx context.Context, f io.Reader, u *url.URL, param *Upload) error { 833 var err error 834 835 if param.Progress != nil { 836 pr := progress.NewReader(ctx, param.Progress, f, param.ContentLength) 837 f = pr 838 839 // Mark progress reader as done when returning from this function. 840 defer func() { 841 pr.Done(err) 842 }() 843 } 844 845 req, err := http.NewRequest(param.Method, u.String(), f) 846 if err != nil { 847 return err 848 } 849 850 req = req.WithContext(ctx) 851 req.Close = param.Close 852 req.ContentLength = param.ContentLength 853 req.Header.Set("Content-Type", param.Type) 854 855 for k, v := range param.Headers { 856 req.Header.Add(k, v) 857 } 858 859 if param.Ticket != nil { 860 req.AddCookie(param.Ticket) 861 } 862 863 res, err := c.Client.Do(req) 864 if err != nil { 865 return err 866 } 867 868 defer res.Body.Close() 869 870 switch res.StatusCode { 871 case http.StatusOK: 872 case http.StatusCreated: 873 default: 874 err = errors.New(res.Status) 875 } 876 877 return err 878 } 879 880 // UploadFile PUTs the local file to the given URL 881 func (c *Client) UploadFile(ctx context.Context, file string, u *url.URL, param *Upload) error { 882 if param == nil { 883 p := DefaultUpload // Copy since we set ContentLength 884 param = &p 885 } 886 887 s, err := os.Stat(file) 888 if err != nil { 889 return err 890 } 891 892 f, err := os.Open(filepath.Clean(file)) 893 if err != nil { 894 return err 895 } 896 defer f.Close() 897 898 param.ContentLength = s.Size() 899 900 return c.Upload(ctx, f, u, param) 901 } 902 903 type Download struct { 904 Method string 905 Headers map[string]string 906 Ticket *http.Cookie 907 Progress progress.Sinker 908 Writer io.Writer 909 Close bool 910 } 911 912 var DefaultDownload = Download{ 913 Method: "GET", 914 } 915 916 // DownloadRequest wraps http.Client.Do, returning the http.Response without checking its StatusCode 917 func (c *Client) DownloadRequest(ctx context.Context, u *url.URL, param *Download) (*http.Response, error) { 918 req, err := http.NewRequest(param.Method, u.String(), nil) 919 if err != nil { 920 return nil, err 921 } 922 923 req = req.WithContext(ctx) 924 req.Close = param.Close 925 926 for k, v := range param.Headers { 927 req.Header.Add(k, v) 928 } 929 930 if param.Ticket != nil { 931 req.AddCookie(param.Ticket) 932 } 933 934 return c.Client.Do(req) 935 } 936 937 // Download GETs the remote file from the given URL 938 func (c *Client) Download(ctx context.Context, u *url.URL, param *Download) (io.ReadCloser, int64, error) { 939 res, err := c.DownloadRequest(ctx, u, param) 940 if err != nil { 941 return nil, 0, err 942 } 943 944 switch res.StatusCode { 945 case http.StatusOK: 946 default: 947 err = fmt.Errorf("download(%s): %s", u, res.Status) 948 } 949 950 if err != nil { 951 return nil, 0, err 952 } 953 954 r := res.Body 955 956 return r, res.ContentLength, nil 957 } 958 959 func (c *Client) WriteFile(ctx context.Context, file string, src io.Reader, size int64, s progress.Sinker, w io.Writer) error { 960 var err error 961 962 r := src 963 964 fh, err := os.Create(file) 965 if err != nil { 966 return err 967 } 968 969 if s != nil { 970 pr := progress.NewReader(ctx, s, src, size) 971 r = pr 972 973 // Mark progress reader as done when returning from this function. 974 defer func() { 975 pr.Done(err) 976 }() 977 } 978 979 if w == nil { 980 w = fh 981 } else { 982 w = io.MultiWriter(w, fh) 983 } 984 985 _, err = io.Copy(w, r) 986 987 cerr := fh.Close() 988 989 if err == nil { 990 err = cerr 991 } 992 993 return err 994 } 995 996 // DownloadFile GETs the given URL to a local file 997 func (c *Client) DownloadFile(ctx context.Context, file string, u *url.URL, param *Download) error { 998 var err error 999 if param == nil { 1000 param = &DefaultDownload 1001 } 1002 1003 rc, contentLength, err := c.Download(ctx, u, param) 1004 if err != nil { 1005 return err 1006 } 1007 1008 return c.WriteFile(ctx, file, rc, contentLength, param.Progress, param.Writer) 1009 } 1010 1011 // execName gets the name of the executable for the current process 1012 func execName() string { 1013 name, err := os.Executable() 1014 if err != nil { 1015 return "N/A" 1016 } 1017 return strings.TrimSuffix(filepath.Base(name), ".exe") 1018 }