github.com/nsqio/nsq@v1.3.0/internal/http_api/api_request.go (about) 1 package http_api 2 3 import ( 4 "crypto/tls" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "net/url" 11 "strconv" 12 "strings" 13 "time" 14 ) 15 16 // A custom http.Transport with support for deadline timeouts 17 func NewDeadlineTransport(connectTimeout time.Duration, requestTimeout time.Duration) *http.Transport { 18 // arbitrary values copied from http.DefaultTransport 19 transport := &http.Transport{ 20 DialContext: (&net.Dialer{ 21 Timeout: connectTimeout, 22 KeepAlive: 30 * time.Second, 23 DualStack: true, 24 }).DialContext, 25 ResponseHeaderTimeout: requestTimeout, 26 MaxIdleConns: 100, 27 IdleConnTimeout: 90 * time.Second, 28 TLSHandshakeTimeout: 10 * time.Second, 29 } 30 return transport 31 } 32 33 type Client struct { 34 c *http.Client 35 } 36 37 func NewClient(tlsConfig *tls.Config, connectTimeout time.Duration, requestTimeout time.Duration) *Client { 38 transport := NewDeadlineTransport(connectTimeout, requestTimeout) 39 transport.TLSClientConfig = tlsConfig 40 return &Client{ 41 c: &http.Client{ 42 Transport: transport, 43 Timeout: requestTimeout, 44 }, 45 } 46 } 47 48 // GETV1 is a helper function to perform a V1 HTTP request 49 // and parse our NSQ daemon's expected response format, with deadlines. 50 func (c *Client) GETV1(endpoint string, v interface{}) error { 51 retry: 52 req, err := http.NewRequest("GET", endpoint, nil) 53 if err != nil { 54 return err 55 } 56 57 req.Header.Add("Accept", "application/vnd.nsq; version=1.0") 58 59 resp, err := c.c.Do(req) 60 if err != nil { 61 return err 62 } 63 64 body, err := io.ReadAll(resp.Body) 65 resp.Body.Close() 66 if err != nil { 67 return err 68 } 69 if resp.StatusCode != 200 { 70 if resp.StatusCode == 403 && !strings.HasPrefix(endpoint, "https") { 71 endpoint, err = httpsEndpoint(endpoint, body) 72 if err != nil { 73 return err 74 } 75 goto retry 76 } 77 return fmt.Errorf("got response %s %q", resp.Status, body) 78 } 79 err = json.Unmarshal(body, &v) 80 if err != nil { 81 return err 82 } 83 84 return nil 85 } 86 87 // PostV1 is a helper function to perform a V1 HTTP request 88 // and parse our NSQ daemon's expected response format, with deadlines. 89 func (c *Client) POSTV1(endpoint string) error { 90 retry: 91 req, err := http.NewRequest("POST", endpoint, nil) 92 if err != nil { 93 return err 94 } 95 96 req.Header.Add("Accept", "application/vnd.nsq; version=1.0") 97 98 resp, err := c.c.Do(req) 99 if err != nil { 100 return err 101 } 102 103 body, err := io.ReadAll(resp.Body) 104 resp.Body.Close() 105 if err != nil { 106 return err 107 } 108 if resp.StatusCode != 200 { 109 if resp.StatusCode == 403 && !strings.HasPrefix(endpoint, "https") { 110 endpoint, err = httpsEndpoint(endpoint, body) 111 if err != nil { 112 return err 113 } 114 goto retry 115 } 116 return fmt.Errorf("got response %s %q", resp.Status, body) 117 } 118 119 return nil 120 } 121 122 func httpsEndpoint(endpoint string, body []byte) (string, error) { 123 var forbiddenResp struct { 124 HTTPSPort int `json:"https_port"` 125 } 126 err := json.Unmarshal(body, &forbiddenResp) 127 if err != nil { 128 return "", err 129 } 130 131 u, err := url.Parse(endpoint) 132 if err != nil { 133 return "", err 134 } 135 136 host, _, err := net.SplitHostPort(u.Host) 137 if err != nil { 138 return "", err 139 } 140 141 u.Scheme = "https" 142 u.Host = net.JoinHostPort(host, strconv.Itoa(forbiddenResp.HTTPSPort)) 143 return u.String(), nil 144 }