2016-12-04 20:18:13 +00:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
2017-01-11 16:01:32 +00:00
|
|
|
"bytes"
|
2017-08-14 21:50:15 +00:00
|
|
|
"compress/gzip"
|
2016-12-04 20:18:13 +00:00
|
|
|
"crypto/tls"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2017-01-11 16:01:32 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2016-12-04 20:18:13 +00:00
|
|
|
"net/url"
|
2017-09-14 00:27:01 +00:00
|
|
|
"path"
|
2016-12-04 20:18:13 +00:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
defaultRequestTimeout = time.Second * 5
|
|
|
|
)
|
|
|
|
|
|
|
|
func NewHTTP(config HTTPConfig, defaultWP WriteParams) (Client, error) {
|
|
|
|
// validate required parameters:
|
|
|
|
if len(config.URL) == 0 {
|
|
|
|
return nil, fmt.Errorf("config.URL is required to create an HTTP client")
|
|
|
|
}
|
|
|
|
if len(defaultWP.Database) == 0 {
|
|
|
|
return nil, fmt.Errorf("A default database is required to create an HTTP client")
|
|
|
|
}
|
|
|
|
|
|
|
|
// set defaults:
|
|
|
|
if config.Timeout == 0 {
|
|
|
|
config.Timeout = defaultRequestTimeout
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse URL:
|
|
|
|
u, err := url.Parse(config.URL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing config.URL: %s", err)
|
|
|
|
}
|
|
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
|
|
return nil, fmt.Errorf("config.URL scheme must be http(s), got %s", u.Scheme)
|
|
|
|
}
|
|
|
|
|
2017-06-16 19:05:08 +00:00
|
|
|
var transport http.Transport
|
|
|
|
if len(config.HTTPProxy) > 0 {
|
|
|
|
proxyURL, err := url.Parse(config.HTTPProxy)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing config.HTTPProxy: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
transport = http.Transport{
|
|
|
|
Proxy: http.ProxyURL(proxyURL),
|
|
|
|
TLSClientConfig: config.TLSConfig,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
transport = http.Transport{
|
2017-09-08 22:35:20 +00:00
|
|
|
Proxy: http.ProxyFromEnvironment,
|
2017-06-16 19:05:08 +00:00
|
|
|
TLSClientConfig: config.TLSConfig,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-04 20:18:13 +00:00
|
|
|
return &httpClient{
|
2017-01-11 16:01:32 +00:00
|
|
|
writeURL: writeURL(u, defaultWP),
|
2016-12-04 20:18:13 +00:00
|
|
|
config: config,
|
|
|
|
url: u,
|
2017-01-11 16:01:32 +00:00
|
|
|
client: &http.Client{
|
2017-06-16 19:05:08 +00:00
|
|
|
Timeout: config.Timeout,
|
|
|
|
Transport: &transport,
|
2016-12-04 20:18:13 +00:00
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2017-08-28 23:08:50 +00:00
|
|
|
type HTTPHeaders map[string]string
|
|
|
|
|
2016-12-04 20:18:13 +00:00
|
|
|
type HTTPConfig struct {
|
|
|
|
// URL should be of the form "http://host:port" (REQUIRED)
|
|
|
|
URL string
|
|
|
|
|
|
|
|
// UserAgent sets the User-Agent header.
|
|
|
|
UserAgent string
|
|
|
|
|
2017-01-11 16:01:32 +00:00
|
|
|
// Timeout specifies a time limit for requests made by this
|
|
|
|
// Client. The timeout includes connection time, any
|
|
|
|
// redirects, and reading the response body. The timer remains
|
|
|
|
// running after Get, Head, Post, or Do return and will
|
|
|
|
// interrupt reading of the Response.Body.
|
|
|
|
//
|
|
|
|
// A Timeout of zero means no timeout.
|
2016-12-04 20:18:13 +00:00
|
|
|
Timeout time.Duration
|
|
|
|
|
|
|
|
// Username is the basic auth username for the server.
|
|
|
|
Username string
|
|
|
|
// Password is the basic auth password for the server.
|
|
|
|
Password string
|
|
|
|
|
|
|
|
// TLSConfig is the tls auth settings to use for each request.
|
|
|
|
TLSConfig *tls.Config
|
|
|
|
|
2017-06-16 19:05:08 +00:00
|
|
|
// Proxy URL should be of the form "http://host:port"
|
|
|
|
HTTPProxy string
|
|
|
|
|
2017-08-28 23:08:50 +00:00
|
|
|
// HTTP headers to append to HTTP requests.
|
|
|
|
HTTPHeaders HTTPHeaders
|
|
|
|
|
2017-08-14 21:50:15 +00:00
|
|
|
// The content encoding mechanism to use for each request.
|
|
|
|
ContentEncoding string
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Response represents a list of statement results.
|
|
|
|
type Response struct {
|
|
|
|
// ignore Results:
|
|
|
|
Results []interface{} `json:"-"`
|
|
|
|
Err string `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error returns the first error from any statement.
|
|
|
|
// Returns nil if no errors occurred on any statements.
|
|
|
|
func (r *Response) Error() error {
|
|
|
|
if r.Err != "" {
|
|
|
|
return fmt.Errorf(r.Err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type httpClient struct {
|
2017-01-11 16:01:32 +00:00
|
|
|
writeURL string
|
2016-12-04 20:18:13 +00:00
|
|
|
config HTTPConfig
|
2017-01-11 16:01:32 +00:00
|
|
|
client *http.Client
|
2016-12-04 20:18:13 +00:00
|
|
|
url *url.URL
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *httpClient) Query(command string) error {
|
2017-01-11 16:01:32 +00:00
|
|
|
req, err := c.makeRequest(queryURL(c.url, command), bytes.NewReader([]byte("")))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.doRequest(req, http.StatusOK)
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
2017-10-05 23:14:21 +00:00
|
|
|
func (c *httpClient) WriteStream(r io.Reader) error {
|
|
|
|
req, err := c.makeWriteRequest(r, c.writeURL)
|
2017-01-11 16:01:32 +00:00
|
|
|
if err != nil {
|
2017-10-05 23:14:21 +00:00
|
|
|
return err
|
2017-01-11 16:01:32 +00:00
|
|
|
}
|
2016-12-04 20:18:13 +00:00
|
|
|
|
2017-10-05 23:14:21 +00:00
|
|
|
return c.doRequest(req, http.StatusNoContent)
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *httpClient) doRequest(
|
2017-01-11 16:01:32 +00:00
|
|
|
req *http.Request,
|
2016-12-04 20:18:13 +00:00
|
|
|
expectedCode int,
|
|
|
|
) error {
|
2017-01-11 16:01:32 +00:00
|
|
|
resp, err := c.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-12-04 20:18:13 +00:00
|
|
|
|
2017-01-11 16:01:32 +00:00
|
|
|
code := resp.StatusCode
|
2016-12-04 20:18:13 +00:00
|
|
|
// If it's a "no content" response, then release and return nil
|
2017-01-11 16:01:32 +00:00
|
|
|
if code == http.StatusNoContent {
|
2016-12-04 20:18:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// not a "no content" response, so parse the result:
|
|
|
|
var response Response
|
2017-01-11 16:01:32 +00:00
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Fatal error reading body: %s", err)
|
|
|
|
}
|
|
|
|
decErr := json.Unmarshal(body, &response)
|
2016-12-04 20:18:13 +00:00
|
|
|
|
|
|
|
// If we got a JSON decode error, send that back
|
|
|
|
if decErr != nil {
|
|
|
|
err = fmt.Errorf("Unable to decode json: received status code %d err: %s", code, decErr)
|
|
|
|
}
|
|
|
|
// Unexpected response code OR error in JSON response body overrides
|
|
|
|
// a JSON decode error:
|
|
|
|
if code != expectedCode || response.Error() != nil {
|
|
|
|
err = fmt.Errorf("Response Error: Status Code [%d], expected [%d], [%v]",
|
|
|
|
code, expectedCode, response.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *httpClient) makeWriteRequest(
|
2017-01-11 16:01:32 +00:00
|
|
|
body io.Reader,
|
|
|
|
writeURL string,
|
|
|
|
) (*http.Request, error) {
|
|
|
|
req, err := c.makeRequest(writeURL, body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-08-14 21:50:15 +00:00
|
|
|
if c.config.ContentEncoding == "gzip" {
|
|
|
|
req.Header.Set("Content-Encoding", "gzip")
|
|
|
|
}
|
2017-01-11 16:01:32 +00:00
|
|
|
return req, nil
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
2017-01-11 16:01:32 +00:00
|
|
|
func (c *httpClient) makeRequest(uri string, body io.Reader) (*http.Request, error) {
|
2017-08-14 21:50:15 +00:00
|
|
|
var req *http.Request
|
|
|
|
var err error
|
|
|
|
if c.config.ContentEncoding == "gzip" {
|
|
|
|
body, err = compressWithGzip(body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
req, err = http.NewRequest("POST", uri, body)
|
2017-01-11 16:01:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-08-28 23:08:50 +00:00
|
|
|
|
|
|
|
for header, value := range c.config.HTTPHeaders {
|
|
|
|
req.Header.Set(header, value)
|
|
|
|
}
|
|
|
|
|
2017-01-11 16:01:32 +00:00
|
|
|
req.Header.Set("Content-Type", "text/plain")
|
|
|
|
req.Header.Set("User-Agent", c.config.UserAgent)
|
2016-12-04 20:18:13 +00:00
|
|
|
if c.config.Username != "" && c.config.Password != "" {
|
2017-01-11 16:01:32 +00:00
|
|
|
req.SetBasicAuth(c.config.Username, c.config.Password)
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
2017-01-11 16:01:32 +00:00
|
|
|
return req, nil
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
2017-08-14 21:50:15 +00:00
|
|
|
func compressWithGzip(data io.Reader) (io.Reader, error) {
|
|
|
|
pr, pw := io.Pipe()
|
|
|
|
gw := gzip.NewWriter(pw)
|
|
|
|
var err error
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
_, err = io.Copy(gw, data)
|
|
|
|
gw.Close()
|
|
|
|
pw.Close()
|
|
|
|
}()
|
|
|
|
|
|
|
|
return pr, err
|
|
|
|
}
|
|
|
|
|
2016-12-04 20:18:13 +00:00
|
|
|
func (c *httpClient) Close() error {
|
|
|
|
// Nothing to do.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeURL(u *url.URL, wp WriteParams) string {
|
|
|
|
params := url.Values{}
|
|
|
|
params.Set("db", wp.Database)
|
|
|
|
if wp.RetentionPolicy != "" {
|
|
|
|
params.Set("rp", wp.RetentionPolicy)
|
|
|
|
}
|
|
|
|
if wp.Precision != "n" && wp.Precision != "" {
|
|
|
|
params.Set("precision", wp.Precision)
|
|
|
|
}
|
|
|
|
if wp.Consistency != "one" && wp.Consistency != "" {
|
|
|
|
params.Set("consistency", wp.Consistency)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.RawQuery = params.Encode()
|
2017-09-14 00:27:01 +00:00
|
|
|
p := u.Path
|
|
|
|
u.Path = path.Join(p, "write")
|
|
|
|
s := u.String()
|
|
|
|
u.Path = p
|
|
|
|
return s
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func queryURL(u *url.URL, command string) string {
|
|
|
|
params := url.Values{}
|
|
|
|
params.Set("q", command)
|
|
|
|
|
|
|
|
u.RawQuery = params.Encode()
|
2017-09-14 00:27:01 +00:00
|
|
|
p := u.Path
|
|
|
|
u.Path = path.Join(p, "query")
|
|
|
|
s := u.String()
|
|
|
|
u.Path = p
|
|
|
|
return s
|
2016-12-04 20:18:13 +00:00
|
|
|
}
|