1// Copyright 2020-2021 InfluxData, Inc. All rights reserved.
2// Use of this source code is governed by MIT
3// license that can be found in the LICENSE file.
4
5// Package http provides HTTP servicing related code.
6//
7// Important type is Service which handles HTTP operations. It is internally used by library and it is not necessary to use it directly for common operations.
8// It can be useful when creating custom InfluxDB2 server API calls using generated code from the domain package, that are not yet exposed by API of this library.
9//
10// Service can be obtained from client using HTTPService() method.
11// It can be also created directly. To instantiate a Service use NewService(). Remember, the authorization param is in form "Token your-auth-token". e.g. "Token DXnd7annkGteV5Wqx9G3YjO9Ezkw87nHk8OabcyHCxF5451kdBV0Ag2cG7OmZZgCUTHroagUPdxbuoyen6TSPw==".
12//     srv := http.NewService("http://localhost:8086", "Token my-token", http.DefaultOptions())
13package http
14
15import (
16	"context"
17	"encoding/json"
18	"io"
19	"io/ioutil"
20	"mime"
21	"net/http"
22	"net/url"
23	"strconv"
24
25	http2 "github.com/influxdata/influxdb-client-go/v2/internal/http"
26	"github.com/influxdata/influxdb-client-go/v2/internal/log"
27)
28
29// RequestCallback defines function called after a request is created before any call
30type RequestCallback func(req *http.Request)
31
32// ResponseCallback defines function called after a successful response was received
33type ResponseCallback func(resp *http.Response) error
34
35// Service handles HTTP operations with taking care of mandatory request headers and known errors
36type Service interface {
37	// DoPostRequest sends HTTP POST request to the given url with body
38	DoPostRequest(ctx context.Context, url string, body io.Reader, requestCallback RequestCallback, responseCallback ResponseCallback) *Error
39	// DoHTTPRequest sends given HTTP request and handles response
40	DoHTTPRequest(req *http.Request, requestCallback RequestCallback, responseCallback ResponseCallback) *Error
41	// DoHTTPRequestWithResponse sends given HTTP request and returns response
42	DoHTTPRequestWithResponse(req *http.Request, requestCallback RequestCallback) (*http.Response, error)
43	// SetAuthorization sets the authorization header value
44	SetAuthorization(authorization string)
45	// Authorization returns current authorization header value
46	Authorization() string
47	// ServerAPIURL returns URL to InfluxDB2 server API space
48	ServerAPIURL() string
49	// ServerURL returns URL to InfluxDB2 server
50	ServerURL() string
51}
52
53// service implements Service interface
54type service struct {
55	serverAPIURL  string
56	serverURL     string
57	authorization string
58	client        Doer
59}
60
61// NewService creates instance of http Service with given parameters
62func NewService(serverURL, authorization string, httpOptions *Options) Service {
63	apiURL, err := url.Parse(serverURL)
64	serverAPIURL := serverURL
65	if err == nil {
66		apiURL, err = apiURL.Parse("api/v2/")
67		if err == nil {
68			serverAPIURL = apiURL.String()
69		}
70	}
71	return &service{
72		serverAPIURL:  serverAPIURL,
73		serverURL:     serverURL,
74		authorization: authorization,
75		client:        httpOptions.HTTPDoer(),
76	}
77}
78
79func (s *service) ServerAPIURL() string {
80	return s.serverAPIURL
81}
82
83func (s *service) ServerURL() string {
84	return s.serverURL
85}
86
87func (s *service) SetAuthorization(authorization string) {
88	s.authorization = authorization
89}
90
91func (s *service) Authorization() string {
92	return s.authorization
93}
94
95func (s *service) DoPostRequest(ctx context.Context, url string, body io.Reader, requestCallback RequestCallback, responseCallback ResponseCallback) *Error {
96	return s.doHTTPRequestWithURL(ctx, http.MethodPost, url, body, requestCallback, responseCallback)
97}
98
99func (s *service) doHTTPRequestWithURL(ctx context.Context, method, url string, body io.Reader, requestCallback RequestCallback, responseCallback ResponseCallback) *Error {
100	req, err := http.NewRequestWithContext(ctx, method, url, body)
101	if err != nil {
102		return NewError(err)
103	}
104	return s.DoHTTPRequest(req, requestCallback, responseCallback)
105}
106
107func (s *service) DoHTTPRequest(req *http.Request, requestCallback RequestCallback, responseCallback ResponseCallback) *Error {
108	resp, err := s.DoHTTPRequestWithResponse(req, requestCallback)
109	if err != nil {
110		return NewError(err)
111	}
112
113	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
114		return s.parseHTTPError(resp)
115	}
116	if responseCallback != nil {
117		err := responseCallback(resp)
118		if err != nil {
119			return NewError(err)
120		}
121	}
122	return nil
123}
124
125func (s *service) DoHTTPRequestWithResponse(req *http.Request, requestCallback RequestCallback) (*http.Response, error) {
126	log.Infof("HTTP %s req to %s", req.Method, req.URL.String())
127	if len(s.authorization) > 0 {
128		req.Header.Set("Authorization", s.authorization)
129	}
130	if req.Header.Get("User-Agent") == "" {
131		req.Header.Set("User-Agent", http2.UserAgent)
132	}
133	if requestCallback != nil {
134		requestCallback(req)
135	}
136	return s.client.Do(req)
137}
138
139func (s *service) parseHTTPError(r *http.Response) *Error {
140	// successful status code range
141	if r.StatusCode >= 200 && r.StatusCode < 300 {
142		return nil
143	}
144	defer func() {
145		// discard body so connection can be reused
146		_, _ = io.Copy(ioutil.Discard, r.Body)
147		_ = r.Body.Close()
148	}()
149
150	perror := NewError(nil)
151	perror.StatusCode = r.StatusCode
152
153	if v := r.Header.Get("Retry-After"); v != "" {
154		r, err := strconv.ParseUint(v, 10, 32)
155		if err == nil {
156			perror.RetryAfter = uint(r)
157		}
158	}
159
160	// json encoded error
161	ctype, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
162	if ctype == "application/json" {
163		perror.Err = json.NewDecoder(r.Body).Decode(perror)
164	} else {
165		body, err := ioutil.ReadAll(r.Body)
166		if err != nil {
167			perror.Err = err
168			return perror
169		}
170
171		perror.Code = r.Status
172		perror.Message = string(body)
173	}
174
175	if perror.Code == "" && perror.Message == "" {
176		switch r.StatusCode {
177		case http.StatusTooManyRequests:
178			perror.Code = "too many requests"
179			perror.Message = "exceeded rate limit"
180		case http.StatusServiceUnavailable:
181			perror.Code = "unavailable"
182			perror.Message = "service temporarily unavailable"
183		default:
184			perror.Code = r.Status
185			perror.Message = r.Header.Get("X-Influxdb-Error")
186		}
187	}
188
189	return perror
190}
191