http2: fix state tracking for pushed streams

Fix sc.state, which was returning "idle" instead of "closed" if the max
pushed stream ID happened to exceed the max client stream ID. This caused
us to erroneously generate connection errors because we believed a state
invariant had been violated when it had not.

I also renamed maxStreamID to maxClientStreamID for clarity, since we
need to track client-generated and server-generated stream IDs separately.

Fixes golang/go#17777

Change-Id: Id3d5700d79cc699a267bd91d6ebace0770fa62fc
Reviewed-on: https://go-review.googlesource.com/32755
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/http2/server.go b/http2/server.go
index b8bf4ef..e020c29 100644
--- a/http2/server.go
+++ b/http2/server.go
@@ -386,8 +386,8 @@
 	advMaxStreams         uint32 // our SETTINGS_MAX_CONCURRENT_STREAMS advertised the client
 	curClientStreams      uint32 // number of open streams initiated by the client
 	curPushedStreams      uint32 // number of open streams initiated by server push
-	maxStreamID           uint32 // max ever seen from client
-	maxPushPromiseID      uint32 // ID of the last push promise, or 0 if there have been no pushes
+	maxClientStreamID     uint32 // max ever seen from client (odd), or 0 if there have been no client requests
+	maxPushPromiseID      uint32 // ID of the last push promise (even), or 0 if there have been no pushes
 	streams               map[uint32]*stream
 	initialWindowSize     int32
 	maxFrameSize          int32
@@ -477,8 +477,14 @@
 	// a client sends a HEADERS frame on stream 7 without ever sending a
 	// frame on stream 5, then stream 5 transitions to the "closed"
 	// state when the first frame for stream 7 is sent or received."
-	if streamID <= sc.maxStreamID {
-		return stateClosed, nil
+	if streamID%2 == 1 {
+		if streamID <= sc.maxClientStreamID {
+			return stateClosed, nil
+		}
+	} else {
+		if streamID <= sc.maxPushPromiseID {
+			return stateClosed, nil
+		}
 	}
 	return stateIdle, nil
 }
@@ -1011,7 +1017,7 @@
 			sc.needToSendGoAway = false
 			sc.startFrameWrite(FrameWriteRequest{
 				write: &writeGoAway{
-					maxStreamID: sc.maxStreamID,
+					maxStreamID: sc.maxClientStreamID,
 					code:        sc.goAwayCode,
 				},
 			})
@@ -1495,10 +1501,10 @@
 	// endpoint has opened or reserved. [...]  An endpoint that
 	// receives an unexpected stream identifier MUST respond with
 	// a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
-	if id <= sc.maxStreamID {
+	if id <= sc.maxClientStreamID {
 		return ConnectionError(ErrCodeProtocol)
 	}
-	sc.maxStreamID = id
+	sc.maxClientStreamID = id
 
 	if sc.idleTimer != nil {
 		sc.idleTimer.Stop()
diff --git a/http2/server_push_test.go b/http2/server_push_test.go
index 5668839..3017be4 100644
--- a/http2/server_push_test.go
+++ b/http2/server_push_test.go
@@ -381,3 +381,49 @@
 			return nil
 		})
 }
+
+func TestServer_Push_StateTransitions(t *testing.T) {
+	const body = "foo"
+
+	startedPromise := 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)
+			}
+			close(startedPromise)
+			// Don't finish this request until the push finishes so we don't
+			// nondeterministically interleave output frames with the push.
+			<-finishedPush
+		}
+		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)
+	<-startedPromise
+	if got, want := st.streamState(2), stateHalfClosedRemote; got != want {
+		t.Fatalf("streamState(2)=%v, want %v", got, want)
+	}
+	st.wantPushPromise()
+	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)
+}