dotcloud/docker

View on GitHub
api/server/httputils/httputils.go

Summary

Maintainability
A
40 mins
Test Coverage
package httputils // import "github.com/docker/docker/api/server/httputils"

import (
    "context"
    "encoding/json"
    "io"
    "mime"
    "net/http"
    "strings"

    "github.com/docker/docker/errdefs"
    "github.com/pkg/errors"
)

// APIVersionKey is the client's requested API version.
type APIVersionKey struct{}

// APIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints.
// Any function that has the appropriate signature can be registered as an API endpoint (e.g. getVersion).
type APIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error

// HijackConnection interrupts the http response writer to get the
// underlying connection and operate with it.
func HijackConnection(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
    conn, _, err := w.(http.Hijacker).Hijack()
    if err != nil {
        return nil, nil, err
    }
    // Flush the options to make sure the client sets the raw mode
    _, _ = conn.Write([]byte{})
    return conn, conn, nil
}

// CloseStreams ensures that a list for http streams are properly closed.
func CloseStreams(streams ...interface{}) {
    for _, stream := range streams {
        if tcpc, ok := stream.(interface {
            CloseWrite() error
        }); ok {
            _ = tcpc.CloseWrite()
        } else if closer, ok := stream.(io.Closer); ok {
            _ = closer.Close()
        }
    }
}

// CheckForJSON makes sure that the request's Content-Type is application/json.
func CheckForJSON(r *http.Request) error {
    ct := r.Header.Get("Content-Type")

    // No Content-Type header is ok as long as there's no Body
    if ct == "" && (r.Body == nil || r.ContentLength == 0) {
        return nil
    }

    // Otherwise it better be json
    return matchesContentType(ct, "application/json")
}

// ReadJSON validates the request to have the correct content-type, and decodes
// the request's Body into out.
func ReadJSON(r *http.Request, out interface{}) error {
    err := CheckForJSON(r)
    if err != nil {
        return err
    }
    if r.Body == nil || r.ContentLength == 0 {
        // an empty body is not invalid, so don't return an error; see
        // https://lists.w3.org/Archives/Public/ietf-http-wg/2010JulSep/0272.html
        return nil
    }

    dec := json.NewDecoder(r.Body)
    err = dec.Decode(out)
    defer r.Body.Close()
    if err != nil {
        if err == io.EOF {
            return errdefs.InvalidParameter(errors.New("invalid JSON: got EOF while reading request body"))
        }
        return errdefs.InvalidParameter(errors.Wrap(err, "invalid JSON"))
    }

    if dec.More() {
        return errdefs.InvalidParameter(errors.New("unexpected content after JSON"))
    }
    return nil
}

// WriteJSON writes the value v to the http response stream as json with standard json encoding.
func WriteJSON(w http.ResponseWriter, code int, v interface{}) error {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    enc := json.NewEncoder(w)
    enc.SetEscapeHTML(false)
    return enc.Encode(v)
}

// ParseForm ensures the request form is parsed even with invalid content types.
// If we don't do this, POST method without Content-type (even with empty body) will fail.
func ParseForm(r *http.Request) error {
    if r == nil {
        return nil
    }
    if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
        return errdefs.InvalidParameter(err)
    }
    return nil
}

// VersionFromContext returns an API version from the context using APIVersionKey.
// It panics if the context value does not have version.Version type.
func VersionFromContext(ctx context.Context) string {
    if ctx == nil {
        return ""
    }

    if val := ctx.Value(APIVersionKey{}); val != nil {
        return val.(string)
    }

    return ""
}

// matchesContentType validates the content type against the expected one
func matchesContentType(contentType, expectedType string) error {
    mimetype, _, err := mime.ParseMediaType(contentType)
    if err != nil {
        return errdefs.InvalidParameter(errors.Wrapf(err, "malformed Content-Type header (%s)", contentType))
    }
    if mimetype != expectedType {
        return errdefs.InvalidParameter(errors.Errorf("unsupported Content-Type header (%s): must be '%s'", contentType, expectedType))
    }
    return nil
}