diff --git a/go.mod b/go.mod index 1446f39a4..57e8a0252 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module golang.org/x/net go 1.18 require ( - golang.org/x/crypto v0.22.0 - golang.org/x/sys v0.19.0 - golang.org/x/term v0.19.0 - golang.org/x/text v0.14.0 + golang.org/x/crypto v0.23.0 + golang.org/x/sys v0.20.0 + golang.org/x/term v0.20.0 + golang.org/x/text v0.15.0 ) diff --git a/go.sum b/go.sum index a4e7f116d..50575cf8c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/html/doc.go b/html/doc.go index 2466ae3d9..3a7e5ab17 100644 --- a/html/doc.go +++ b/html/doc.go @@ -104,7 +104,7 @@ tokenization, and tokenization and tree construction stages of the WHATWG HTML parsing specification respectively. While the tokenizer parses and normalizes individual HTML tokens, only the parser constructs the DOM tree from the tokenized HTML, as described in the tree construction stage of the -specification, dynamically modifying or extending the docuemnt's DOM tree. +specification, dynamically modifying or extending the document's DOM tree. If your use case requires semantically well-formed HTML documents, as defined by the WHATWG specification, the parser should be used rather than the tokenizer. diff --git a/http/httpguts/httplex.go b/http/httpguts/httplex.go index 6e071e852..9b4de9401 100644 --- a/http/httpguts/httplex.go +++ b/http/httpguts/httplex.go @@ -12,7 +12,7 @@ import ( "golang.org/x/net/idna" ) -var isTokenTable = [127]bool{ +var isTokenTable = [256]bool{ '!': true, '#': true, '$': true, @@ -93,12 +93,7 @@ var isTokenTable = [127]bool{ } func IsTokenRune(r rune) bool { - i := int(r) - return i < len(isTokenTable) && isTokenTable[i] -} - -func isNotToken(r rune) bool { - return !IsTokenRune(r) + return r < utf8.RuneSelf && isTokenTable[byte(r)] } // HeaderValuesContainsToken reports whether any string in values @@ -202,8 +197,8 @@ func ValidHeaderFieldName(v string) bool { if len(v) == 0 { return false } - for _, r := range v { - if !IsTokenRune(r) { + for i := 0; i < len(v); i++ { + if !isTokenTable[v[i]] { return false } } diff --git a/http/httpguts/httplex_test.go b/http/httpguts/httplex_test.go index a2c57f392..791440b1a 100644 --- a/http/httpguts/httplex_test.go +++ b/http/httpguts/httplex_test.go @@ -20,7 +20,7 @@ func isSeparator(c rune) bool { return false } -func TestIsToken(t *testing.T) { +func TestIsTokenRune(t *testing.T) { for i := 0; i <= 130; i++ { r := rune(i) expected := isChar(r) && !isCtl(r) && !isSeparator(r) @@ -30,6 +30,15 @@ func TestIsToken(t *testing.T) { } } +func BenchmarkIsTokenRune(b *testing.B) { + for i := 0; i < b.N; i++ { + var r rune + for ; r < 1024; r++ { + IsTokenRune(r) + } + } +} + func TestHeaderValuesContainsToken(t *testing.T) { tests := []struct { vals []string @@ -100,6 +109,44 @@ func TestHeaderValuesContainsToken(t *testing.T) { } } +func TestValidHeaderFieldName(t *testing.T) { + tests := []struct { + in string + want bool + }{ + {"", false}, + {"Accept Charset", false}, + {"Accept-Charset", true}, + {"AccepT-EncodinG", true}, + {"CONNECTION", true}, + {"résumé", false}, + } + for _, tt := range tests { + got := ValidHeaderFieldName(tt.in) + if tt.want != got { + t.Errorf("ValidHeaderFieldName(%q) = %t; want %t", tt.in, got, tt.want) + } + } +} + +func BenchmarkValidHeaderFieldName(b *testing.B) { + names := []string{ + "", + "Accept Charset", + "Accept-Charset", + "AccepT-EncodinG", + "CONNECTION", + "résumé", + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, name := range names { + ValidHeaderFieldName(name) + } + } +} + func TestPunycodeHostPort(t *testing.T) { tests := []struct { in, want string diff --git a/http2/frame.go b/http2/frame.go index 43557ab7e..105c3b279 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -490,6 +490,9 @@ func terminalReadFrameError(err error) bool { // returned error is ErrFrameTooLarge. Other errors may be of type // ConnectionError, StreamError, or anything else from the underlying // reader. +// +// If ReadFrame returns an error and a non-nil Frame, the Frame's StreamID +// indicates the stream responsible for the error. func (fr *Framer) ReadFrame() (Frame, error) { fr.errDetail = nil if fr.lastFrame != nil { @@ -1521,7 +1524,7 @@ func (fr *Framer) maxHeaderStringLen() int { // readMetaFrame returns 0 or more CONTINUATION frames from fr and // merge them into the provided hf and returns a MetaHeadersFrame // with the decoded hpack values. -func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { +func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) { if fr.AllowIllegalReads { return nil, errors.New("illegal use of AllowIllegalReads with ReadMetaHeaders") } @@ -1592,7 +1595,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { } // It would be nice to send a RST_STREAM before sending the GOAWAY, // but the structure of the server's frame writer makes this difficult. - return nil, ConnectionError(ErrCodeProtocol) + return mh, ConnectionError(ErrCodeProtocol) } // Also close the connection after any CONTINUATION frame following an @@ -1604,11 +1607,11 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { } // It would be nice to send a RST_STREAM before sending the GOAWAY, // but the structure of the server's frame writer makes this difficult. - return nil, ConnectionError(ErrCodeProtocol) + return mh, ConnectionError(ErrCodeProtocol) } if _, err := hdec.Write(frag); err != nil { - return nil, ConnectionError(ErrCodeCompression) + return mh, ConnectionError(ErrCodeCompression) } if hc.HeadersEnded() { @@ -1625,7 +1628,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { mh.HeadersFrame.invalidate() if err := hdec.Close(); err != nil { - return nil, ConnectionError(ErrCodeCompression) + return mh, ConnectionError(ErrCodeCompression) } if invalid != nil { fr.errDetail = invalid diff --git a/http2/server.go b/http2/server.go index ce2e8b40e..c5d081081 100644 --- a/http2/server.go +++ b/http2/server.go @@ -732,11 +732,7 @@ func isClosedConnError(err error) bool { return false } - // TODO: remove this string search and be more like the Windows - // case below. That might involve modifying the standard library - // to return better error types. - str := err.Error() - if strings.Contains(str, "use of closed network connection") { + if errors.Is(err, net.ErrClosed) { return true } @@ -1482,6 +1478,11 @@ func (sc *serverConn) processFrameFromReader(res readFrameResult) bool { sc.goAway(ErrCodeFlowControl) return true case ConnectionError: + if res.f != nil { + if id := res.f.Header().StreamID; id > sc.maxClientStreamID { + sc.maxClientStreamID = id + } + } sc.logf("http2: server connection error from %v: %v", sc.conn.RemoteAddr(), ev) sc.goAway(ErrCode(ev)) return true // goAway will handle shutdown diff --git a/http2/server_test.go b/http2/server_test.go index a931a06e5..61c773a54 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -4818,9 +4818,17 @@ func TestServerContinuationFlood(t *testing.T) { if err != nil { break } - switch f.(type) { + switch f := f.(type) { case *HeadersFrame: t.Fatalf("received HEADERS frame; want GOAWAY and a closed connection") + case *GoAwayFrame: + // We might not see the GOAWAY (see below), but if we do it should + // indicate that the server processed this request so the client doesn't + // attempt to retry it. + if got, want := f.LastStreamID, uint32(1); got != want { + t.Errorf("received GOAWAY with LastStreamId %v, want %v", got, want) + } + } } // We expect to have seen a GOAWAY before the connection closes, diff --git a/http2/transport.go b/http2/transport.go index ce375c8c7..2fa49490c 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -936,7 +936,20 @@ func (cc *ClientConn) setGoAway(f *GoAwayFrame) { } last := f.LastStreamID for streamID, cs := range cc.streams { - if streamID > last { + if streamID <= last { + // The server's GOAWAY indicates that it received this stream. + // It will either finish processing it, or close the connection + // without doing so. Either way, leave the stream alone for now. + continue + } + if streamID == 1 && cc.goAway.ErrCode != ErrCodeNo { + // Don't retry the first stream on a connection if we get a non-NO error. + // If the server is sending an error on a new connection, + // retrying the request on a new one probably isn't going to work. + cs.abortStreamLocked(fmt.Errorf("http2: Transport received GOAWAY from server ErrCode:%v", cc.goAway.ErrCode)) + } else { + // Aborting the stream with errClentConnGotGoAway indicates that + // the request should be retried on a new connection. cs.abortStreamLocked(errClientConnGotGoAway) } } diff --git a/http2/transport_test.go b/http2/transport_test.go index 11ff67b4c..3e4297f28 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -3144,13 +3144,40 @@ func TestTransportPingWhenReadingPingDisabled(t *testing.T) { } } -func TestTransportRetryAfterGOAWAY(t *testing.T) { +func TestTransportRetryAfterGOAWAYNoRetry(t *testing.T) { tt := newTestTransport(t) req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) rt := tt.roundTrip(req) - // First attempt: Server sends a GOAWAY. + // First attempt: Server sends a GOAWAY with an error and + // a MaxStreamID less than the request ID. + // This probably indicates that there was something wrong with our request, + // so we don't retry it. + tc := tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc.writeSettings() + tc.writeGoAway(0 /*max id*/, ErrCodeInternal, nil) + if rt.err() == nil { + t.Fatalf("after GOAWAY, RoundTrip is not done, want error") + } +} + +func TestTransportRetryAfterGOAWAYRetry(t *testing.T) { + tt := newTestTransport(t) + + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt := tt.roundTrip(req) + + // First attempt: Server sends a GOAWAY with ErrCodeNo and + // a MaxStreamID less than the request ID. + // We take the server at its word that nothing has really gone wrong, + // and retry the request. tc := tt.getConn() tc.wantFrameType(FrameSettings) tc.wantFrameType(FrameWindowUpdate) @@ -3185,6 +3212,69 @@ func TestTransportRetryAfterGOAWAY(t *testing.T) { rt.wantStatus(200) } +func TestTransportRetryAfterGOAWAYSecondRequest(t *testing.T) { + tt := newTestTransport(t) + + // First request succeeds. + req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + rt1 := tt.roundTrip(req) + tc := tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc.writeSettings() + tc.wantFrameType(FrameSettings) // Settings ACK + tc.writeHeaders(HeadersFrameParam{ + StreamID: 1, + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt1.wantStatus(200) + + // Second request: Server sends a GOAWAY with + // a MaxStreamID less than the request ID. + // The server says it didn't see this request, + // so we retry it on a new connection. + req, _ = http.NewRequest("GET", "https://dummy.tld/", nil) + rt2 := tt.roundTrip(req) + + // Second request, first attempt. + tc.wantHeaders(wantHeader{ + streamID: 3, + endStream: true, + }) + tc.writeSettings() + tc.writeGoAway(1 /*max id*/, ErrCodeProtocol, nil) + if rt2.done() { + t.Fatalf("after GOAWAY, RoundTrip is done; want it to be retrying") + } + + // Second request, second attempt. + tc = tt.getConn() + tc.wantFrameType(FrameSettings) + tc.wantFrameType(FrameWindowUpdate) + tc.wantHeaders(wantHeader{ + streamID: 1, + endStream: true, + }) + tc.writeSettings() + tc.writeHeaders(HeadersFrameParam{ + StreamID: 1, + EndHeaders: true, + EndStream: true, + BlockFragment: tc.makeHeaderBlockFragment( + ":status", "200", + ), + }) + rt2.wantStatus(200) +} + func TestTransportRetryAfterRefusedStream(t *testing.T) { tt := newTestTransport(t) diff --git a/quic/main_test.go b/quic/main_test.go index ecd0b1e9f..25e0096e4 100644 --- a/quic/main_test.go +++ b/quic/main_test.go @@ -16,6 +16,20 @@ import ( ) func TestMain(m *testing.M) { + // Add all goroutines running at the start of the test to the set + // of not-leaked goroutines. This includes TestMain, and anything else + // that might have been started by test infrastructure. + skip := [][]byte{ + []byte("created by os/signal.Notify"), + []byte("gotraceback_test.go"), + } + buf := make([]byte, 2<<20) + buf = buf[:runtime.Stack(buf, true)] + for _, g := range bytes.Split(buf, []byte("\n\n")) { + id, _, _ := bytes.Cut(g, []byte("[")) + skip = append(skip, id) + } + defer os.Exit(m.Run()) // Look for leaked goroutines. @@ -34,12 +48,13 @@ func TestMain(m *testing.M) { buf = buf[:runtime.Stack(buf, true)] leaked := false for _, g := range bytes.Split(buf, []byte("\n\n")) { - if bytes.Contains(g, []byte("quic.TestMain")) || - bytes.Contains(g, []byte("created by os/signal.Notify")) || - bytes.Contains(g, []byte("gotraceback_test.go")) { - continue - } leaked = true + for _, s := range skip { + if bytes.Contains(g, s) { + leaked = false + break + } + } } if !leaked { break diff --git a/quic/packet_protection.go b/quic/packet_protection.go index 1f939f491..fe48c14c5 100644 --- a/quic/packet_protection.go +++ b/quic/packet_protection.go @@ -351,7 +351,13 @@ func (k *updatingKeyPair) init() { // We perform the first key update early in the connection so a peer // which does not support key updates will fail rapidly, // rather than after the connection has been long established. - k.updateAfter = 1000 + // + // The QUIC interop runner "keyupdate" test requires that the client + // initiate a key rotation early in the connection. Increasing this + // value may cause interop test failures; if we do want to increase it, + // we should either skip the keyupdate test or provide a way to override + // the setting in interop tests. + k.updateAfter = 100 } func (k *updatingKeyPair) canRead() bool {