| 
									
										
										
										
											2023-03-12 16:00:57 +01:00
										 |  |  | // GoToSocial | 
					
						
							|  |  |  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  | // it under the terms of the GNU Affero General Public License as published by | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or | 
					
						
							|  |  |  | // (at your option) any later version. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  | // GNU Affero General Public License for more details. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | package httpclient | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2023-08-01 19:50:17 +02:00
										 |  |  | 	"crypto/tls" | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	"errors" | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	"io" | 
					
						
							|  |  |  | 	"net" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/netip" | 
					
						
							|  |  |  | 	"runtime" | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	"codeberg.org/gruf/go-cache/v3" | 
					
						
							|  |  |  | 	errorsv2 "codeberg.org/gruf/go-errors/v2" | 
					
						
							| 
									
										
										
										
											2023-06-02 09:34:52 +01:00
										 |  |  | 	"codeberg.org/gruf/go-iotools" | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | 
					
						
							|  |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | var ( | 
					
						
							| 
									
										
										
										
											2023-05-21 17:59:14 +01:00
										 |  |  | 	// ErrInvalidRequest is returned if a given HTTP request is invalid and cannot be performed. | 
					
						
							|  |  |  | 	ErrInvalidRequest = errors.New("invalid http request") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	// ErrInvalidNetwork is returned if the request would not be performed over TCP | 
					
						
							|  |  |  | 	ErrInvalidNetwork = errors.New("invalid network type") | 
					
						
							| 
									
										
										
										
											2022-05-26 13:38:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	// ErrReservedAddr is returned if a dialed address resolves to an IP within a blocked or reserved net. | 
					
						
							|  |  |  | 	ErrReservedAddr = errors.New("dial within blocked / reserved IP range") | 
					
						
							|  |  |  | ) | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | // Config provides configuration details for setting up a new | 
					
						
							|  |  |  | // instance of httpclient.Client{}. Within are a subset of the | 
					
						
							|  |  |  | // configuration values passed to initialized http.Transport{} | 
					
						
							|  |  |  | // and http.Client{}, along with httpclient.Client{} specific. | 
					
						
							|  |  |  | type Config struct { | 
					
						
							| 
									
										
										
										
											2024-04-26 13:50:46 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// MaxOpenConnsPerHost limits the max | 
					
						
							|  |  |  | 	// number of open connections to a host. | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 	MaxOpenConnsPerHost int | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 13:50:46 +01:00
										 |  |  | 	// AllowRanges allows outgoing | 
					
						
							|  |  |  | 	// communications to given IP nets. | 
					
						
							|  |  |  | 	AllowRanges []netip.Prefix | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// BlockRanges blocks outgoing | 
					
						
							|  |  |  | 	// communiciations to given IP nets. | 
					
						
							|  |  |  | 	BlockRanges []netip.Prefix | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// TLSInsecureSkipVerify can be set to true to | 
					
						
							|  |  |  | 	// skip validation of remote TLS certificates. | 
					
						
							|  |  |  | 	// | 
					
						
							|  |  |  | 	// THIS SHOULD BE USED FOR TESTING ONLY, IF YOU | 
					
						
							|  |  |  | 	// TURN THIS ON WHILE RUNNING IN PRODUCTION YOU | 
					
						
							|  |  |  | 	// ARE LEAVING YOUR SERVER WIDE OPEN TO ATTACKS! | 
					
						
							|  |  |  | 	TLSInsecureSkipVerify bool | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	// MaxIdleConns: see http.Transport{}.MaxIdleConns. | 
					
						
							|  |  |  | 	MaxIdleConns int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ReadBufferSize: see http.Transport{}.ReadBufferSize. | 
					
						
							|  |  |  | 	ReadBufferSize int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// WriteBufferSize: see http.Transport{}.WriteBufferSize. | 
					
						
							|  |  |  | 	WriteBufferSize int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Timeout: see http.Client{}.Timeout. | 
					
						
							|  |  |  | 	Timeout time.Duration | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// DisableCompression: see http.Transport{}.DisableCompression. | 
					
						
							|  |  |  | 	DisableCompression bool | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Client wraps an underlying http.Client{} to provide the following: | 
					
						
							| 
									
										
										
										
											2022-09-28 18:30:40 +01:00
										 |  |  | //   - setting a maximum received request body size, returning error on | 
					
						
							|  |  |  | //     large content lengths, and using a limited reader in all other | 
					
						
							|  |  |  | //     cases to protect against forged / unknown content-lengths | 
					
						
							|  |  |  | //   - protection from server side request forgery (SSRF) by only dialing | 
					
						
							|  |  |  | //     out to known public IP prefixes, configurable with allows/blocks | 
					
						
							| 
									
										
										
										
											2023-05-21 17:59:14 +01:00
										 |  |  | //   - retry-backoff logic for error temporary HTTP error responses | 
					
						
							|  |  |  | //   - optional request signing | 
					
						
							|  |  |  | //   - request logging | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | type Client struct { | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	client   http.Client | 
					
						
							| 
									
										
										
										
											2023-08-03 10:34:35 +01:00
										 |  |  | 	badHosts cache.TTLCache[string, struct{}] | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	retries  uint | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // New returns a new instance of Client initialized using configuration. | 
					
						
							|  |  |  | func New(cfg Config) *Client { | 
					
						
							|  |  |  | 	var c Client | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	c.retries = 5 | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-18 11:25:26 +02:00
										 |  |  | 	d := &net.Dialer{ | 
					
						
							| 
									
										
										
										
											2022-11-23 22:40:07 +01:00
										 |  |  | 		Timeout:   15 * time.Second, | 
					
						
							| 
									
										
										
										
											2022-07-18 11:25:26 +02:00
										 |  |  | 		KeepAlive: 30 * time.Second, | 
					
						
							|  |  |  | 		Resolver:  &net.Resolver{}, | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 	if cfg.MaxOpenConnsPerHost <= 0 { | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 		// By default base this value on GOMAXPROCS. | 
					
						
							|  |  |  | 		maxprocs := runtime.GOMAXPROCS(0) | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 		cfg.MaxOpenConnsPerHost = maxprocs * 20 | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if cfg.MaxIdleConns <= 0 { | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 		// By default base this value on MaxOpenConns. | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 		cfg.MaxIdleConns = cfg.MaxOpenConnsPerHost * 10 | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 13:50:46 +01:00
										 |  |  | 	// Protect the dialer | 
					
						
							|  |  |  | 	// with IP range sanitizer. | 
					
						
							| 
									
										
										
										
											2023-07-07 16:17:39 +02:00
										 |  |  | 	d.Control = (&Sanitizer{ | 
					
						
							|  |  |  | 		Allow: cfg.AllowRanges, | 
					
						
							|  |  |  | 		Block: cfg.BlockRanges, | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	}).Sanitize | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	// Prepare client fields. | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	c.client.Timeout = cfg.Timeout | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-26 13:50:46 +01:00
										 |  |  | 	// Prepare transport TLS config. | 
					
						
							| 
									
										
										
										
											2023-08-01 19:50:17 +02:00
										 |  |  | 	tlsClientConfig := &tls.Config{ | 
					
						
							|  |  |  | 		InsecureSkipVerify: cfg.TLSInsecureSkipVerify, //nolint:gosec | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if tlsClientConfig.InsecureSkipVerify { | 
					
						
							|  |  |  | 		// Warn against playing silly buggers. | 
					
						
							|  |  |  | 		log.Warn(nil, "http-client.tls-insecure-skip-verify was set to TRUE. "+ | 
					
						
							|  |  |  | 			"*****THIS SHOULD BE USED FOR TESTING ONLY, IF YOU TURN THIS ON WHILE "+ | 
					
						
							|  |  |  | 			"RUNNING IN PRODUCTION YOU ARE LEAVING YOUR SERVER WIDE OPEN TO ATTACKS! "+ | 
					
						
							|  |  |  | 			"IF IN DOUBT, STOP YOUR SERVER *NOW* AND ADJUST YOUR CONFIGURATION!*****", | 
					
						
							|  |  |  | 		) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	// Set underlying HTTP client roundtripper. | 
					
						
							| 
									
										
										
										
											2024-04-02 12:12:26 +01:00
										 |  |  | 	c.client.Transport = &signingtransport{http.Transport{ | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 		Proxy:                 http.ProxyFromEnvironment, | 
					
						
							|  |  |  | 		ForceAttemptHTTP2:     true, | 
					
						
							|  |  |  | 		DialContext:           d.DialContext, | 
					
						
							| 
									
										
										
										
											2023-08-01 19:50:17 +02:00
										 |  |  | 		TLSClientConfig:       tlsClientConfig, | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 		MaxIdleConns:          cfg.MaxIdleConns, | 
					
						
							|  |  |  | 		IdleConnTimeout:       90 * time.Second, | 
					
						
							|  |  |  | 		TLSHandshakeTimeout:   10 * time.Second, | 
					
						
							|  |  |  | 		ExpectContinueTimeout: 1 * time.Second, | 
					
						
							|  |  |  | 		ReadBufferSize:        cfg.ReadBufferSize, | 
					
						
							|  |  |  | 		WriteBufferSize:       cfg.WriteBufferSize, | 
					
						
							|  |  |  | 		DisableCompression:    cfg.DisableCompression, | 
					
						
							| 
									
										
										
										
											2024-04-02 12:12:26 +01:00
										 |  |  | 	}} | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	// Initiate outgoing bad hosts lookup cache. | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	c.badHosts = cache.NewTTL[string, struct{}](0, 512, 0) | 
					
						
							| 
									
										
										
										
											2023-04-29 17:44:20 +01:00
										 |  |  | 	c.badHosts.SetTTL(time.Hour, false) | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	if !c.badHosts.Start(time.Minute) { | 
					
						
							|  |  |  | 		log.Panic(nil, "failed to start transport controller cache") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	return &c | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-21 17:59:14 +01:00
										 |  |  | // Do will essentially perform http.Client{}.Do() with retry-backoff functionality. | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | func (c *Client) Do(r *http.Request) (rsp *http.Response, err error) { | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-21 17:59:14 +01:00
										 |  |  | 	// First validate incoming request. | 
					
						
							|  |  |  | 	if err := ValidateRequest(r); err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	// Wrap in our own request | 
					
						
							|  |  |  | 	// type for retry-backoff. | 
					
						
							|  |  |  | 	req := WrapRequest(r) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if gtscontext.IsFastfail(r.Context()) { | 
					
						
							|  |  |  | 		// If the fast-fail flag was set, just | 
					
						
							|  |  |  | 		// attempt a single iteration instead of | 
					
						
							|  |  |  | 		// following the below retry-backoff loop. | 
					
						
							| 
									
										
										
										
											2024-07-30 11:58:31 +00:00
										 |  |  | 		rsp, _, err = c.DoOnce(req) | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, fmt.Errorf("%w (fast fail)", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return rsp, nil | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	for { | 
					
						
							|  |  |  | 		var retry bool | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		// Perform the http request. | 
					
						
							| 
									
										
										
										
											2024-07-30 11:58:31 +00:00
										 |  |  | 		rsp, retry, err = c.DoOnce(req) | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		if err == nil { | 
					
						
							|  |  |  | 			return rsp, nil | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		if !retry { | 
					
						
							|  |  |  | 			// reached max retries, don't further backoff | 
					
						
							|  |  |  | 			return nil, fmt.Errorf("%w (max retries)", err) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		// Start new backoff sleep timer. | 
					
						
							|  |  |  | 		backoff := time.NewTimer(req.BackOff()) | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		select { | 
					
						
							|  |  |  | 		// Request ctx cancelled. | 
					
						
							|  |  |  | 		case <-r.Context().Done(): | 
					
						
							|  |  |  | 			backoff.Stop() | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			// Return context error. | 
					
						
							|  |  |  | 			err = r.Context().Err() | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		// Backoff for time. | 
					
						
							|  |  |  | 		case <-backoff.C: | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | // DoOnce wraps an underlying http.Client{}.Do() to perform our wrapped request type: | 
					
						
							|  |  |  | // rewinding response body to permit reuse, signing request data when SignFunc provided, | 
					
						
							|  |  |  | // marking erroring hosts, updating retry attempt counts and setting backoff from header. | 
					
						
							|  |  |  | func (c *Client) DoOnce(r *Request) (rsp *http.Response, retry bool, err error) { | 
					
						
							|  |  |  | 	if r.attempts > c.retries { | 
					
						
							|  |  |  | 		// Ensure request hasn't reached max number of attempts. | 
					
						
							|  |  |  | 		err = fmt.Errorf("httpclient: reached max retries (%d)", c.retries) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-04-02 12:12:26 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	// Update no. | 
					
						
							|  |  |  | 	// attempts. | 
					
						
							|  |  |  | 	r.attempts++ | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	// Reset backoff. | 
					
						
							|  |  |  | 	r.backoff = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Perform main routine. | 
					
						
							|  |  |  | 	rsp, retry, err = c.do(r) | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	if rsp != nil { | 
					
						
							|  |  |  | 		// Log successful rsp. | 
					
						
							|  |  |  | 		r.Entry.Info(rsp.Status) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-04-29 17:44:20 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	// Log any errors. | 
					
						
							|  |  |  | 	r.Entry.Error(err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch { | 
					
						
							|  |  |  | 	case !retry: | 
					
						
							|  |  |  | 		// If they were told not to | 
					
						
							|  |  |  | 		// retry, also set number of | 
					
						
							|  |  |  | 		// attempts to prevent retry. | 
					
						
							|  |  |  | 		r.attempts = c.retries + 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	case r.attempts > c.retries: | 
					
						
							|  |  |  | 		// On max retries, mark this as | 
					
						
							|  |  |  | 		// a "badhost", i.e. is erroring. | 
					
						
							|  |  |  | 		c.badHosts.Set(r.Host, struct{}{}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Ensure retry flag is unset | 
					
						
							|  |  |  | 		// when reached max attempts. | 
					
						
							|  |  |  | 		retry = false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	case c.badHosts.Has(r.Host): | 
					
						
							|  |  |  | 		// When retry is still permitted, | 
					
						
							|  |  |  | 		// check host hasn't been marked | 
					
						
							|  |  |  | 		// as a "badhost", i.e. erroring. | 
					
						
							|  |  |  | 		r.attempts = c.retries + 1 | 
					
						
							|  |  |  | 		retry = false | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // do performs the "meat" of DoOnce(), but it's separated out to allow | 
					
						
							|  |  |  | // easier wrapping of the response, retry, error returns with further logic. | 
					
						
							|  |  |  | func (c *Client) do(r *Request) (rsp *http.Response, retry bool, err error) { | 
					
						
							|  |  |  | 	// Perform the HTTP request. | 
					
						
							|  |  |  | 	rsp, err = c.client.Do(r.Request) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if errorsv2.IsV2(err, | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 			context.DeadlineExceeded, | 
					
						
							|  |  |  | 			context.Canceled, | 
					
						
							|  |  |  | 			ErrReservedAddr, | 
					
						
							|  |  |  | 		) { | 
					
						
							| 
									
										
										
										
											2023-04-29 17:44:20 +01:00
										 |  |  | 			// Non-retryable errors. | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			return nil, false, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if errstr := err.Error(); // | 
					
						
							| 
									
										
										
										
											2023-04-30 09:11:18 +01:00
										 |  |  | 		strings.Contains(errstr, "stopped after 10 redirects") || | 
					
						
							|  |  |  | 			strings.Contains(errstr, "tls: ") || | 
					
						
							|  |  |  | 			strings.Contains(errstr, "x509: ") { | 
					
						
							|  |  |  | 			// These error types aren't wrapped | 
					
						
							|  |  |  | 			// so we have to check the error string. | 
					
						
							|  |  |  | 			// All are unrecoverable! | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			return nil, false, err | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		if dnserr := errorsv2.AsV2[*net.DNSError](err); // | 
					
						
							|  |  |  | 		dnserr != nil && dnserr.IsNotFound { | 
					
						
							|  |  |  | 			// DNS lookup failure, this domain does not exist | 
					
						
							|  |  |  | 			return nil, false, gtserror.SetNotFound(err) | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		// A retryable error. | 
					
						
							|  |  |  | 		return nil, true, err | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 15:18:32 +01:00
										 |  |  | 	} else if rsp.StatusCode >= 500 || | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		rsp.StatusCode == http.StatusTooManyRequests { | 
					
						
							| 
									
										
										
										
											2022-11-08 09:35:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		// Codes over 500 (and 429: too many requests) | 
					
						
							|  |  |  | 		// are generally temporary errors. For these | 
					
						
							|  |  |  | 		// we replace the response with a loggable error. | 
					
						
							|  |  |  | 		err = fmt.Errorf(`http response: %s`, rsp.Status) | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 		// Search for a provided "Retry-After" header value. | 
					
						
							|  |  |  | 		if after := rsp.Header.Get("Retry-After"); after != "" { | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			// Get cur time. | 
					
						
							|  |  |  | 			now := time.Now() | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			if u, _ := strconv.ParseUint(after, 10, 32); u != 0 { | 
					
						
							|  |  |  | 				// An integer no. of backoff seconds was provided. | 
					
						
							| 
									
										
										
										
											2024-10-16 14:13:58 +02:00
										 |  |  | 				r.backoff = time.Duration(u) * time.Second // #nosec G115 -- We clamp backoff below. | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			} else if at, _ := http.ParseTime(after); !at.Before(now) { | 
					
						
							|  |  |  | 				// An HTTP formatted future date-time was provided. | 
					
						
							|  |  |  | 				r.backoff = at.Sub(now) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Don't let their provided backoff exceed our max. | 
					
						
							| 
									
										
										
										
											2024-10-16 14:13:58 +02:00
										 |  |  | 			if max := baseBackoff * time.Duration(c.retries); // #nosec G115 -- We control c.retries. | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 			r.backoff > max { | 
					
						
							|  |  |  | 				r.backoff = max | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Unset + close rsp. | 
					
						
							|  |  |  | 		_ = rsp.Body.Close() | 
					
						
							|  |  |  | 		return nil, true, err | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-28 16:45:21 +01:00
										 |  |  | 	// Seperate the body implementers. | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | 	rbody := (io.Reader)(rsp.Body) | 
					
						
							|  |  |  | 	cbody := (io.Closer)(rsp.Body) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	// Wrap closer to ensure body drained BEFORE close. | 
					
						
							| 
									
										
										
										
											2023-06-02 09:34:52 +01:00
										 |  |  | 	cbody = iotools.CloserAfterCallback(cbody, func() { | 
					
						
							|  |  |  | 		_, _ = discard.ReadFrom(rbody) | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-12 09:39:47 +00:00
										 |  |  | 	// Set the wrapped response body. | 
					
						
							|  |  |  | 	rsp.Body = &iotools.ReadCloserType{ | 
					
						
							|  |  |  | 		Reader: rbody, | 
					
						
							|  |  |  | 		Closer: cbody, | 
					
						
							| 
									
										
										
										
											2023-06-02 09:34:52 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 10:45:35 +01:00
										 |  |  | 	return rsp, true, nil | 
					
						
							| 
									
										
										
										
											2022-05-15 10:16:43 +01:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2023-06-02 09:34:52 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | // cast discard writer to full interface it supports. | 
					
						
							|  |  |  | var discard = io.Discard.(interface { //nolint | 
					
						
							|  |  |  | 	io.Writer | 
					
						
							|  |  |  | 	io.StringWriter | 
					
						
							|  |  |  | 	io.ReaderFrom | 
					
						
							|  |  |  | }) |