github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/net.go (about) 1 /* 2 * Copyright (c) 2015, Psiphon Inc. 3 * All rights reserved. 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 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 psiphon 21 22 import ( 23 "context" 24 "crypto/tls" 25 "crypto/x509" 26 std_errors "errors" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "net" 31 "net/http" 32 "os" 33 "strings" 34 "sync" 35 "sync/atomic" 36 "time" 37 38 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" 39 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" 40 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/fragmentor" 41 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters" 42 "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver" 43 "golang.org/x/net/bpf" 44 ) 45 46 // DialConfig contains parameters to determine the behavior 47 // of a Psiphon dialer (TCPDial, UDPDial, MeekDial, etc.) 48 type DialConfig struct { 49 50 // DiagnosticID is the server ID to record in any diagnostics notices. 51 DiagnosticID string 52 53 // UpstreamProxyURL specifies a proxy to connect through. 54 // E.g., "http://proxyhost:8080" 55 // "socks5://user:password@proxyhost:1080" 56 // "socks4a://proxyhost:1080" 57 // "http://NTDOMAIN\NTUser:password@proxyhost:3375" 58 // 59 // Certain tunnel protocols require HTTP CONNECT support 60 // when a HTTP proxy is specified. If CONNECT is not 61 // supported, those protocols will not connect. 62 // 63 // UpstreamProxyURL is not used by UDPDial. 64 UpstreamProxyURL string 65 66 // CustomHeaders is a set of additional arbitrary HTTP headers that are 67 // added to all plaintext HTTP requests and requests made through an HTTP 68 // upstream proxy when specified by UpstreamProxyURL. 69 CustomHeaders http.Header 70 71 // BPFProgramInstructions specifies a BPF program to attach to the dial 72 // socket before connecting. 73 BPFProgramInstructions []bpf.RawInstruction 74 75 // DeviceBinder, when not nil, is applied when dialing UDP/TCP. See: 76 // DeviceBinder doc. 77 DeviceBinder DeviceBinder 78 79 // IPv6Synthesizer, when not nil, is applied when dialing UDP/TCP. See: 80 // IPv6Synthesizer doc. 81 IPv6Synthesizer IPv6Synthesizer 82 83 // ResolveIP is used to resolve destination domains. ResolveIP should 84 // return either at least one IP address or an error. 85 ResolveIP func(context.Context, string) ([]net.IP, error) 86 87 // ResolvedIPCallback, when set, is called with the IP address that was 88 // dialed. This is either the specified IP address in the dial address, 89 // or the resolved IP address in the case where the dial address is a 90 // domain name. 91 // The callback may be invoked by a concurrent goroutine. 92 ResolvedIPCallback func(string) 93 94 // TrustedCACertificatesFilename specifies a file containing trusted 95 // CA certs. See Config.TrustedCACertificatesFilename. 96 TrustedCACertificatesFilename string 97 98 // FragmentorConfig specifies whether to layer a fragmentor.Conn on top 99 // of dialed TCP conns, and the fragmentation configuration to use. 100 FragmentorConfig *fragmentor.Config 101 102 // UpstreamProxyErrorCallback is called when a dial fails due to an upstream 103 // proxy error. As the upstream proxy is user configured, the error message 104 // may need to be relayed to the user. 105 UpstreamProxyErrorCallback func(error) 106 107 // CustomDialer overrides the dialer created by NewNetDialer/NewTCPDialer. 108 // When CustomDialer is set, all other DialConfig parameters are ignored by 109 // NewNetDialer/NewTCPDialer. Other DialConfig consumers may still reference 110 // other DialConfig parameters; for example MeekConfig still uses 111 // TrustedCACertificatesFilename. 112 CustomDialer common.Dialer 113 } 114 115 // WithoutFragmentor returns a copy of the DialConfig with any fragmentor 116 // configuration disabled. The return value is not a deep copy and may be the 117 // input DialConfig; it should not be modified. 118 func (config *DialConfig) WithoutFragmentor() *DialConfig { 119 if config.FragmentorConfig == nil { 120 return config 121 } 122 newConfig := new(DialConfig) 123 *newConfig = *config 124 newConfig.FragmentorConfig = nil 125 return newConfig 126 } 127 128 // NetworkConnectivityChecker defines the interface to the external 129 // HasNetworkConnectivity provider, which call into the host application to 130 // check for network connectivity. 131 type NetworkConnectivityChecker interface { 132 // TODO: change to bool return value once gobind supports that type 133 HasNetworkConnectivity() int 134 } 135 136 // DeviceBinder defines the interface to the external BindToDevice provider 137 // which calls into the host application to bind sockets to specific devices. 138 // This is used for VPN routing exclusion. 139 // The string return value should report device information for diagnostics. 140 type DeviceBinder interface { 141 BindToDevice(fileDescriptor int) (string, error) 142 } 143 144 // DNSServerGetter defines the interface to the external GetDNSServers provider 145 // which calls into the host application to discover the native network DNS 146 // server settings. 147 type DNSServerGetter interface { 148 GetDNSServers() []string 149 } 150 151 // IPv6Synthesizer defines the interface to the external IPv6Synthesize 152 // provider which calls into the host application to synthesize IPv6 addresses 153 // from IPv4 ones. This is used to correctly lookup IPs on DNS64/NAT64 154 // networks. 155 type IPv6Synthesizer interface { 156 IPv6Synthesize(IPv4Addr string) string 157 } 158 159 // HasIPv6RouteGetter defines the interface to the external HasIPv6Route 160 // provider which calls into the host application to determine if the host 161 // has an IPv6 route. 162 type HasIPv6RouteGetter interface { 163 // TODO: change to bool return value once gobind supports that type 164 HasIPv6Route() int 165 } 166 167 // NetworkIDGetter defines the interface to the external GetNetworkID 168 // provider, which returns an identifier for the host's current active 169 // network. 170 // 171 // The identifier is a string that should indicate the network type and 172 // identity; for example "WIFI-<BSSID>" or "MOBILE-<MCC/MNC>". As this network 173 // ID is personally identifying, it is only used locally in the client to 174 // determine network context and is not sent to the Psiphon server. The 175 // identifer will be logged in diagnostics messages; in this case only the 176 // substring before the first "-" is logged, so all PII must appear after the 177 // first "-". 178 // 179 // NetworkIDGetter.GetNetworkID should always return an identifier value, as 180 // logic that uses GetNetworkID, including tactics, is intended to proceed 181 // regardless of whether an accurate network identifier can be obtained. By 182 // convention, the provider should return "UNKNOWN" when an accurate network 183 // identifier cannot be obtained. Best-effort is acceptable: e.g., return just 184 // "WIFI" when only the type of the network but no details can be determined. 185 type NetworkIDGetter interface { 186 GetNetworkID() string 187 } 188 189 // NetDialer implements an interface that matches net.Dialer. 190 // Limitation: only "tcp" Dials are supported. 191 type NetDialer struct { 192 dialTCP common.Dialer 193 } 194 195 // NewNetDialer creates a new NetDialer. 196 func NewNetDialer(config *DialConfig) *NetDialer { 197 return &NetDialer{ 198 dialTCP: NewTCPDialer(config), 199 } 200 } 201 202 func (d *NetDialer) Dial(network, address string) (net.Conn, error) { 203 conn, err := d.DialContext(context.Background(), network, address) 204 if err != nil { 205 return nil, errors.Trace(err) 206 } 207 return conn, nil 208 } 209 210 func (d *NetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 211 switch network { 212 case "tcp": 213 conn, err := d.dialTCP(ctx, "tcp", address) 214 if err != nil { 215 return nil, errors.Trace(err) 216 } 217 return conn, nil 218 default: 219 return nil, errors.Tracef("unsupported network: %s", network) 220 } 221 } 222 223 // LocalProxyRelay sends to remoteConn bytes received from localConn, 224 // and sends to localConn bytes received from remoteConn. 225 // 226 // LocalProxyRelay must close localConn in order to interrupt blocking 227 // I/O calls when the upstream port forward is closed. remoteConn is 228 // also closed before returning. 229 func LocalProxyRelay(config *Config, proxyType string, localConn, remoteConn net.Conn) { 230 231 closing := int32(0) 232 233 copyWaitGroup := new(sync.WaitGroup) 234 copyWaitGroup.Add(1) 235 236 go func() { 237 defer copyWaitGroup.Done() 238 239 _, err := RelayCopyBuffer(config, localConn, remoteConn) 240 if err != nil && atomic.LoadInt32(&closing) != 1 { 241 NoticeLocalProxyError(proxyType, errors.TraceMsg(err, "Relay failed")) 242 } 243 244 // When the server closes a port forward, ex. due to idle timeout, 245 // remoteConn.Read will return EOF, which causes the downstream io.Copy to 246 // return (with a nil error). To ensure the downstream local proxy 247 // connection also closes at this point, we interrupt the blocking upstream 248 // io.Copy by closing localConn. 249 250 atomic.StoreInt32(&closing, 1) 251 localConn.Close() 252 }() 253 254 _, err := RelayCopyBuffer(config, remoteConn, localConn) 255 if err != nil && atomic.LoadInt32(&closing) != 1 { 256 NoticeLocalProxyError(proxyType, errors.TraceMsg(err, "Relay failed")) 257 } 258 259 // When a local proxy peer connection closes, localConn.Read will return EOF. 260 // As above, close the other end of the relay to ensure immediate shutdown, 261 // as no more data can be relayed. 262 263 atomic.StoreInt32(&closing, 1) 264 remoteConn.Close() 265 266 copyWaitGroup.Wait() 267 } 268 269 // RelayCopyBuffer performs an io.Copy, optionally using a smaller buffer when 270 // config.LimitRelayBufferSizes is set. 271 func RelayCopyBuffer(config *Config, dst io.Writer, src io.Reader) (int64, error) { 272 273 // By default, io.CopyBuffer will allocate a 32K buffer when a nil buffer 274 // is passed in. When configured, make and specify a smaller buffer. But 275 // only if src doesn't implement WriterTo and dst doesn't implement 276 // ReaderFrom, as in those cases io.CopyBuffer entirely avoids a buffer 277 // allocation. 278 279 var buffer []byte 280 if config.LimitRelayBufferSizes { 281 _, isWT := src.(io.WriterTo) 282 _, isRF := dst.(io.ReaderFrom) 283 if !isWT && !isRF { 284 buffer = make([]byte, 4096) 285 } 286 } 287 288 // Do not wrap any I/O errors 289 return io.CopyBuffer(dst, src, buffer) 290 } 291 292 // WaitForNetworkConnectivity uses a NetworkConnectivityChecker to 293 // periodically check for network connectivity. It returns true if 294 // no NetworkConnectivityChecker is provided (waiting is disabled) 295 // or when NetworkConnectivityChecker.HasNetworkConnectivity() 296 // indicates connectivity. It waits and polls the checker once a second. 297 // When the context is done, false is returned immediately. 298 func WaitForNetworkConnectivity( 299 ctx context.Context, connectivityChecker NetworkConnectivityChecker) bool { 300 301 if connectivityChecker == nil || connectivityChecker.HasNetworkConnectivity() == 1 { 302 return true 303 } 304 305 NoticeInfo("waiting for network connectivity") 306 307 ticker := time.NewTicker(1 * time.Second) 308 defer ticker.Stop() 309 310 for { 311 if connectivityChecker.HasNetworkConnectivity() == 1 { 312 return true 313 } 314 315 select { 316 case <-ticker.C: 317 // Check HasNetworkConnectivity again 318 case <-ctx.Done(): 319 return false 320 } 321 } 322 } 323 324 // New Resolver creates a new resolver using the specified config. 325 // useBindToDevice indicates whether to apply config.BindToDevice, when it 326 // exists; set useBindToDevice to false when the resolve doesn't need to be 327 // excluded from any VPN routing. 328 func NewResolver(config *Config, useBindToDevice bool) *resolver.Resolver { 329 330 p := config.GetParameters().Get() 331 332 networkConfig := &resolver.NetworkConfig{ 333 LogWarning: func(err error) { NoticeWarning("ResolveIP: %v", err) }, 334 LogHostnames: config.EmitDiagnosticNetworkParameters, 335 CacheExtensionInitialTTL: p.Duration(parameters.DNSResolverCacheExtensionInitialTTL), 336 CacheExtensionVerifiedTTL: p.Duration(parameters.DNSResolverCacheExtensionVerifiedTTL), 337 } 338 339 if config.DNSServerGetter != nil { 340 networkConfig.GetDNSServers = config.DNSServerGetter.GetDNSServers 341 } 342 343 if useBindToDevice && config.DeviceBinder != nil { 344 networkConfig.BindToDevice = config.DeviceBinder.BindToDevice 345 networkConfig.AllowDefaultResolverWithBindToDevice = 346 config.AllowDefaultDNSResolverWithBindToDevice 347 } 348 349 if config.IPv6Synthesizer != nil { 350 networkConfig.IPv6Synthesize = config.IPv6Synthesizer.IPv6Synthesize 351 } 352 353 if config.HasIPv6RouteGetter != nil { 354 networkConfig.HasIPv6Route = func() bool { 355 return config.HasIPv6RouteGetter.HasIPv6Route() == 1 356 } 357 } 358 359 return resolver.NewResolver(networkConfig, config.GetNetworkID()) 360 } 361 362 // UntunneledResolveIP is used to resolve domains for untunneled dials, 363 // including remote server list and upgrade downloads. 364 func UntunneledResolveIP( 365 ctx context.Context, 366 config *Config, 367 resolver *resolver.Resolver, 368 hostname string) ([]net.IP, error) { 369 370 // Limitations: for untunneled resolves, there is currently no resolve 371 // parameter replay, and no support for pre-resolved IPs. 372 373 params, err := resolver.MakeResolveParameters( 374 config.GetParameters().Get(), "") 375 if err != nil { 376 return nil, errors.Trace(err) 377 } 378 379 IPs, err := resolver.ResolveIP( 380 ctx, 381 config.GetNetworkID(), 382 params, 383 hostname) 384 if err != nil { 385 return nil, errors.Trace(err) 386 } 387 388 return IPs, nil 389 } 390 391 // MakeUntunneledHTTPClient returns a net/http.Client which is configured to 392 // use custom dialing features -- including BindToDevice, etc. 393 // 394 // The context is applied to underlying TCP dials. The caller is responsible 395 // for applying the context to requests made with the returned http.Client. 396 func MakeUntunneledHTTPClient( 397 ctx context.Context, 398 config *Config, 399 untunneledDialConfig *DialConfig, 400 skipVerify bool) (*http.Client, error) { 401 402 dialer := NewTCPDialer(untunneledDialConfig) 403 404 tlsConfig := &CustomTLSConfig{ 405 Parameters: config.GetParameters(), 406 Dial: dialer, 407 UseDialAddrSNI: true, 408 SNIServerName: "", 409 SkipVerify: skipVerify, 410 TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename, 411 } 412 tlsConfig.EnableClientSessionCache() 413 414 tlsDialer := NewCustomTLSDialer(tlsConfig) 415 416 transport := &http.Transport{ 417 Dial: func(network, addr string) (net.Conn, error) { 418 return dialer(ctx, network, addr) 419 }, 420 DialTLS: func(network, addr string) (net.Conn, error) { 421 return tlsDialer(ctx, network, addr) 422 }, 423 } 424 425 httpClient := &http.Client{ 426 Transport: transport, 427 } 428 429 return httpClient, nil 430 } 431 432 // MakeTunneledHTTPClient returns a net/http.Client which is 433 // configured to use custom dialing features including tunneled 434 // dialing and, optionally, UseTrustedCACertificatesForStockTLS. 435 // This http.Client uses stock TLS for HTTPS. 436 func MakeTunneledHTTPClient( 437 config *Config, 438 tunnel *Tunnel, 439 skipVerify bool) (*http.Client, error) { 440 441 // Note: there is no dial context since SSH port forward dials cannot 442 // be interrupted directly. Closing the tunnel will interrupt the dials. 443 444 tunneledDialer := func(_, addr string) (net.Conn, error) { 445 // Set alwaysTunneled to ensure the http.Client traffic is always tunneled, 446 // even when split tunnel mode is enabled. 447 conn, _, err := tunnel.DialTCPChannel(addr, true, nil) 448 return conn, errors.Trace(err) 449 } 450 451 transport := &http.Transport{ 452 Dial: tunneledDialer, 453 } 454 455 if skipVerify { 456 457 transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 458 459 } else if config.TrustedCACertificatesFilename != "" { 460 461 rootCAs := x509.NewCertPool() 462 certData, err := ioutil.ReadFile(config.TrustedCACertificatesFilename) 463 if err != nil { 464 return nil, errors.Trace(err) 465 } 466 rootCAs.AppendCertsFromPEM(certData) 467 transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs} 468 } 469 470 return &http.Client{ 471 Transport: transport, 472 }, nil 473 } 474 475 // MakeDownloadHTTPClient is a helper that sets up a http.Client 476 // for use either untunneled or through a tunnel. 477 func MakeDownloadHTTPClient( 478 ctx context.Context, 479 config *Config, 480 tunnel *Tunnel, 481 untunneledDialConfig *DialConfig, 482 skipVerify bool) (*http.Client, bool, error) { 483 484 var httpClient *http.Client 485 var err error 486 487 tunneled := tunnel != nil 488 489 if tunneled { 490 491 httpClient, err = MakeTunneledHTTPClient( 492 config, tunnel, skipVerify) 493 if err != nil { 494 return nil, false, errors.Trace(err) 495 } 496 497 } else { 498 499 httpClient, err = MakeUntunneledHTTPClient( 500 ctx, config, untunneledDialConfig, skipVerify) 501 if err != nil { 502 return nil, false, errors.Trace(err) 503 } 504 } 505 506 return httpClient, tunneled, nil 507 } 508 509 // ResumeDownload is a reusable helper that downloads requestUrl via the 510 // httpClient, storing the result in downloadFilename when the download is 511 // complete. Intermediate, partial downloads state is stored in 512 // downloadFilename.part and downloadFilename.part.etag. 513 // Any existing downloadFilename file will be overwritten. 514 // 515 // In the case where the remote object has changed while a partial download 516 // is to be resumed, the partial state is reset and resumeDownload fails. 517 // The caller must restart the download. 518 // 519 // When ifNoneMatchETag is specified, no download is made if the remote 520 // object has the same ETag. ifNoneMatchETag has an effect only when no 521 // partial download is in progress. 522 // 523 func ResumeDownload( 524 ctx context.Context, 525 httpClient *http.Client, 526 downloadURL string, 527 userAgent string, 528 downloadFilename string, 529 ifNoneMatchETag string) (int64, string, error) { 530 531 partialFilename := fmt.Sprintf("%s.part", downloadFilename) 532 533 partialETagFilename := fmt.Sprintf("%s.part.etag", downloadFilename) 534 535 file, err := os.OpenFile(partialFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) 536 if err != nil { 537 return 0, "", errors.Trace(err) 538 } 539 defer file.Close() 540 541 fileInfo, err := file.Stat() 542 if err != nil { 543 return 0, "", errors.Trace(err) 544 } 545 546 // A partial download should have an ETag which is to be sent with the 547 // Range request to ensure that the source object is the same as the 548 // one that is partially downloaded. 549 var partialETag []byte 550 if fileInfo.Size() > 0 { 551 552 partialETag, err = ioutil.ReadFile(partialETagFilename) 553 554 // When the ETag can't be loaded, delete the partial download. To keep the 555 // code simple, there is no immediate, inline retry here, on the assumption 556 // that the controller's upgradeDownloader will shortly call DownloadUpgrade 557 // again. 558 if err != nil { 559 560 // On Windows, file must be closed before it can be deleted 561 file.Close() 562 563 tempErr := os.Remove(partialFilename) 564 if tempErr != nil && !os.IsNotExist(tempErr) { 565 NoticeWarning("reset partial download failed: %s", tempErr) 566 } 567 568 tempErr = os.Remove(partialETagFilename) 569 if tempErr != nil && !os.IsNotExist(tempErr) { 570 NoticeWarning("reset partial download ETag failed: %s", tempErr) 571 } 572 573 return 0, "", errors.Tracef( 574 "failed to load partial download ETag: %s", err) 575 } 576 } 577 578 request, err := http.NewRequest("GET", downloadURL, nil) 579 if err != nil { 580 return 0, "", errors.Trace(err) 581 } 582 583 request = request.WithContext(ctx) 584 585 request.Header.Set("User-Agent", userAgent) 586 587 request.Header.Add("Range", fmt.Sprintf("bytes=%d-", fileInfo.Size())) 588 589 if partialETag != nil { 590 591 // Note: not using If-Range, since not all host servers support it. 592 // Using If-Match means we need to check for status code 412 and reset 593 // when the ETag has changed since the last partial download. 594 request.Header.Add("If-Match", string(partialETag)) 595 596 } else if ifNoneMatchETag != "" { 597 598 // Can't specify both If-Match and If-None-Match. Behavior is undefined. 599 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 600 // So for downloaders that store an ETag and wish to use that to prevent 601 // redundant downloads, that ETag is sent as If-None-Match in the case 602 // where a partial download is not in progress. When a partial download 603 // is in progress, the partial ETag is sent as If-Match: either that's 604 // a version that was never fully received, or it's no longer current in 605 // which case the response will be StatusPreconditionFailed, the partial 606 // download will be discarded, and then the next retry will use 607 // If-None-Match. 608 609 // Note: in this case, fileInfo.Size() == 0 610 611 request.Header.Add("If-None-Match", ifNoneMatchETag) 612 } 613 614 response, err := httpClient.Do(request) 615 616 // The resumeable download may ask for bytes past the resource range 617 // since it doesn't store the "completed download" state. In this case, 618 // the HTTP server returns 416. Otherwise, we expect 206. We may also 619 // receive 412 on ETag mismatch. 620 if err == nil && 621 (response.StatusCode != http.StatusPartialContent && 622 623 // Certain http servers return 200 OK where we expect 206, so accept that. 624 response.StatusCode != http.StatusOK && 625 626 response.StatusCode != http.StatusRequestedRangeNotSatisfiable && 627 response.StatusCode != http.StatusPreconditionFailed && 628 response.StatusCode != http.StatusNotModified) { 629 response.Body.Close() 630 err = fmt.Errorf("unexpected response status code: %d", response.StatusCode) 631 } 632 if err != nil { 633 634 // Redact URL from "net/http" error message. 635 if !GetEmitNetworkParameters() { 636 errStr := err.Error() 637 err = std_errors.New(strings.Replace(errStr, downloadURL, "[redacted]", -1)) 638 } 639 640 return 0, "", errors.Trace(err) 641 } 642 defer response.Body.Close() 643 644 responseETag := response.Header.Get("ETag") 645 646 if response.StatusCode == http.StatusPreconditionFailed { 647 // When the ETag no longer matches, delete the partial download. As above, 648 // simply failing and relying on the caller's retry schedule. 649 os.Remove(partialFilename) 650 os.Remove(partialETagFilename) 651 return 0, "", errors.TraceNew("partial download ETag mismatch") 652 653 } else if response.StatusCode == http.StatusNotModified { 654 // This status code is possible in the "If-None-Match" case. Don't leave 655 // any partial download in progress. Caller should check that responseETag 656 // matches ifNoneMatchETag. 657 os.Remove(partialFilename) 658 os.Remove(partialETagFilename) 659 return 0, responseETag, nil 660 } 661 662 // Not making failure to write ETag file fatal, in case the entire download 663 // succeeds in this one request. 664 ioutil.WriteFile(partialETagFilename, []byte(responseETag), 0600) 665 666 // A partial download occurs when this copy is interrupted. The io.Copy 667 // will fail, leaving a partial download in place (.part and .part.etag). 668 n, err := io.Copy(NewSyncFileWriter(file), response.Body) 669 670 // From this point, n bytes are indicated as downloaded, even if there is 671 // an error; the caller may use this to report partial download progress. 672 673 if err != nil { 674 return n, "", errors.Trace(err) 675 } 676 677 // Ensure the file is flushed to disk. The deferred close 678 // will be a noop when this succeeds. 679 err = file.Close() 680 if err != nil { 681 return n, "", errors.Trace(err) 682 } 683 684 // Remove if exists, to enable rename 685 os.Remove(downloadFilename) 686 687 err = os.Rename(partialFilename, downloadFilename) 688 if err != nil { 689 return n, "", errors.Trace(err) 690 } 691 692 os.Remove(partialETagFilename) 693 694 return n, responseETag, nil 695 }