Commit c46f265c authored by Tom Bergan's avatar Tom Bergan Committed by Brad Fitzpatrick

http2: implement support for server push

This makes x/net/http2's ResponseWriter implement the new interface,
http.Pusher. This new interface requires Go 1.8. When compiled against
older versions of Go, the ResponseWriter does not have a Push method.

Fixes golang/go#13443

Change-Id: I8486ffe4bb5562a94270ace21e90e8c9a4653da0
Reviewed-on: https://go-review.googlesource.com/29439Reviewed-by: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
parent 65dfc087
...@@ -6,6 +6,21 @@ ...@@ -6,6 +6,21 @@
package http2 package http2
import "crypto/tls" import (
"crypto/tls"
"net/http"
)
func cloneTLSConfig(c *tls.Config) *tls.Config { return c.Clone() } func cloneTLSConfig(c *tls.Config) *tls.Config { return c.Clone() }
var _ http.Pusher = (*responseWriter)(nil)
// Push implements http.Pusher.
func (w *responseWriter) Push(target string, opts *http.PushOptions) error {
internalOpts := pushOptions{}
if opts != nil {
internalOpts.Method = opts.Method
internalOpts.Header = opts.Header
}
return w.push(target, internalOpts)
}
...@@ -78,13 +78,23 @@ var ( ...@@ -78,13 +78,23 @@ var (
type streamState int type streamState int
// HTTP/2 stream states.
//
// See http://tools.ietf.org/html/rfc7540#section-5.1.
//
// For simplicity, the server code merges "reserved (local)" into
// "half-closed (remote)". This is one less state transition to track.
// The only downside is that we send PUSH_PROMISEs slightly less
// liberally than allowable. More discussion here:
// https://lists.w3.org/Archives/Public/ietf-http-wg/2016JulSep/0599.html
//
// "reserved (remote)" is omitted since the client code does not
// support server push.
const ( const (
stateIdle streamState = iota stateIdle streamState = iota
stateOpen stateOpen
stateHalfClosedLocal stateHalfClosedLocal
stateHalfClosedRemote stateHalfClosedRemote
stateResvLocal
stateResvRemote
stateClosed stateClosed
) )
...@@ -93,8 +103,6 @@ var stateName = [...]string{ ...@@ -93,8 +103,6 @@ var stateName = [...]string{
stateOpen: "Open", stateOpen: "Open",
stateHalfClosedLocal: "HalfClosedLocal", stateHalfClosedLocal: "HalfClosedLocal",
stateHalfClosedRemote: "HalfClosedRemote", stateHalfClosedRemote: "HalfClosedRemote",
stateResvLocal: "ResvLocal",
stateResvRemote: "ResvRemote",
stateClosed: "Closed", stateClosed: "Closed",
} }
......
This diff is collapsed.
This diff is collapsed.
...@@ -287,37 +287,42 @@ func (st *serverTester) encodeHeaderRaw(headers ...string) []byte { ...@@ -287,37 +287,42 @@ func (st *serverTester) encodeHeaderRaw(headers ...string) []byte {
// encodeHeader encodes headers and returns their HPACK bytes. headers // encodeHeader encodes headers and returns their HPACK bytes. headers
// must contain an even number of key/value pairs. There may be // must contain an even number of key/value pairs. There may be
// multiple pairs for keys (e.g. "cookie"). The :method, :path, and // multiple pairs for keys (e.g. "cookie"). The :method, :path, and
// :scheme headers default to GET, / and https. // :scheme headers default to GET, / and https. The :authority header
// defaults to st.ts.Listener.Addr().
func (st *serverTester) encodeHeader(headers ...string) []byte { func (st *serverTester) encodeHeader(headers ...string) []byte {
if len(headers)%2 == 1 { if len(headers)%2 == 1 {
panic("odd number of kv args") panic("odd number of kv args")
} }
st.headerBuf.Reset() st.headerBuf.Reset()
defaultAuthority := st.ts.Listener.Addr().String()
if len(headers) == 0 { if len(headers) == 0 {
// Fast path, mostly for benchmarks, so test code doesn't pollute // Fast path, mostly for benchmarks, so test code doesn't pollute
// profiles when we're looking to improve server allocations. // profiles when we're looking to improve server allocations.
st.encodeHeaderField(":method", "GET") st.encodeHeaderField(":method", "GET")
st.encodeHeaderField(":path", "/")
st.encodeHeaderField(":scheme", "https") st.encodeHeaderField(":scheme", "https")
st.encodeHeaderField(":authority", defaultAuthority)
st.encodeHeaderField(":path", "/")
return st.headerBuf.Bytes() return st.headerBuf.Bytes()
} }
if len(headers) == 2 && headers[0] == ":method" { if len(headers) == 2 && headers[0] == ":method" {
// Another fast path for benchmarks. // Another fast path for benchmarks.
st.encodeHeaderField(":method", headers[1]) st.encodeHeaderField(":method", headers[1])
st.encodeHeaderField(":path", "/")
st.encodeHeaderField(":scheme", "https") st.encodeHeaderField(":scheme", "https")
st.encodeHeaderField(":authority", defaultAuthority)
st.encodeHeaderField(":path", "/")
return st.headerBuf.Bytes() return st.headerBuf.Bytes()
} }
pseudoCount := map[string]int{} pseudoCount := map[string]int{}
keys := []string{":method", ":path", ":scheme"} keys := []string{":method", ":scheme", ":authority", ":path"}
vals := map[string][]string{ vals := map[string][]string{
":method": {"GET"}, ":method": {"GET"},
":path": {"/"}, ":scheme": {"https"},
":scheme": {"https"}, ":authority": {defaultAuthority},
":path": {"/"},
} }
for len(headers) > 0 { for len(headers) > 0 {
k, v := headers[0], headers[1] k, v := headers[0], headers[1]
...@@ -512,7 +517,18 @@ func (st *serverTester) wantSettingsAck() { ...@@ -512,7 +517,18 @@ func (st *serverTester) wantSettingsAck() {
if !sf.Header().Flags.Has(FlagSettingsAck) { if !sf.Header().Flags.Has(FlagSettingsAck) {
st.t.Fatal("Settings Frame didn't have ACK set") st.t.Fatal("Settings Frame didn't have ACK set")
} }
}
func (st *serverTester) wantPushPromise() *PushPromiseFrame {
f, err := st.readFrame()
if err != nil {
st.t.Fatal(err)
}
ppf, ok := f.(*PushPromiseFrame)
if !ok {
st.t.Fatalf("Wanted PushPromise, received %T", ppf)
}
return ppf
} }
func TestServer(t *testing.T) { func TestServer(t *testing.T) {
...@@ -767,7 +783,7 @@ func TestServer_Request_Get_Host(t *testing.T) { ...@@ -767,7 +783,7 @@ func TestServer_Request_Get_Host(t *testing.T) {
testServerRequest(t, func(st *serverTester) { testServerRequest(t, func(st *serverTester) {
st.writeHeaders(HeadersFrameParam{ st.writeHeaders(HeadersFrameParam{
StreamID: 1, // clients send odd numbers StreamID: 1, // clients send odd numbers
BlockFragment: st.encodeHeader("host", host), BlockFragment: st.encodeHeader(":authority", "", "host", host),
EndStream: true, EndStream: true,
EndHeaders: true, EndHeaders: true,
}) })
...@@ -3314,40 +3330,40 @@ func (he *hpackEncoder) encodeHeaderRaw(t *testing.T, headers ...string) []byte ...@@ -3314,40 +3330,40 @@ func (he *hpackEncoder) encodeHeaderRaw(t *testing.T, headers ...string) []byte
func TestCheckValidHTTP2Request(t *testing.T) { func TestCheckValidHTTP2Request(t *testing.T) {
tests := []struct { tests := []struct {
req *http.Request h http.Header
want error want error
}{ }{
{ {
req: &http.Request{Header: http.Header{"Te": {"trailers"}}}, h: http.Header{"Te": {"trailers"}},
want: nil, want: nil,
}, },
{ {
req: &http.Request{Header: http.Header{"Te": {"trailers", "bogus"}}}, h: http.Header{"Te": {"trailers", "bogus"}},
want: errors.New(`request header "TE" may only be "trailers" in HTTP/2`), want: errors.New(`request header "TE" may only be "trailers" in HTTP/2`),
}, },
{ {
req: &http.Request{Header: http.Header{"Foo": {""}}}, h: http.Header{"Foo": {""}},
want: nil, want: nil,
}, },
{ {
req: &http.Request{Header: http.Header{"Connection": {""}}}, h: http.Header{"Connection": {""}},
want: errors.New(`request header "Connection" is not valid in HTTP/2`), want: errors.New(`request header "Connection" is not valid in HTTP/2`),
}, },
{ {
req: &http.Request{Header: http.Header{"Proxy-Connection": {""}}}, h: http.Header{"Proxy-Connection": {""}},
want: errors.New(`request header "Proxy-Connection" is not valid in HTTP/2`), want: errors.New(`request header "Proxy-Connection" is not valid in HTTP/2`),
}, },
{ {
req: &http.Request{Header: http.Header{"Keep-Alive": {""}}}, h: http.Header{"Keep-Alive": {""}},
want: errors.New(`request header "Keep-Alive" is not valid in HTTP/2`), want: errors.New(`request header "Keep-Alive" is not valid in HTTP/2`),
}, },
{ {
req: &http.Request{Header: http.Header{"Upgrade": {""}}}, h: http.Header{"Upgrade": {""}},
want: errors.New(`request header "Upgrade" is not valid in HTTP/2`), want: errors.New(`request header "Upgrade" is not valid in HTTP/2`),
}, },
} }
for i, tt := range tests { for i, tt := range tests {
got := checkValidHTTP2Request(tt.req) got := checkValidHTTP2RequestHeaders(tt.h)
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%d. checkValidHTTP2Request = %v; want %v", i, got, tt.want) t.Errorf("%d. checkValidHTTP2Request = %v; want %v", i, got, tt.want)
} }
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"time" "time"
"golang.org/x/net/http2/hpack" "golang.org/x/net/http2/hpack"
...@@ -150,6 +151,33 @@ func (writeSettingsAck) writeFrame(ctx writeContext) error { ...@@ -150,6 +151,33 @@ func (writeSettingsAck) writeFrame(ctx writeContext) error {
func (writeSettingsAck) staysWithinBuffer(max int) bool { return frameHeaderLen <= max } func (writeSettingsAck) staysWithinBuffer(max int) bool { return frameHeaderLen <= max }
// splitHeaderBlock splits headerBlock into fragments so that each fragment fits
// in a single frame, then calls fn for each fragment. firstFrag/lastFrag are true
// for the first/last fragment, respectively.
func splitHeaderBlock(ctx writeContext, headerBlock []byte, fn func(ctx writeContext, frag []byte, firstFrag, lastFrag bool) error) error {
// For now we're lazy and just pick the minimum MAX_FRAME_SIZE
// that all peers must support (16KB). Later we could care
// more and send larger frames if the peer advertised it, but
// there's little point. Most headers are small anyway (so we
// generally won't have CONTINUATION frames), and extra frames
// only waste 9 bytes anyway.
const maxFrameSize = 16384
first := true
for len(headerBlock) > 0 {
frag := headerBlock
if len(frag) > maxFrameSize {
frag = frag[:maxFrameSize]
}
headerBlock = headerBlock[len(frag):]
if err := fn(ctx, frag, first, len(headerBlock) == 0); err != nil {
return err
}
first = false
}
return nil
}
// writeResHeaders is a request to write a HEADERS and 0+ CONTINUATION frames // writeResHeaders is a request to write a HEADERS and 0+ CONTINUATION frames
// for HTTP response headers or trailers from a server handler. // for HTTP response headers or trailers from a server handler.
type writeResHeaders struct { type writeResHeaders struct {
...@@ -207,39 +235,69 @@ func (w *writeResHeaders) writeFrame(ctx writeContext) error { ...@@ -207,39 +235,69 @@ func (w *writeResHeaders) writeFrame(ctx writeContext) error {
panic("unexpected empty hpack") panic("unexpected empty hpack")
} }
// For now we're lazy and just pick the minimum MAX_FRAME_SIZE return splitHeaderBlock(ctx, headerBlock, w.writeHeaderBlock)
// that all peers must support (16KB). Later we could care }
// more and send larger frames if the peer advertised it, but
// there's little point. Most headers are small anyway (so we
// generally won't have CONTINUATION frames), and extra frames
// only waste 9 bytes anyway.
const maxFrameSize = 16384
first := true func (w *writeResHeaders) writeHeaderBlock(ctx writeContext, frag []byte, firstFrag, lastFrag bool) error {
for len(headerBlock) > 0 { if firstFrag {
frag := headerBlock return ctx.Framer().WriteHeaders(HeadersFrameParam{
if len(frag) > maxFrameSize { StreamID: w.streamID,
frag = frag[:maxFrameSize] BlockFragment: frag,
} EndStream: w.endStream,
headerBlock = headerBlock[len(frag):] EndHeaders: lastFrag,
endHeaders := len(headerBlock) == 0 })
var err error } else {
if first { return ctx.Framer().WriteContinuation(w.streamID, lastFrag, frag)
first = false }
err = ctx.Framer().WriteHeaders(HeadersFrameParam{ }
StreamID: w.streamID,
BlockFragment: frag, // writePushPromise is a request to write a PUSH_PROMISE and 0+ CONTINUATION frames.
EndStream: w.endStream, type writePushPromise struct {
EndHeaders: endHeaders, streamID uint32 // pusher stream
}) method string // for :method
} else { url *url.URL // for :scheme, :authority, :path
err = ctx.Framer().WriteContinuation(w.streamID, endHeaders, frag) h http.Header
}
if err != nil { // Creates an ID for a pushed stream. This runs on serveG just before
return err // the frame is written. The returned ID is copied to promisedID.
} allocatePromisedID func() (uint32, error)
promisedID uint32
}
func (w *writePushPromise) staysWithinBuffer(max int) bool {
// TODO: see writeResHeaders.staysWithinBuffer
return false
}
func (w *writePushPromise) writeFrame(ctx writeContext) error {
enc, buf := ctx.HeaderEncoder()
buf.Reset()
encKV(enc, ":method", w.method)
encKV(enc, ":scheme", w.url.Scheme)
encKV(enc, ":authority", w.url.Host)
encKV(enc, ":path", w.url.RequestURI())
encodeHeaders(enc, w.h, nil)
headerBlock := buf.Bytes()
if len(headerBlock) == 0 {
panic("unexpected empty hpack")
}
return splitHeaderBlock(ctx, headerBlock, w.writeHeaderBlock)
}
func (w *writePushPromise) writeHeaderBlock(ctx writeContext, frag []byte, firstFrag, lastFrag bool) error {
if firstFrag {
return ctx.Framer().WritePushPromise(PushPromiseParam{
StreamID: w.streamID,
PromiseID: w.promisedID,
BlockFragment: frag,
EndHeaders: lastFrag,
})
} else {
return ctx.Framer().WriteContinuation(w.streamID, lastFrag, frag)
} }
return nil
} }
type write100ContinueHeadersFrame struct { type write100ContinueHeadersFrame struct {
...@@ -274,6 +332,8 @@ func (wu writeWindowUpdate) writeFrame(ctx writeContext) error { ...@@ -274,6 +332,8 @@ func (wu writeWindowUpdate) writeFrame(ctx writeContext) error {
return ctx.Framer().WriteWindowUpdate(wu.streamID, wu.n) return ctx.Framer().WriteWindowUpdate(wu.streamID, wu.n)
} }
// encodeHeaders encodes an http.Header. If keys is not nil, then (k, h[k])
// is encoded only only if k is in keys.
func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) { func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) {
if keys == nil { if keys == nil {
sorter := sorterPool.Get().(*sorter) sorter := sorterPool.Get().(*sorter)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment