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