You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
497 lines
15 KiB
497 lines
15 KiB
//
|
|
// DISCLAIMER
|
|
//
|
|
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
// Copyright holder is ArangoDB GmbH, Cologne, Germany
|
|
//
|
|
// Author Ewout Prangsma
|
|
//
|
|
|
|
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptrace"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
velocypack "github.com/arangodb/go-velocypack"
|
|
|
|
driver "github.com/arangodb/go-driver"
|
|
"github.com/arangodb/go-driver/cluster"
|
|
"github.com/arangodb/go-driver/util"
|
|
)
|
|
|
|
const (
|
|
DefaultMaxIdleConnsPerHost = 64
|
|
DefaultConnLimit = 32
|
|
|
|
keyRawResponse driver.ContextKey = "arangodb-rawResponse"
|
|
keyResponse driver.ContextKey = "arangodb-response"
|
|
)
|
|
|
|
// ConnectionConfig provides all configuration options for a HTTP connection.
|
|
type ConnectionConfig struct {
|
|
// Endpoints holds 1 or more URL's used to connect to the database.
|
|
// In case of a connection to an ArangoDB cluster, you must provide the URL's of all coordinators.
|
|
Endpoints []string
|
|
// TLSConfig holds settings used to configure a TLS (HTTPS) connection.
|
|
// This is only used for endpoints using the HTTPS scheme.
|
|
TLSConfig *tls.Config
|
|
// Transport allows the use of a custom round tripper.
|
|
// If Transport is not of type `*http.Transport`, the `TLSConfig` property is not used.
|
|
// Otherwise a `TLSConfig` property other than `nil` will overwrite the `TLSClientConfig`
|
|
// property of `Transport`.
|
|
//
|
|
// When using a custom `http.Transport`, make sure to set the `MaxIdleConnsPerHost` field at least as
|
|
// high as the maximum number of concurrent requests you will make to your database.
|
|
// A lower number will cause the golang runtime to create additional connections and close them
|
|
// directly after use, resulting in a large number of connections in `TIME_WAIT` state.
|
|
// When this value is not set, the driver will set it to 64 automatically.
|
|
Transport http.RoundTripper
|
|
// DontFollowRedirect; if set, redirect will not be followed, response from the initial request will be returned without an error
|
|
// DontFollowRedirect takes precendance over FailOnRedirect.
|
|
DontFollowRedirect bool
|
|
// FailOnRedirect; if set, redirect will not be followed, instead the status code is returned as error
|
|
FailOnRedirect bool
|
|
// Cluster configuration settings
|
|
cluster.ConnectionConfig
|
|
// ContentType specified type of content encoding to use.
|
|
ContentType driver.ContentType
|
|
// ConnLimit is the upper limit to the number of connections to a single server.
|
|
// The default is 32 (DefaultConnLimit).
|
|
// Set this value to -1 if you do not want any upper limit.
|
|
ConnLimit int
|
|
}
|
|
|
|
// NewConnection creates a new HTTP connection based on the given configuration settings.
|
|
func NewConnection(config ConnectionConfig) (driver.Connection, error) {
|
|
c, err := cluster.NewConnection(config.ConnectionConfig, func(endpoint string) (driver.Connection, error) {
|
|
conn, err := newHTTPConnection(endpoint, config)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
return conn, nil
|
|
}, config.Endpoints)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// newHTTPConnection creates a new HTTP connection for a single endpoint and the remainder of the given configuration settings.
|
|
func newHTTPConnection(endpoint string, config ConnectionConfig) (driver.Connection, error) {
|
|
if config.ConnLimit == 0 {
|
|
config.ConnLimit = DefaultConnLimit
|
|
}
|
|
endpoint = util.FixupEndpointURLScheme(endpoint)
|
|
u, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
var httpTransport *http.Transport
|
|
if config.Transport != nil {
|
|
httpTransport, _ = config.Transport.(*http.Transport)
|
|
} else {
|
|
httpTransport = &http.Transport{
|
|
// Copy default values from http.DefaultTransport
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
DualStack: true,
|
|
}).DialContext,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
config.Transport = httpTransport
|
|
}
|
|
if httpTransport != nil {
|
|
if httpTransport.MaxIdleConnsPerHost == 0 {
|
|
// Raise the default number of idle connections per host since in a database application
|
|
// it is very likely that you want more than 2 concurrent connections to a host.
|
|
// We raise it to avoid the extra concurrent connections being closed directly
|
|
// after use, resulting in a lot of connection in `TIME_WAIT` state.
|
|
httpTransport.MaxIdleConnsPerHost = DefaultMaxIdleConnsPerHost
|
|
}
|
|
defaultMaxIdleConns := 3 * DefaultMaxIdleConnsPerHost
|
|
if httpTransport.MaxIdleConns > 0 && httpTransport.MaxIdleConns < defaultMaxIdleConns {
|
|
// For a cluster scenario we assume the use of 3 coordinators (don't know the exact number here)
|
|
// and derive the maximum total number of idle connections from that.
|
|
httpTransport.MaxIdleConns = defaultMaxIdleConns
|
|
}
|
|
if config.TLSConfig != nil {
|
|
httpTransport.TLSClientConfig = config.TLSConfig
|
|
}
|
|
}
|
|
httpClient := &http.Client{
|
|
Transport: config.Transport,
|
|
}
|
|
if config.DontFollowRedirect {
|
|
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse // Do not wrap, standard library will not understand
|
|
}
|
|
} else if config.FailOnRedirect {
|
|
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
return driver.ArangoError{
|
|
HasError: true,
|
|
Code: http.StatusFound,
|
|
ErrorNum: 0,
|
|
ErrorMessage: "Redirect not allowed",
|
|
}
|
|
}
|
|
}
|
|
var connPool chan int
|
|
if config.ConnLimit > 0 {
|
|
connPool = make(chan int, config.ConnLimit)
|
|
// Fill with available tokens
|
|
for i := 0; i < config.ConnLimit; i++ {
|
|
connPool <- i
|
|
}
|
|
}
|
|
c := &httpConnection{
|
|
endpoint: *u,
|
|
contentType: config.ContentType,
|
|
client: httpClient,
|
|
connPool: connPool,
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// httpConnection implements an HTTP + JSON connection to an arangodb server.
|
|
type httpConnection struct {
|
|
endpoint url.URL
|
|
contentType driver.ContentType
|
|
client *http.Client
|
|
connPool chan int
|
|
}
|
|
|
|
// String returns the endpoint as string
|
|
func (c *httpConnection) String() string {
|
|
return c.endpoint.String()
|
|
}
|
|
|
|
// NewRequest creates a new request with given method and path.
|
|
func (c *httpConnection) NewRequest(method, path string) (driver.Request, error) {
|
|
switch method {
|
|
case "GET", "POST", "DELETE", "HEAD", "PATCH", "PUT", "OPTIONS":
|
|
// Ok
|
|
default:
|
|
return nil, driver.WithStack(driver.InvalidArgumentError{Message: fmt.Sprintf("Invalid method '%s'", method)})
|
|
}
|
|
|
|
ct := c.contentType
|
|
if ct != driver.ContentTypeJSON && strings.Contains(path, "_api/gharial") {
|
|
// Currently (3.1.18) calls to this API do not work well with vpack.
|
|
ct = driver.ContentTypeJSON
|
|
}
|
|
|
|
r := &httpRequest{
|
|
method: method,
|
|
path: path,
|
|
}
|
|
|
|
switch ct {
|
|
case driver.ContentTypeJSON:
|
|
r.bodyBuilder = NewJsonBodyBuilder()
|
|
return r, nil
|
|
case driver.ContentTypeVelocypack:
|
|
r.bodyBuilder = NewVelocyPackBodyBuilder()
|
|
r.velocyPack = true
|
|
return r, nil
|
|
default:
|
|
return nil, driver.WithStack(fmt.Errorf("Unsupported content type %d", int(c.contentType)))
|
|
}
|
|
}
|
|
|
|
// Do performs a given request, returning its response.
|
|
func (c *httpConnection) Do(ctx context.Context, req driver.Request) (driver.Response, error) {
|
|
request, ok := req.(*httpRequest)
|
|
if !ok {
|
|
return nil, driver.WithStack(driver.InvalidArgumentError{Message: "request is not a httpRequest type"})
|
|
}
|
|
|
|
r, err := request.createHTTPRequest(c.endpoint)
|
|
rctx := ctx
|
|
if rctx == nil {
|
|
rctx = context.Background()
|
|
}
|
|
rctx = httptrace.WithClientTrace(rctx, &httptrace.ClientTrace{
|
|
WroteRequest: func(info httptrace.WroteRequestInfo) {
|
|
request.WroteRequest(info)
|
|
},
|
|
})
|
|
r = r.WithContext(rctx)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
|
|
// Block on too many concurrent connections
|
|
if c.connPool != nil {
|
|
select {
|
|
case t := <-c.connPool:
|
|
// Ok, we're allowed to continue
|
|
defer func() {
|
|
// Give back token
|
|
c.connPool <- t
|
|
}()
|
|
case <-rctx.Done():
|
|
// Context cancelled or expired
|
|
return nil, driver.WithStack(rctx.Err())
|
|
}
|
|
}
|
|
|
|
resp, err := c.client.Do(r)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
var rawResponse *[]byte
|
|
useRawResponse := false
|
|
if ctx != nil {
|
|
if v := ctx.Value(keyRawResponse); v != nil {
|
|
useRawResponse = true
|
|
if buf, ok := v.(*[]byte); ok {
|
|
rawResponse = buf
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read response body
|
|
body, err := readBody(resp)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
if rawResponse != nil {
|
|
*rawResponse = body
|
|
}
|
|
|
|
ct := resp.Header.Get("Content-Type")
|
|
var httpResp driver.Response
|
|
switch strings.Split(ct, ";")[0] {
|
|
case "application/json", "application/x-arango-dump":
|
|
httpResp = &httpJSONResponse{resp: resp, rawResponse: body}
|
|
case "application/x-velocypack":
|
|
httpResp = &httpVPackResponse{resp: resp, rawResponse: body}
|
|
default:
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
// When unauthorized the server sometimes return a `text/plain` response.
|
|
return nil, driver.WithStack(driver.ArangoError{
|
|
HasError: true,
|
|
Code: resp.StatusCode,
|
|
ErrorMessage: string(body),
|
|
})
|
|
}
|
|
// Handle empty 'text/plain' body as empty JSON object
|
|
if len(body) == 0 {
|
|
body = []byte("{}")
|
|
if rawResponse != nil {
|
|
*rawResponse = body
|
|
}
|
|
httpResp = &httpJSONResponse{resp: resp, rawResponse: body}
|
|
} else if useRawResponse {
|
|
httpResp = &httpJSONResponse{resp: resp, rawResponse: body}
|
|
} else {
|
|
return nil, driver.WithStack(fmt.Errorf("Unsupported content type '%s' with status %d and content '%s'", ct, resp.StatusCode, string(body)))
|
|
}
|
|
}
|
|
if ctx != nil {
|
|
if v := ctx.Value(keyResponse); v != nil {
|
|
if respPtr, ok := v.(*driver.Response); ok {
|
|
*respPtr = httpResp
|
|
}
|
|
}
|
|
}
|
|
return httpResp, nil
|
|
}
|
|
|
|
// readBody reads the body of the given response into a byte slice.
|
|
func readBody(resp *http.Response) ([]byte, error) {
|
|
defer resp.Body.Close()
|
|
contentLength := resp.ContentLength
|
|
if contentLength < 0 {
|
|
// Don't know the content length, do it the slowest way
|
|
result, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
return result, nil
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
if int64(int(contentLength)) == contentLength {
|
|
// contentLength is an int64. If we can safely cast to int, use Grow.
|
|
buf.Grow(int(contentLength))
|
|
}
|
|
if _, err := buf.ReadFrom(resp.Body); err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// Unmarshal unmarshals the given raw object into the given result interface.
|
|
func (c *httpConnection) Unmarshal(data driver.RawObject, result interface{}) error {
|
|
ct := c.contentType
|
|
if ct == driver.ContentTypeVelocypack && len(data) >= 2 {
|
|
// Poor mans auto detection of json
|
|
l := len(data)
|
|
if (data[0] == '{' && data[l-1] == '}') || (data[0] == '[' && data[l-1] == ']') {
|
|
ct = driver.ContentTypeJSON
|
|
}
|
|
}
|
|
switch ct {
|
|
case driver.ContentTypeJSON:
|
|
if err := json.Unmarshal(data, result); err != nil {
|
|
return driver.WithStack(err)
|
|
}
|
|
case driver.ContentTypeVelocypack:
|
|
//panic(velocypack.Slice(data))
|
|
if err := velocypack.Unmarshal(velocypack.Slice(data), result); err != nil {
|
|
return driver.WithStack(err)
|
|
}
|
|
default:
|
|
return driver.WithStack(fmt.Errorf("Unsupported content type %d", int(c.contentType)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Endpoints returns the endpoints used by this connection.
|
|
func (c *httpConnection) Endpoints() []string {
|
|
return []string{c.endpoint.String()}
|
|
}
|
|
|
|
// UpdateEndpoints reconfigures the connection to use the given endpoints.
|
|
func (c *httpConnection) UpdateEndpoints(endpoints []string) error {
|
|
// Do nothing here.
|
|
// The real updating is done in cluster Connection.
|
|
return nil
|
|
}
|
|
|
|
// SetAuthentication creates a copy of connection wrapper for given auth parameters.
|
|
func (c *httpConnection) SetAuthentication(auth driver.Authentication) (driver.Connection, error) {
|
|
var httpAuth httpAuthentication
|
|
switch auth.Type() {
|
|
case driver.AuthenticationTypeBasic:
|
|
userName := auth.Get("username")
|
|
password := auth.Get("password")
|
|
httpAuth = newBasicAuthentication(userName, password)
|
|
case driver.AuthenticationTypeJWT:
|
|
userName := auth.Get("username")
|
|
password := auth.Get("password")
|
|
httpAuth = newJWTAuthentication(userName, password)
|
|
case driver.AuthenticationTypeRaw:
|
|
value := auth.Get("value")
|
|
httpAuth = newRawAuthentication(value)
|
|
default:
|
|
return nil, driver.WithStack(fmt.Errorf("Unsupported authentication type %d", int(auth.Type())))
|
|
}
|
|
|
|
result, err := newAuthenticatedConnection(c, httpAuth)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Protocols returns all protocols used by this connection.
|
|
func (c *httpConnection) Protocols() driver.ProtocolSet {
|
|
return driver.ProtocolSet{driver.ProtocolHTTP}
|
|
}
|
|
|
|
// RequestRepeater creates possibility to send the request many times.
|
|
type RequestRepeater interface {
|
|
Repeat(conn driver.Connection, resp driver.Response, err error) bool
|
|
}
|
|
|
|
// RepeatConnection is responsible for sending request until request repeater gives up.
|
|
type RepeatConnection struct {
|
|
mutex sync.Mutex
|
|
auth driver.Authentication
|
|
conn driver.Connection
|
|
repeat RequestRepeater
|
|
}
|
|
|
|
func NewRepeatConnection(conn driver.Connection, repeat RequestRepeater) driver.Connection {
|
|
return &RepeatConnection{
|
|
conn: conn,
|
|
repeat: repeat,
|
|
}
|
|
}
|
|
|
|
// NewRequest creates a new request with given method and path.
|
|
func (h *RepeatConnection) NewRequest(method, path string) (driver.Request, error) {
|
|
return h.conn.NewRequest(method, path)
|
|
}
|
|
|
|
// Do performs a given request, returning its response. Repeats requests until repeat function gives up.
|
|
func (h *RepeatConnection) Do(ctx context.Context, req driver.Request) (driver.Response, error) {
|
|
for {
|
|
resp, err := h.conn.Do(ctx, req.Clone())
|
|
|
|
if !h.repeat.Repeat(h, resp, err) {
|
|
return resp, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unmarshal unmarshals the given raw object into the given result interface.
|
|
func (h *RepeatConnection) Unmarshal(data driver.RawObject, result interface{}) error {
|
|
return h.conn.Unmarshal(data, result)
|
|
}
|
|
|
|
// Endpoints returns the endpoints used by this connection.
|
|
func (h *RepeatConnection) Endpoints() []string {
|
|
return h.conn.Endpoints()
|
|
}
|
|
|
|
// UpdateEndpoints reconfigures the connection to use the given endpoints.
|
|
func (h *RepeatConnection) UpdateEndpoints(endpoints []string) error {
|
|
return h.conn.UpdateEndpoints(endpoints)
|
|
}
|
|
|
|
// SetAuthentication configure the authentication used for this connection.
|
|
// Returns ErrAuthenticationNotChanged when the authentication is not changed.
|
|
func (h *RepeatConnection) SetAuthentication(authentication driver.Authentication) (driver.Connection, error) {
|
|
h.mutex.Lock()
|
|
defer h.mutex.Unlock()
|
|
|
|
if IsAuthenticationTheSame(h.auth, authentication) {
|
|
return h, ErrAuthenticationNotChanged
|
|
}
|
|
|
|
newConn, err := h.conn.SetAuthentication(authentication)
|
|
if err != nil {
|
|
return nil, driver.WithStack(err)
|
|
}
|
|
h.conn = newConn
|
|
h.auth = authentication
|
|
|
|
return h, nil
|
|
}
|
|
|
|
// Protocols returns all protocols used by this connection.
|
|
func (h *RepeatConnection) Protocols() driver.ProtocolSet {
|
|
return h.conn.Protocols()
|
|
}
|
|
|