api/server/httputils: add ReadJSON() utility and fix handling of invalid JSON
Implement a `httputils.ReadJSON()` utility to help reduce some code-duplication, and to make sure we handle JSON requests consistently (e.g. always check for the content-type).

Differences compared to current handling:

- prevent possible panic if request.Body is nil ("should never happen")
- always require Content-Type to be "application/json"
- be stricter about additional content after JSON (previously ignored)
- close body after reading (some code did this)

We should consider to add a "max body size" on this function, similar to
https://github.com/moby/moby/blob/7b9275c0da707b030e62c96b679a976f31f929d3/api/server/middleware/debug.go#L27-L40

In addition to the changes above, two commits are included in this PR that:

- api/server/httputils: matchesContentType(): return error instead of logging
- api/server/httputils: move WriteJSON() together with ReadJSON()



**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->

```
API: Improve validation of invalid JSON requests
```


**- A picture of a cute animal (not mandatory but encouraged)**


api/server/httputils/httputils.go
package httputils // import "github.com/docker/docker/api/server/httputils"

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

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

// 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 == "" {
		if r.Body == nil || r.ContentLength == 0 {
			return nil
		}
	}

	// Otherwise it better be json
	if matchesContentType(ct, "application/json") {
		return nil
	}
	return errdefs.InvalidParameter(errors.Errorf("Content-Type specified (%s) must be 'application/json'", ct))
}

// 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) bool {
	mimetype, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		logrus.Errorf("Error parsing media type: %s error: %v", contentType, err)
	}
	return err == nil && mimetype == expectedType
}

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
}

Right, so this is the tricky one; previously, this check wasn't there (in existing code), and it would either `panic` (if `.Body` was `nil` - although in practice that would not happen), or fall through to the code below, which would mark the request as "invalid" because the body was empty.

There's a test failing because of this change, as it expected it to fail early, but now falls through, producing a different error;

```
=== RUN   TestContainerInvalidJSON//containers/foobar/exec/no_body
=== CONT  TestContainerInvalidJSON//containers/foobar/exec/no_body
    container_test.go:42: assertion failed: string "{\"message\":\"No exec command specified\"}\n" does not contain "invalid JSON: got EOF while reading request body"
        --- FAIL: TestContainerInvalidJSON//containers/foobar/exec/no_body (0.00s)

=== RUN   TestContainerInvalidJSON//exec/foobar/start/no_body
=== CONT  TestContainerInvalidJSON//exec/foobar/start/no_body
    container_test.go:38: assertion failed: 404 (res.StatusCode int) != 400 (http.StatusBadRequest int)
        --- FAIL: TestContainerInvalidJSON//exec/foobar/start/no_body (0.00s)
```

I think _technically_ the new handling is correct; an empty body is not an invalid request (irregardless of the content-type or request type (`POST`, `PUT`, `PATCH`, `DELETE`), but I'll have to update the test to make it pass 