// ------------------------------------------------------------ // 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 }