mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 12:32:21 +00:00
522 lines
14 KiB
Go
522 lines
14 KiB
Go
// Copyright 2016 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.
|
|
|
|
// +build go1.8
|
|
|
|
package http2
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"reflect"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestServer_Push_Success(t *testing.T) {
|
|
const (
|
|
mainBody = "<html>index page</html>"
|
|
pushedBody = "<html>pushed page</html>"
|
|
userAgent = "testagent"
|
|
cookie = "testcookie"
|
|
)
|
|
|
|
var stURL string
|
|
checkPromisedReq := func(r *http.Request, wantMethod string, wantH http.Header) error {
|
|
if got, want := r.Method, wantMethod; got != want {
|
|
return fmt.Errorf("promised Req.Method=%q, want %q", got, want)
|
|
}
|
|
if got, want := r.Header, wantH; !reflect.DeepEqual(got, want) {
|
|
return fmt.Errorf("promised Req.Header=%q, want %q", got, want)
|
|
}
|
|
if got, want := "https://"+r.Host, stURL; got != want {
|
|
return fmt.Errorf("promised Req.Host=%q, want %q", got, want)
|
|
}
|
|
if r.Body == nil {
|
|
return fmt.Errorf("nil Body")
|
|
}
|
|
if buf, err := ioutil.ReadAll(r.Body); err != nil || len(buf) != 0 {
|
|
return fmt.Errorf("ReadAll(Body)=%q,%v, want '',nil", buf, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
errc := make(chan error, 3)
|
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.RequestURI() {
|
|
case "/":
|
|
// Push "/pushed?get" as a GET request, using an absolute URL.
|
|
opt := &http.PushOptions{
|
|
Header: http.Header{
|
|
"User-Agent": {userAgent},
|
|
},
|
|
}
|
|
if err := w.(http.Pusher).Push(stURL+"/pushed?get", opt); err != nil {
|
|
errc <- fmt.Errorf("error pushing /pushed?get: %v", err)
|
|
return
|
|
}
|
|
// Push "/pushed?head" as a HEAD request, using a path.
|
|
opt = &http.PushOptions{
|
|
Method: "HEAD",
|
|
Header: http.Header{
|
|
"User-Agent": {userAgent},
|
|
"Cookie": {cookie},
|
|
},
|
|
}
|
|
if err := w.(http.Pusher).Push("/pushed?head", opt); err != nil {
|
|
errc <- fmt.Errorf("error pushing /pushed?head: %v", err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(mainBody)))
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, mainBody)
|
|
errc <- nil
|
|
|
|
case "/pushed?get":
|
|
wantH := http.Header{}
|
|
wantH.Set("User-Agent", userAgent)
|
|
if err := checkPromisedReq(r, "GET", wantH); err != nil {
|
|
errc <- fmt.Errorf("/pushed?get: %v", err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(pushedBody)))
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, pushedBody)
|
|
errc <- nil
|
|
|
|
case "/pushed?head":
|
|
wantH := http.Header{}
|
|
wantH.Set("User-Agent", userAgent)
|
|
wantH.Set("Cookie", cookie)
|
|
if err := checkPromisedReq(r, "HEAD", wantH); err != nil {
|
|
errc <- fmt.Errorf("/pushed?head: %v", err)
|
|
return
|
|
}
|
|
w.WriteHeader(204)
|
|
errc <- nil
|
|
|
|
default:
|
|
errc <- fmt.Errorf("unknown RequestURL %q", r.URL.RequestURI())
|
|
}
|
|
})
|
|
stURL = st.ts.URL
|
|
|
|
// Send one request, which should push two responses.
|
|
st.greet()
|
|
getSlash(st)
|
|
for k := 0; k < 3; k++ {
|
|
select {
|
|
case <-time.After(2 * time.Second):
|
|
t.Errorf("timeout waiting for handler %d to finish", k)
|
|
case err := <-errc:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
checkPushPromise := func(f Frame, promiseID uint32, wantH [][2]string) error {
|
|
pp, ok := f.(*PushPromiseFrame)
|
|
if !ok {
|
|
return fmt.Errorf("got a %T; want *PushPromiseFrame", f)
|
|
}
|
|
if !pp.HeadersEnded() {
|
|
return fmt.Errorf("want END_HEADERS flag in PushPromiseFrame")
|
|
}
|
|
if got, want := pp.PromiseID, promiseID; got != want {
|
|
return fmt.Errorf("got PromiseID %v; want %v", got, want)
|
|
}
|
|
gotH := st.decodeHeader(pp.HeaderBlockFragment())
|
|
if !reflect.DeepEqual(gotH, wantH) {
|
|
return fmt.Errorf("got promised headers %v; want %v", gotH, wantH)
|
|
}
|
|
return nil
|
|
}
|
|
checkHeaders := func(f Frame, wantH [][2]string) error {
|
|
hf, ok := f.(*HeadersFrame)
|
|
if !ok {
|
|
return fmt.Errorf("got a %T; want *HeadersFrame", f)
|
|
}
|
|
gotH := st.decodeHeader(hf.HeaderBlockFragment())
|
|
if !reflect.DeepEqual(gotH, wantH) {
|
|
return fmt.Errorf("got response headers %v; want %v", gotH, wantH)
|
|
}
|
|
return nil
|
|
}
|
|
checkData := func(f Frame, wantData string) error {
|
|
df, ok := f.(*DataFrame)
|
|
if !ok {
|
|
return fmt.Errorf("got a %T; want *DataFrame", f)
|
|
}
|
|
if gotData := string(df.Data()); gotData != wantData {
|
|
return fmt.Errorf("got response data %q; want %q", gotData, wantData)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stream 1 has 2 PUSH_PROMISE + HEADERS + DATA
|
|
// Stream 2 has HEADERS + DATA
|
|
// Stream 4 has HEADERS
|
|
expected := map[uint32][]func(Frame) error{
|
|
1: {
|
|
func(f Frame) error {
|
|
return checkPushPromise(f, 2, [][2]string{
|
|
{":method", "GET"},
|
|
{":scheme", "https"},
|
|
{":authority", st.ts.Listener.Addr().String()},
|
|
{":path", "/pushed?get"},
|
|
{"user-agent", userAgent},
|
|
})
|
|
},
|
|
func(f Frame) error {
|
|
return checkPushPromise(f, 4, [][2]string{
|
|
{":method", "HEAD"},
|
|
{":scheme", "https"},
|
|
{":authority", st.ts.Listener.Addr().String()},
|
|
{":path", "/pushed?head"},
|
|
{"cookie", cookie},
|
|
{"user-agent", userAgent},
|
|
})
|
|
},
|
|
func(f Frame) error {
|
|
return checkHeaders(f, [][2]string{
|
|
{":status", "200"},
|
|
{"content-type", "text/html"},
|
|
{"content-length", strconv.Itoa(len(mainBody))},
|
|
})
|
|
},
|
|
func(f Frame) error {
|
|
return checkData(f, mainBody)
|
|
},
|
|
},
|
|
2: {
|
|
func(f Frame) error {
|
|
return checkHeaders(f, [][2]string{
|
|
{":status", "200"},
|
|
{"content-type", "text/html"},
|
|
{"content-length", strconv.Itoa(len(pushedBody))},
|
|
})
|
|
},
|
|
func(f Frame) error {
|
|
return checkData(f, pushedBody)
|
|
},
|
|
},
|
|
4: {
|
|
func(f Frame) error {
|
|
return checkHeaders(f, [][2]string{
|
|
{":status", "204"},
|
|
})
|
|
},
|
|
},
|
|
}
|
|
|
|
consumed := map[uint32]int{}
|
|
for k := 0; len(expected) > 0; k++ {
|
|
f, err := st.readFrame()
|
|
if err != nil {
|
|
for id, left := range expected {
|
|
t.Errorf("stream %d: missing %d frames", id, len(left))
|
|
}
|
|
t.Fatalf("readFrame %d: %v", k, err)
|
|
}
|
|
id := f.Header().StreamID
|
|
label := fmt.Sprintf("stream %d, frame %d", id, consumed[id])
|
|
if len(expected[id]) == 0 {
|
|
t.Fatalf("%s: unexpected frame %#+v", label, f)
|
|
}
|
|
check := expected[id][0]
|
|
expected[id] = expected[id][1:]
|
|
if len(expected[id]) == 0 {
|
|
delete(expected, id)
|
|
}
|
|
if err := check(f); err != nil {
|
|
t.Fatalf("%s: %v", label, err)
|
|
}
|
|
consumed[id]++
|
|
}
|
|
}
|
|
|
|
func TestServer_Push_SuccessNoRace(t *testing.T) {
|
|
// Regression test for issue #18326. Ensure the request handler can mutate
|
|
// pushed request headers without racing with the PUSH_PROMISE write.
|
|
errc := make(chan error, 2)
|
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.RequestURI() {
|
|
case "/":
|
|
opt := &http.PushOptions{
|
|
Header: http.Header{"User-Agent": {"testagent"}},
|
|
}
|
|
if err := w.(http.Pusher).Push("/pushed", opt); err != nil {
|
|
errc <- fmt.Errorf("error pushing: %v", err)
|
|
return
|
|
}
|
|
w.WriteHeader(200)
|
|
errc <- nil
|
|
|
|
case "/pushed":
|
|
// Update request header, ensure there is no race.
|
|
r.Header.Set("User-Agent", "newagent")
|
|
r.Header.Set("Cookie", "cookie")
|
|
w.WriteHeader(200)
|
|
errc <- nil
|
|
|
|
default:
|
|
errc <- fmt.Errorf("unknown RequestURL %q", r.URL.RequestURI())
|
|
}
|
|
})
|
|
|
|
// Send one request, which should push one response.
|
|
st.greet()
|
|
getSlash(st)
|
|
for k := 0; k < 2; k++ {
|
|
select {
|
|
case <-time.After(2 * time.Second):
|
|
t.Errorf("timeout waiting for handler %d to finish", k)
|
|
case err := <-errc:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServer_Push_RejectRecursivePush(t *testing.T) {
|
|
// Expect two requests, but might get three if there's a bug and the second push succeeds.
|
|
errc := make(chan error, 3)
|
|
handler := func(w http.ResponseWriter, r *http.Request) error {
|
|
baseURL := "https://" + r.Host
|
|
switch r.URL.Path {
|
|
case "/":
|
|
if err := w.(http.Pusher).Push(baseURL+"/push1", nil); err != nil {
|
|
return fmt.Errorf("first Push()=%v, want nil", err)
|
|
}
|
|
return nil
|
|
|
|
case "/push1":
|
|
if got, want := w.(http.Pusher).Push(baseURL+"/push2", nil), ErrRecursivePush; got != want {
|
|
return fmt.Errorf("Push()=%v, want %v", got, want)
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("unexpected path: %q", r.URL.Path)
|
|
}
|
|
}
|
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
errc <- handler(w, r)
|
|
})
|
|
defer st.Close()
|
|
st.greet()
|
|
getSlash(st)
|
|
if err := <-errc; err != nil {
|
|
t.Errorf("First request failed: %v", err)
|
|
}
|
|
if err := <-errc; err != nil {
|
|
t.Errorf("Second request failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func testServer_Push_RejectSingleRequest(t *testing.T, doPush func(http.Pusher, *http.Request) error, settings ...Setting) {
|
|
// Expect one request, but might get two if there's a bug and the push succeeds.
|
|
errc := make(chan error, 2)
|
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
errc <- doPush(w.(http.Pusher), r)
|
|
})
|
|
defer st.Close()
|
|
st.greet()
|
|
if err := st.fr.WriteSettings(settings...); err != nil {
|
|
st.t.Fatalf("WriteSettings: %v", err)
|
|
}
|
|
st.wantSettingsAck()
|
|
getSlash(st)
|
|
if err := <-errc; err != nil {
|
|
t.Error(err)
|
|
}
|
|
// Should not get a PUSH_PROMISE frame.
|
|
hf := st.wantHeaders()
|
|
if !hf.StreamEnded() {
|
|
t.Error("stream should end after headers")
|
|
}
|
|
}
|
|
|
|
func TestServer_Push_RejectIfDisabled(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
if got, want := p.Push("https://"+r.Host+"/pushed", nil), http.ErrNotSupported; got != want {
|
|
return fmt.Errorf("Push()=%v, want %v", got, want)
|
|
}
|
|
return nil
|
|
},
|
|
Setting{SettingEnablePush, 0})
|
|
}
|
|
|
|
func TestServer_Push_RejectWhenNoConcurrentStreams(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
if got, want := p.Push("https://"+r.Host+"/pushed", nil), ErrPushLimitReached; got != want {
|
|
return fmt.Errorf("Push()=%v, want %v", got, want)
|
|
}
|
|
return nil
|
|
},
|
|
Setting{SettingMaxConcurrentStreams, 0})
|
|
}
|
|
|
|
func TestServer_Push_RejectWrongScheme(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
if err := p.Push("http://"+r.Host+"/pushed", nil); err == nil {
|
|
return errors.New("Push() should have failed (push target URL is http)")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestServer_Push_RejectMissingHost(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
if err := p.Push("https:pushed", nil); err == nil {
|
|
return errors.New("Push() should have failed (push target URL missing host)")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestServer_Push_RejectRelativePath(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
if err := p.Push("../test", nil); err == nil {
|
|
return errors.New("Push() should have failed (push target is a relative path)")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestServer_Push_RejectForbiddenMethod(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
if err := p.Push("https://"+r.Host+"/pushed", &http.PushOptions{Method: "POST"}); err == nil {
|
|
return errors.New("Push() should have failed (cannot promise a POST)")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestServer_Push_RejectForbiddenHeader(t *testing.T) {
|
|
testServer_Push_RejectSingleRequest(t,
|
|
func(p http.Pusher, r *http.Request) error {
|
|
header := http.Header{
|
|
"Content-Length": {"10"},
|
|
"Content-Encoding": {"gzip"},
|
|
"Trailer": {"Foo"},
|
|
"Te": {"trailers"},
|
|
"Host": {"test.com"},
|
|
":authority": {"test.com"},
|
|
}
|
|
if err := p.Push("https://"+r.Host+"/pushed", &http.PushOptions{Header: header}); err == nil {
|
|
return errors.New("Push() should have failed (forbidden headers)")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestServer_Push_StateTransitions(t *testing.T) {
|
|
const body = "foo"
|
|
|
|
gotPromise := make(chan bool)
|
|
finishedPush := make(chan bool)
|
|
|
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.RequestURI() {
|
|
case "/":
|
|
if err := w.(http.Pusher).Push("/pushed", nil); err != nil {
|
|
t.Errorf("Push error: %v", err)
|
|
}
|
|
// Don't finish this request until the push finishes so we don't
|
|
// nondeterministically interleave output frames with the push.
|
|
<-finishedPush
|
|
case "/pushed":
|
|
<-gotPromise
|
|
}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, body)
|
|
})
|
|
defer st.Close()
|
|
|
|
st.greet()
|
|
if st.stream(2) != nil {
|
|
t.Fatal("stream 2 should be empty")
|
|
}
|
|
if got, want := st.streamState(2), stateIdle; got != want {
|
|
t.Fatalf("streamState(2)=%v, want %v", got, want)
|
|
}
|
|
getSlash(st)
|
|
// After the PUSH_PROMISE is sent, the stream should be stateHalfClosedRemote.
|
|
st.wantPushPromise()
|
|
if got, want := st.streamState(2), stateHalfClosedRemote; got != want {
|
|
t.Fatalf("streamState(2)=%v, want %v", got, want)
|
|
}
|
|
// We stall the HTTP handler for "/pushed" until the above check. If we don't
|
|
// stall the handler, then the handler might write HEADERS and DATA and finish
|
|
// the stream before we check st.streamState(2) -- should that happen, we'll
|
|
// see stateClosed and fail the above check.
|
|
close(gotPromise)
|
|
st.wantHeaders()
|
|
if df := st.wantData(); !df.StreamEnded() {
|
|
t.Fatal("expected END_STREAM flag on DATA")
|
|
}
|
|
if got, want := st.streamState(2), stateClosed; got != want {
|
|
t.Fatalf("streamState(2)=%v, want %v", got, want)
|
|
}
|
|
close(finishedPush)
|
|
}
|
|
|
|
func TestServer_Push_RejectAfterGoAway(t *testing.T) {
|
|
var readyOnce sync.Once
|
|
ready := make(chan struct{})
|
|
errc := make(chan error, 2)
|
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case <-ready:
|
|
case <-time.After(5 * time.Second):
|
|
errc <- fmt.Errorf("timeout waiting for GOAWAY to be processed")
|
|
}
|
|
if got, want := w.(http.Pusher).Push("https://"+r.Host+"/pushed", nil), http.ErrNotSupported; got != want {
|
|
errc <- fmt.Errorf("Push()=%v, want %v", got, want)
|
|
}
|
|
errc <- nil
|
|
})
|
|
defer st.Close()
|
|
st.greet()
|
|
getSlash(st)
|
|
|
|
// Send GOAWAY and wait for it to be processed.
|
|
st.fr.WriteGoAway(1, ErrCodeNo, nil)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ready:
|
|
return
|
|
default:
|
|
}
|
|
st.sc.serveMsgCh <- func(loopNum int) {
|
|
if !st.sc.pushEnabled {
|
|
readyOnce.Do(func() { close(ready) })
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
if err := <-errc; err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|