Browse Source

http2/h2c: add h2c implementation (unencrypted HTTP/2)

Implements h2c by leveraging the existing http2.Server by implementing the 2
ways to start an h2c connection as described in RFC 7540, which are: (1)
create a connection starting with HTTP/1 and then upgrading to h2c [Section 3.2]
and (2) starting a connection directly speaking h2c (aka starting with prior
knowledge) [Section 3.4].

For both of the above connection methods the implementation strategy is to
hijack a HTTP/1 connection, perform the h2c connection on the hijacked
net.Conn, and create a suitably configured net.Conn to pass into
http2.Server.ServeConn.

For h2c with prior knowledge this is relatively simple. For that we just have
to verify the HTTP/2 client preface has been written to the net.Conn, and
then reforward the client preface to the hijacked connection.

For h2c upgraded from HTTP/1, this is a bit more involved. First we validate
the HTTP/1 Upgrade request, and respond to the client with 101 Switching
Protocols. Then we write a HTTP/2 client preface on behalf of the client,
and a settings frame and a headers frame which correspond to what was in
the upgrade request. Then since http2.Server is going respond with a
settings ACK, we swallow it so that it is not forwarded to the client since
for h2c upgrade from HTTP/1 the 101 Switching Protocols response replaces
the settins ACK.

Fixes golang/go#14141

Change-Id: I435f40216c883809c0dcb75426c9c59e2ea31182
Reviewed-on: https://go-review.googlesource.com/112999
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
William Chang 7 years ago
parent
commit
c4299a1a0d
2 changed files with 550 additions and 0 deletions
  1. 492 0
      http2/h2c/h2c.go
  2. 58 0
      http2/h2c/h2c_test.go

+ 492 - 0
http2/h2c/h2c.go

@@ -0,0 +1,492 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package h2c implements the unencrypted "h2c" form of HTTP/2.
+//
+// The h2c protocol is the non-TLS version of HTTP/2 which is not available from
+// net/http or golang.org/x/net/http2.
+package h2c
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/base64"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"net/http"
+	"net/textproto"
+	"os"
+	"strings"
+
+	"golang.org/x/net/http/httpguts"
+	"golang.org/x/net/http2"
+	"golang.org/x/net/http2/hpack"
+)
+
+var (
+	http2VerboseLogs bool
+)
+
+func init() {
+	e := os.Getenv("GODEBUG")
+	if strings.Contains(e, "http2debug=1") || strings.Contains(e, "http2debug=2") {
+		http2VerboseLogs = true
+	}
+}
+
+// h2cHandler is a Handler which implements h2c by hijacking the HTTP/1 traffic
+// that should be h2c traffic. There are two ways to begin a h2c connection
+// (RFC 7540 Section 3.2 and 3.4): (1) Starting with Prior Knowledge - this
+// works by starting an h2c connection with a string of bytes that is valid
+// HTTP/1, but unlikely to occur in practice and (2) Upgrading from HTTP/1 to
+// h2c - this works by using the HTTP/1 Upgrade header to request an upgrade to
+// h2c. When either of those situations occur we hijack the HTTP/1 connection,
+// convert it to a HTTP/2 connection and pass the net.Conn to http2.ServeConn.
+type h2cHandler struct {
+	Handler http.Handler
+	s       *http2.Server
+}
+
+// NewHandler returns an http.Handler that wraps h, intercepting any h2c
+// traffic. If a request is an h2c connection, it's hijacked and redirected to
+// s.ServeConn. Otherwise the returned Handler just forwards requests to h. This
+// works because h2c is designed to be parseable as valid HTTP/1, but ignored by
+// any HTTP server that does not handle h2c. Therefore we leverage the HTTP/1
+// compatible parts of the Go http library to parse and recognize h2c requests.
+// Once a request is recognized as h2c, we hijack the connection and convert it
+// to an HTTP/2 connection which is understandable to s.ServeConn. (s.ServeConn
+// understands HTTP/2 except for the h2c part of it.)
+func NewHandler(h http.Handler, s *http2.Server) http.Handler {
+	return &h2cHandler{
+		Handler: h,
+		s:       s,
+	}
+}
+
+// ServeHTTP implement the h2c support that is enabled by h2c.GetH2CHandler.
+func (s h2cHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// Handle h2c with prior knowledge (RFC 7540 Section 3.4)
+	if r.Method == "PRI" && len(r.Header) == 0 && r.URL.Path == "*" && r.Proto == "HTTP/2.0" {
+		if http2VerboseLogs {
+			log.Print("h2c: attempting h2c with prior knowledge.")
+		}
+		conn, err := initH2CWithPriorKnowledge(w)
+		if err != nil {
+			if http2VerboseLogs {
+				log.Printf("h2c: error h2c with prior knowledge: %v", err)
+			}
+			return
+		}
+		defer conn.Close()
+
+		s.s.ServeConn(conn, &http2.ServeConnOpts{Handler: s.Handler})
+		return
+	}
+	// Handle Upgrade to h2c (RFC 7540 Section 3.2)
+	if conn, err := h2cUpgrade(w, r); err == nil {
+		defer conn.Close()
+
+		s.s.ServeConn(conn, &http2.ServeConnOpts{Handler: s.Handler})
+		return
+	}
+
+	s.Handler.ServeHTTP(w, r)
+	return
+}
+
+// initH2CWithPriorKnowledge implements creating a h2c connection with prior
+// knowledge (Section 3.4) and creates a net.Conn suitable for http2.ServeConn.
+// All we have to do is look for the client preface that is suppose to be part
+// of the body, and reforward the client preface on the net.Conn this function
+// creates.
+func initH2CWithPriorKnowledge(w http.ResponseWriter) (net.Conn, error) {
+	hijacker, ok := w.(http.Hijacker)
+	if !ok {
+		panic("Hijack not supported.")
+	}
+	conn, rw, err := hijacker.Hijack()
+	if err != nil {
+		panic(fmt.Sprintf("Hijack failed: %v", err))
+	}
+
+	const expectedBody = "SM\r\n\r\n"
+
+	buf := make([]byte, len(expectedBody))
+	n, err := io.ReadFull(rw, buf)
+
+	if string(buf[:n]) == expectedBody {
+		c := &rwConn{
+			Conn:      conn,
+			Reader:    io.MultiReader(strings.NewReader(http2.ClientPreface), rw),
+			BufWriter: rw.Writer,
+		}
+		return c, nil
+	}
+
+	conn.Close()
+	if http2VerboseLogs {
+		log.Printf(
+			"h2c: missing the request body portion of the client preface. Wanted: %v Got: %v",
+			[]byte(expectedBody),
+			buf[0:n],
+		)
+	}
+	return nil, errors.New("invalid client preface")
+}
+
+// drainClientPreface reads a single instance of the HTTP/2 client preface from
+// the supplied reader.
+func drainClientPreface(r io.Reader) error {
+	var buf bytes.Buffer
+	prefaceLen := int64(len(http2.ClientPreface))
+	n, err := io.CopyN(&buf, r, prefaceLen)
+	if err != nil {
+		return err
+	}
+	if n != prefaceLen || buf.String() != http2.ClientPreface {
+		return fmt.Errorf("Client never sent: %s", http2.ClientPreface)
+	}
+	return nil
+}
+
+// h2cUpgrade establishes a h2c connection using the HTTP/1 upgrade (Section 3.2).
+func h2cUpgrade(w http.ResponseWriter, r *http.Request) (net.Conn, error) {
+	if !isH2CUpgrade(r.Header) {
+		return nil, errors.New("non-conforming h2c headers")
+	}
+
+	// Initial bytes we put into conn to fool http2 server
+	initBytes, _, err := convertH1ReqToH2(r)
+	if err != nil {
+		return nil, err
+	}
+
+	hijacker, ok := w.(http.Hijacker)
+	if !ok {
+		return nil, errors.New("hijack not supported.")
+	}
+	conn, rw, err := hijacker.Hijack()
+	if err != nil {
+		return nil, fmt.Errorf("hijack failed: %v", err)
+	}
+
+	rw.Write([]byte("HTTP/1.1 101 Switching Protocols\r\n" +
+		"Connection: Upgrade\r\n" +
+		"Upgrade: h2c\r\n\r\n"))
+	rw.Flush()
+
+	// A conforming client will now send an H2 client preface which need to drain
+	// since we already sent this.
+	if err := drainClientPreface(rw); err != nil {
+		return nil, err
+	}
+
+	c := &rwConn{
+		Conn:      conn,
+		Reader:    io.MultiReader(initBytes, rw),
+		BufWriter: newSettingsAckSwallowWriter(rw.Writer),
+	}
+	return c, nil
+}
+
+// convert the data contained in the HTTP/1 upgrade request into the HTTP/2
+// version in byte form.
+func convertH1ReqToH2(r *http.Request) (*bytes.Buffer, []http2.Setting, error) {
+	h2Bytes := bytes.NewBuffer([]byte((http2.ClientPreface)))
+	framer := http2.NewFramer(h2Bytes, nil)
+	settings, err := getH2Settings(r.Header)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if err := framer.WriteSettings(settings...); err != nil {
+		return nil, nil, err
+	}
+
+	headerBytes, err := getH2HeaderBytes(r, getMaxHeaderTableSize(settings))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	maxFrameSize := int(getMaxFrameSize(settings))
+	needOneHeader := len(headerBytes) < maxFrameSize
+	err = framer.WriteHeaders(http2.HeadersFrameParam{
+		StreamID:      1,
+		BlockFragment: headerBytes,
+		EndHeaders:    needOneHeader,
+	})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	for i := maxFrameSize; i < len(headerBytes); i += maxFrameSize {
+		if len(headerBytes)-i > maxFrameSize {
+			if err := framer.WriteContinuation(1,
+				false, // endHeaders
+				headerBytes[i:maxFrameSize]); err != nil {
+				return nil, nil, err
+			}
+		} else {
+			if err := framer.WriteContinuation(1,
+				true, // endHeaders
+				headerBytes[i:]); err != nil {
+				return nil, nil, err
+			}
+		}
+	}
+
+	return h2Bytes, settings, nil
+}
+
+// getMaxFrameSize returns the SETTINGS_MAX_FRAME_SIZE. If not present default
+// value is 16384 as specified by RFC 7540 Section 6.5.2.
+func getMaxFrameSize(settings []http2.Setting) uint32 {
+	for _, setting := range settings {
+		if setting.ID == http2.SettingMaxFrameSize {
+			return setting.Val
+		}
+	}
+	return 16384
+}
+
+// getMaxHeaderTableSize returns the SETTINGS_HEADER_TABLE_SIZE. If not present
+// default value is 4096 as specified by RFC 7540 Section 6.5.2.
+func getMaxHeaderTableSize(settings []http2.Setting) uint32 {
+	for _, setting := range settings {
+		if setting.ID == http2.SettingHeaderTableSize {
+			return setting.Val
+		}
+	}
+	return 4096
+}
+
+// bufWriter is a Writer interface that also has a Flush method.
+type bufWriter interface {
+	io.Writer
+	Flush() error
+}
+
+// rwConn implements net.Conn but overrides Read and Write so that reads and
+// writes are forwarded to the provided io.Reader and bufWriter.
+type rwConn struct {
+	net.Conn
+	io.Reader
+	BufWriter bufWriter
+}
+
+// Read forwards reads to the underlying Reader.
+func (c *rwConn) Read(p []byte) (int, error) {
+	return c.Reader.Read(p)
+}
+
+// Write forwards writes to the underlying bufWriter and immediately flushes.
+func (c *rwConn) Write(p []byte) (int, error) {
+	n, err := c.BufWriter.Write(p)
+	if err := c.BufWriter.Flush(); err != nil {
+		return 0, err
+	}
+	return n, err
+}
+
+// settingsAckSwallowWriter is a writer that normally forwards bytes to it's
+// underlying Writer, but swallows the first SettingsAck frame that it sees.
+type settingsAckSwallowWriter struct {
+	Writer     *bufio.Writer
+	buf        []byte
+	didSwallow bool
+}
+
+// newSettingsAckSwallowWriter returns a new settingsAckSwallowWriter.
+func newSettingsAckSwallowWriter(w *bufio.Writer) *settingsAckSwallowWriter {
+	return &settingsAckSwallowWriter{
+		Writer:     w,
+		buf:        make([]byte, 0),
+		didSwallow: false,
+	}
+}
+
+// Write implements io.Writer interface. Normally forwards bytes to w.Writer,
+// except for the first Settings ACK frame that it sees.
+func (w *settingsAckSwallowWriter) Write(p []byte) (int, error) {
+	if !w.didSwallow {
+		w.buf = append(w.buf, p...)
+		// Process all the frames we have collected into w.buf
+		for {
+			// Append until we get full frame header which is 9 bytes
+			if len(w.buf) < 9 {
+				break
+			}
+			// Check if we have collected a whole frame.
+			fh, err := http2.ReadFrameHeader(bytes.NewBuffer(w.buf))
+			if err != nil {
+				// Corrupted frame, fail current Write
+				return 0, err
+			}
+			fSize := fh.Length + 9
+			if uint32(len(w.buf)) < fSize {
+				// Have not collected whole frame. Stop processing buf, and withold on
+				// forward bytes to w.Writer until we get the full frame.
+				break
+			}
+
+			// We have now collected a whole frame.
+			if fh.Type == http2.FrameSettings && fh.Flags.Has(http2.FlagSettingsAck) {
+				// If Settings ACK frame, do not forward to underlying writer, remove
+				// bytes from w.buf, and record that we have swallowed Settings Ack
+				// frame.
+				w.didSwallow = true
+				w.buf = w.buf[fSize:]
+				continue
+			}
+
+			// Not settings ack frame. Forward bytes to w.Writer.
+			if _, err := w.Writer.Write(w.buf[:fSize]); err != nil {
+				// Couldn't forward bytes. Fail current Write.
+				return 0, err
+			}
+			w.buf = w.buf[fSize:]
+		}
+		return len(p), nil
+	}
+	return w.Writer.Write(p)
+}
+
+// Flush calls w.Writer.Flush.
+func (w *settingsAckSwallowWriter) Flush() error {
+	return w.Writer.Flush()
+}
+
+// isH2CUpgrade returns true if the header properly request an upgrade to h2c
+// as specified by Section 3.2.
+func isH2CUpgrade(h http.Header) bool {
+	return httpguts.HeaderValuesContainsToken(h[textproto.CanonicalMIMEHeaderKey("Upgrade")], "h2c") &&
+		httpguts.HeaderValuesContainsToken(h[textproto.CanonicalMIMEHeaderKey("Connection")], "HTTP2-Settings")
+}
+
+// getH2Settings returns the []http2.Setting that are encoded in the
+// HTTP2-Settings header.
+func getH2Settings(h http.Header) ([]http2.Setting, error) {
+	vals, ok := h[textproto.CanonicalMIMEHeaderKey("HTTP2-Settings")]
+	if !ok {
+		return nil, errors.New("missing HTTP2-Settings header")
+	}
+	if len(vals) != 1 {
+		return nil, fmt.Errorf("expected 1 HTTP2-Settings. Got: %v", vals)
+	}
+	settings, err := decodeSettings(vals[0])
+	if err != nil {
+		return nil, fmt.Errorf("Invalid HTTP2-Settings: %q", vals[0])
+	}
+	return settings, nil
+}
+
+// decodeSettings decodes the base64url header value of the HTTP2-Settings
+// header. RFC 7540 Section 3.2.1.
+func decodeSettings(headerVal string) ([]http2.Setting, error) {
+	b, err := base64.RawURLEncoding.DecodeString(headerVal)
+	if err != nil {
+		return nil, err
+	}
+	if len(b)%6 != 0 {
+		return nil, err
+	}
+	settings := make([]http2.Setting, 0)
+	for i := 0; i < len(b)/6; i++ {
+		settings = append(settings, http2.Setting{
+			ID:  http2.SettingID(binary.BigEndian.Uint16(b[i*6 : i*6+2])),
+			Val: binary.BigEndian.Uint32(b[i*6+2 : i*6+6]),
+		})
+	}
+
+	return settings, nil
+}
+
+// getH2HeaderBytes return the headers in r a []bytes encoded by HPACK.
+func getH2HeaderBytes(r *http.Request, maxHeaderTableSize uint32) ([]byte, error) {
+	headerBytes := bytes.NewBuffer(nil)
+	hpackEnc := hpack.NewEncoder(headerBytes)
+	hpackEnc.SetMaxDynamicTableSize(maxHeaderTableSize)
+
+	// Section 8.1.2.3
+	err := hpackEnc.WriteField(hpack.HeaderField{
+		Name:  ":method",
+		Value: r.Method,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = hpackEnc.WriteField(hpack.HeaderField{
+		Name:  ":scheme",
+		Value: "http",
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = hpackEnc.WriteField(hpack.HeaderField{
+		Name:  ":authority",
+		Value: r.Host,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	path := r.URL.Path
+	if r.URL.RawQuery != "" {
+		path = strings.Join([]string{path, r.URL.RawQuery}, "?")
+	}
+	err = hpackEnc.WriteField(hpack.HeaderField{
+		Name:  ":path",
+		Value: path,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO Implement Section 8.3
+
+	for header, values := range r.Header {
+		// Skip non h2 headers
+		if isNonH2Header(header) {
+			continue
+		}
+		for _, v := range values {
+			err := hpackEnc.WriteField(hpack.HeaderField{
+				Name:  strings.ToLower(header),
+				Value: v,
+			})
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	return headerBytes.Bytes(), nil
+}
+
+// Connection specific headers listed in RFC 7540 Section 8.1.2.2 that are not
+// suppose to be transferred to HTTP/2. The Http2-Settings header is skipped
+// since already use to create the HTTP/2 SETTINGS frame.
+var nonH2Headers = []string{
+	"Connection",
+	"Keep-Alive",
+	"Proxy-Connection",
+	"Transfer-Encoding",
+	"Upgrade",
+	"Http2-Settings",
+}
+
+// isNonH2Header returns true if header should not be transferred to HTTP/2.
+func isNonH2Header(header string) bool {
+	for _, nonH2h := range nonH2Headers {
+		if header == nonH2h {
+			return true
+		}
+	}
+	return false
+}

+ 58 - 0
http2/h2c/h2c_test.go

@@ -0,0 +1,58 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package h2c
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"log"
+	"net/http"
+	"testing"
+
+	"golang.org/x/net/http2"
+)
+
+func TestSettingsAckSwallowWriter(t *testing.T) {
+	var buf bytes.Buffer
+	swallower := newSettingsAckSwallowWriter(bufio.NewWriter(&buf))
+	fw := http2.NewFramer(swallower, nil)
+	fw.WriteSettings(http2.Setting{http2.SettingMaxFrameSize, 2})
+	fw.WriteSettingsAck()
+	fw.WriteData(1, true, []byte{})
+	swallower.Flush()
+
+	fr := http2.NewFramer(nil, bufio.NewReader(&buf))
+
+	f, err := fr.ReadFrame()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if f.Header().Type != http2.FrameSettings {
+		t.Fatalf("Expected first frame to be SETTINGS. Got: %v", f.Header().Type)
+	}
+
+	f, err = fr.ReadFrame()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if f.Header().Type != http2.FrameData {
+		t.Fatalf("Expected first frame to be DATA. Got: %v", f.Header().Type)
+	}
+}
+
+func ExampleNewHandler() {
+	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprint(w, "Hello world")
+	})
+	h2s := &http2.Server{
+		// ...
+	}
+	h1s := &http.Server{
+		Addr:    ":8080",
+		Handler: NewHandler(handler, h2s),
+	}
+	log.Fatal(h1s.ListenAndServe())
+}