components-contrib/bindings/http/http.go

169 lines
4.1 KiB
Go

// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation and Dapr Contributors.
// Licensed under the MIT License.
// ------------------------------------------------------------
package http
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"time"
"unicode"
"github.com/mitchellh/mapstructure"
"github.com/dapr/components-contrib/bindings"
"github.com/dapr/kit/logger"
)
// HTTPSource is a binding for an http url endpoint invocation
// nolint:golint
type HTTPSource struct {
metadata httpMetadata
client *http.Client
logger logger.Logger
}
type httpMetadata struct {
URL string `mapstructure:"url"`
}
// NewHTTP returns a new HTTPSource.
func NewHTTP(logger logger.Logger) *HTTPSource {
return &HTTPSource{logger: logger}
}
// Init performs metadata parsing.
func (h *HTTPSource) Init(metadata bindings.Metadata) error {
if err := mapstructure.Decode(metadata.Properties, &h.metadata); err != nil {
return err
}
// See guidance on proper HTTP client settings here:
// https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
dialer := &net.Dialer{
Timeout: 5 * time.Second,
}
netTransport := &http.Transport{
Dial: dialer.Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
h.client = &http.Client{
Timeout: time.Second * 10,
Transport: netTransport,
}
return nil
}
// Operations returns the supported operations for this binding.
func (h *HTTPSource) Operations() []bindings.OperationKind {
return []bindings.OperationKind{
bindings.CreateOperation, // For backward compatibility
"get",
"head",
"post",
"put",
"patch",
"delete",
"options",
"trace",
}
}
// Invoke performs an HTTP request to the configured HTTP endpoint.
func (h *HTTPSource) Invoke(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
u := h.metadata.URL
if req.Metadata != nil {
if path, ok := req.Metadata["path"]; ok {
// Simplicity and no "../../.." type exploits.
u = fmt.Sprintf("%s/%s", strings.TrimRight(u, "/"), strings.TrimLeft(path, "/"))
if strings.Contains(u, "..") {
return nil, fmt.Errorf("invalid path: %s", path)
}
}
}
var body io.Reader
method := strings.ToUpper(string(req.Operation))
// For backward compatibility
if method == "CREATE" {
method = "POST"
}
switch method {
case "PUT", "POST", "PATCH":
body = bytes.NewBuffer(req.Data)
case "GET", "HEAD", "DELETE", "OPTIONS", "TRACE":
default:
return nil, fmt.Errorf("invalid operation: %s", req.Operation)
}
// nolint: noctx
request, err := http.NewRequest(method, u, body)
if err != nil {
return nil, err
}
// Set default values for Content-Type and Accept headers.
if body != nil {
if _, ok := req.Metadata["Content-Type"]; !ok {
request.Header.Set("Content-Type", "application/json; charset=utf-8")
}
}
if _, ok := req.Metadata["Accept"]; !ok {
request.Header.Set("Accept", "application/json; charset=utf-8")
}
// Any metadata keys that start with a capital letter
// are treated as request headers
for mdKey, mdValue := range req.Metadata {
keyAsRunes := []rune(mdKey)
if len(keyAsRunes) > 0 && unicode.IsUpper(keyAsRunes[0]) {
request.Header.Set(mdKey, mdValue)
}
}
// Send the question
resp, err := h.client.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Read the response body. For empty responses (e.g. 204 No Content)
// `b` will be an empty slice.
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
metadata := make(map[string]string, len(resp.Header)+2)
// Include status code & desc
metadata["statusCode"] = strconv.Itoa(resp.StatusCode)
metadata["status"] = resp.Status
// Response headers are mapped from `map[string][]string` to `map[string]string`
// where headers with multiple values are delimited with ", ".
for key, values := range resp.Header {
metadata[key] = strings.Join(values, ", ")
}
// Create an error for non-200 status codes.
if resp.StatusCode/100 != 2 {
err = fmt.Errorf("received status code %d", resp.StatusCode)
}
return &bindings.InvokeResponse{
Data: b,
Metadata: metadata,
}, err
}