diff --git a/README.md b/README.md
index a15f253dff..235c8d8b30 100644
--- a/README.md
+++ b/README.md
@@ -2,17 +2,15 @@
[](https://pkg.go.dev/golang.org/x/net)
-This repository holds supplementary Go networking libraries.
+This repository holds supplementary Go networking packages.
-## Download/Install
+## Report Issues / Send Patches
-The easiest way to install is to run `go get -u golang.org/x/net`. You can
-also manually git clone the repository to `$GOPATH/src/golang.org/x/net`.
+This repository uses Gerrit for code changes. To learn how to submit changes to
+this repository, see https://go.dev/doc/contribute.
-## Report Issues / Send Patches
+The git repository is https://go.googlesource.com/net.
-This repository uses Gerrit for code changes. To learn how to submit
-changes to this repository, see https://golang.org/doc/contribute.html.
The main issue tracker for the net repository is located at
-https://github.com/golang/go/issues. Prefix your issue with "x/net:" in the
+https://go.dev/issues. Prefix your issue with "x/net:" in the
subject line, so it is easy to find.
diff --git a/go.mod b/go.mod
index ffda816a88..26852e7822 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.27.0
- golang.org/x/sys v0.25.0
- golang.org/x/term v0.24.0
- golang.org/x/text v0.18.0
+ golang.org/x/crypto v0.31.0
+ golang.org/x/sys v0.28.0
+ golang.org/x/term v0.27.0
+ golang.org/x/text v0.21.0
)
diff --git a/go.sum b/go.sum
index a1a56bf3d0..43b5f03464 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
diff --git a/html/doc.go b/html/doc.go
index 3a7e5ab176..885c4c5936 100644
--- a/html/doc.go
+++ b/html/doc.go
@@ -78,16 +78,11 @@ example, to process each anchor node in depth-first order:
if err != nil {
// ...
}
- var f func(*html.Node)
- f = func(n *html.Node) {
+ for n := range doc.Descendants() {
if n.Type == html.ElementNode && n.Data == "a" {
// Do something with n...
}
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- f(c)
- }
}
- f(doc)
The relevant specifications include:
https://html.spec.whatwg.org/multipage/syntax.html and
diff --git a/html/doctype.go b/html/doctype.go
index c484e5a94f..bca3ae9a0c 100644
--- a/html/doctype.go
+++ b/html/doctype.go
@@ -87,7 +87,7 @@ func parseDoctype(s string) (n *Node, quirks bool) {
}
}
if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" &&
- strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" {
+ strings.EqualFold(lastAttr.Val, "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
quirks = true
}
}
diff --git a/html/example_test.go b/html/example_test.go
index 0b06ed7730..830f0b27af 100644
--- a/html/example_test.go
+++ b/html/example_test.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+//go:build go1.23
+
// This example demonstrates parsing HTML data and walking the resulting tree.
package html_test
@@ -11,6 +13,7 @@ import (
"strings"
"golang.org/x/net/html"
+ "golang.org/x/net/html/atom"
)
func ExampleParse() {
@@ -19,9 +22,8 @@ func ExampleParse() {
if err != nil {
log.Fatal(err)
}
- var f func(*html.Node)
- f = func(n *html.Node) {
- if n.Type == html.ElementNode && n.Data == "a" {
+ for n := range doc.Descendants() {
+ if n.Type == html.ElementNode && n.DataAtom == atom.A {
for _, a := range n.Attr {
if a.Key == "href" {
fmt.Println(a.Val)
@@ -29,11 +31,8 @@ func ExampleParse() {
}
}
}
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- f(c)
- }
}
- f(doc)
+
// Output:
// foo
// /bar/baz
diff --git a/html/foreign.go b/html/foreign.go
index 9da9e9dc42..e8515d8e88 100644
--- a/html/foreign.go
+++ b/html/foreign.go
@@ -40,8 +40,7 @@ func htmlIntegrationPoint(n *Node) bool {
if n.Data == "annotation-xml" {
for _, a := range n.Attr {
if a.Key == "encoding" {
- val := strings.ToLower(a.Val)
- if val == "text/html" || val == "application/xhtml+xml" {
+ if strings.EqualFold(a.Val, "text/html") || strings.EqualFold(a.Val, "application/xhtml+xml") {
return true
}
}
diff --git a/html/iter.go b/html/iter.go
new file mode 100644
index 0000000000..54be8fd30f
--- /dev/null
+++ b/html/iter.go
@@ -0,0 +1,56 @@
+// Copyright 2024 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.
+
+//go:build go1.23
+
+package html
+
+import "iter"
+
+// Ancestors returns an iterator over the ancestors of n, starting with n.Parent.
+//
+// Mutating a Node or its parents while iterating may have unexpected results.
+func (n *Node) Ancestors() iter.Seq[*Node] {
+ _ = n.Parent // eager nil check
+
+ return func(yield func(*Node) bool) {
+ for p := n.Parent; p != nil && yield(p); p = p.Parent {
+ }
+ }
+}
+
+// ChildNodes returns an iterator over the immediate children of n,
+// starting with n.FirstChild.
+//
+// Mutating a Node or its children while iterating may have unexpected results.
+func (n *Node) ChildNodes() iter.Seq[*Node] {
+ _ = n.FirstChild // eager nil check
+
+ return func(yield func(*Node) bool) {
+ for c := n.FirstChild; c != nil && yield(c); c = c.NextSibling {
+ }
+ }
+
+}
+
+// Descendants returns an iterator over all nodes recursively beneath
+// n, excluding n itself. Nodes are visited in depth-first preorder.
+//
+// Mutating a Node or its descendants while iterating may have unexpected results.
+func (n *Node) Descendants() iter.Seq[*Node] {
+ _ = n.FirstChild // eager nil check
+
+ return func(yield func(*Node) bool) {
+ n.descendants(yield)
+ }
+}
+
+func (n *Node) descendants(yield func(*Node) bool) bool {
+ for c := range n.ChildNodes() {
+ if !yield(c) || !c.descendants(yield) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/html/iter_test.go b/html/iter_test.go
new file mode 100644
index 0000000000..cca7f82f54
--- /dev/null
+++ b/html/iter_test.go
@@ -0,0 +1,96 @@
+// Copyright 2024 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.
+
+//go:build go1.23
+
+package html
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestNode_ChildNodes(t *testing.T) {
+ tests := []struct {
+ in string
+ want string
+ }{
+ {"", ""},
+ {" ", "a"},
+ {"a", "a"},
+ {" ", "a b"},
+ {"a c", "a b c"},
+ {"a d", "a b d"},
+ {"ce fi ", "a f g h"},
+ }
+ for _, test := range tests {
+ doc, err := Parse(strings.NewReader(test.in))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Drill to
+ n := doc.FirstChild.FirstChild.NextSibling
+ var results []string
+ for c := range n.ChildNodes() {
+ results = append(results, c.Data)
+ }
+ if got := strings.Join(results, " "); got != test.want {
+ t.Errorf("ChildNodes = %q, want %q", got, test.want)
+ }
+ }
+}
+
+func TestNode_Descendants(t *testing.T) {
+ tests := []struct {
+ in string
+ want string
+ }{
+ {"", ""},
+ {" ", "a"},
+ {" ", "a b"},
+ {"b ", "a b"},
+ {" ", "a b"},
+ {"b d ", "a b c d"},
+ {"b e ", "a b c d e"},
+ {"df gj ", "a b c d e f g h i j"},
+ }
+ for _, test := range tests {
+ doc, err := Parse(strings.NewReader(test.in))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Drill to
+ n := doc.FirstChild.FirstChild.NextSibling
+ var results []string
+ for c := range n.Descendants() {
+ results = append(results, c.Data)
+ }
+ if got := strings.Join(results, " "); got != test.want {
+ t.Errorf("Descendants = %q; want: %q", got, test.want)
+ }
+ }
+}
+
+func TestNode_Ancestors(t *testing.T) {
+ for _, size := range []int{0, 1, 2, 10, 100, 10_000} {
+ n := buildChain(size)
+ nParents := 0
+ for _ = range n.Ancestors() {
+ nParents++
+ }
+ if nParents != size {
+ t.Errorf("number of Ancestors = %d; want: %d", nParents, size)
+ }
+ }
+}
+
+func buildChain(size int) *Node {
+ child := new(Node)
+ for range size {
+ parent := child
+ child = new(Node)
+ parent.AppendChild(child)
+ }
+ return child
+}
diff --git a/html/node.go b/html/node.go
index 1350eef22c..77741a1950 100644
--- a/html/node.go
+++ b/html/node.go
@@ -38,6 +38,10 @@ var scopeMarker = Node{Type: scopeMarkerNode}
// that it looks like "a ",
"",
+ " ",
}
for _, src := range srcs {
// The next line shouldn't infinite-loop.
diff --git a/http2/client_conn_pool.go b/http2/client_conn_pool.go
index 780968d6c1..e81b73e6a7 100644
--- a/http2/client_conn_pool.go
+++ b/http2/client_conn_pool.go
@@ -8,8 +8,8 @@ package http2
import (
"context"
- "crypto/tls"
"errors"
+ "net"
"net/http"
"sync"
)
@@ -158,7 +158,7 @@ func (c *dialCall) dial(ctx context.Context, addr string) {
// This code decides which ones live or die.
// The return value used is whether c was used.
// c is never closed.
-func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) {
+func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c net.Conn) (used bool, err error) {
p.mu.Lock()
for _, cc := range p.conns[key] {
if cc.CanTakeNewRequest() {
@@ -194,8 +194,8 @@ type addConnCall struct {
err error
}
-func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
- cc, err := t.NewClientConn(tc)
+func (c *addConnCall) run(t *Transport, key string, nc net.Conn) {
+ cc, err := t.NewClientConn(nc)
p := c.p
p.mu.Lock()
diff --git a/http2/clientconn_test.go b/http2/clientconn_test.go
index 5533790992..42d9fd2dcc 100644
--- a/http2/clientconn_test.go
+++ b/http2/clientconn_test.go
@@ -10,6 +10,7 @@ package http2
import (
"bytes"
"context"
+ "crypto/tls"
"fmt"
"io"
"net/http"
@@ -112,27 +113,40 @@ func newTestClientConnFromClientConn(t *testing.T, cc *ClientConn) *testClientCo
cc: cc,
group: cc.t.transportTestHooks.group.(*synctestGroup),
}
- cli, srv := synctestNetPipe(tc.group)
+
+ // srv is the side controlled by the test.
+ var srv *synctestNetConn
+ if cc.tconn == nil {
+ // If cc.tconn is nil, we're being called with a new conn created by the
+ // Transport's client pool. This path skips dialing the server, and we
+ // create a test connection pair here.
+ cc.tconn, srv = synctestNetPipe(tc.group)
+ } else {
+ // If cc.tconn is non-nil, we're in a test which provides a conn to the
+ // Transport via a TLSNextProto hook. Extract the test connection pair.
+ if tc, ok := cc.tconn.(*tls.Conn); ok {
+ // Unwrap any *tls.Conn to the underlying net.Conn,
+ // to avoid dealing with encryption in tests.
+ cc.tconn = tc.NetConn()
+ }
+ srv = cc.tconn.(*synctestNetConn).peer
+ }
+
srv.SetReadDeadline(tc.group.Now())
srv.autoWait = true
tc.netconn = srv
tc.enc = hpack.NewEncoder(&tc.encbuf)
-
- // all writes and reads are finished.
- //
- // cli is the ClientConn's side, srv is the side controlled by the test.
- cc.tconn = cli
tc.fr = NewFramer(srv, srv)
tc.testConnFramer = testConnFramer{
t: t,
fr: tc.fr,
dec: hpack.NewDecoder(initialHeaderTableSize, nil),
}
-
tc.fr.SetMaxReadFrameSize(10 << 20)
t.Cleanup(func() {
tc.closeWrite()
})
+
return tc
}
@@ -148,7 +162,7 @@ func (tc *testClientConn) readClientPreface() {
}
}
-func newTestClientConn(t *testing.T, opts ...func(*Transport)) *testClientConn {
+func newTestClientConn(t *testing.T, opts ...any) *testClientConn {
t.Helper()
tt := newTestTransport(t, opts...)
@@ -486,7 +500,7 @@ type testTransport struct {
ccs []*testClientConn
}
-func newTestTransport(t *testing.T, opts ...func(*Transport)) *testTransport {
+func newTestTransport(t *testing.T, opts ...any) *testTransport {
tt := &testTransport{
t: t,
group: newSynctest(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
@@ -495,7 +509,17 @@ func newTestTransport(t *testing.T, opts ...func(*Transport)) *testTransport {
tr := &Transport{}
for _, o := range opts {
- o(tr)
+ switch o := o.(type) {
+ case func(*http.Transport):
+ if tr.t1 == nil {
+ tr.t1 = &http.Transport{}
+ }
+ o(tr.t1)
+ case func(*Transport):
+ o(tr)
+ case *Transport:
+ tr = o
+ }
}
tt.tr = tr
diff --git a/http2/config.go b/http2/config.go
new file mode 100644
index 0000000000..de58dfb8dc
--- /dev/null
+++ b/http2/config.go
@@ -0,0 +1,122 @@
+// Copyright 2024 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 http2
+
+import (
+ "math"
+ "net/http"
+ "time"
+)
+
+// http2Config is a package-internal version of net/http.HTTP2Config.
+//
+// http.HTTP2Config was added in Go 1.24.
+// When running with a version of net/http that includes HTTP2Config,
+// we merge the configuration with the fields in Transport or Server
+// to produce an http2Config.
+//
+// Zero valued fields in http2Config are interpreted as in the
+// net/http.HTTPConfig documentation.
+//
+// Precedence order for reconciling configurations is:
+//
+// - Use the net/http.{Server,Transport}.HTTP2Config value, when non-zero.
+// - Otherwise use the http2.{Server.Transport} value.
+// - If the resulting value is zero or out of range, use a default.
+type http2Config struct {
+ MaxConcurrentStreams uint32
+ MaxDecoderHeaderTableSize uint32
+ MaxEncoderHeaderTableSize uint32
+ MaxReadFrameSize uint32
+ MaxUploadBufferPerConnection int32
+ MaxUploadBufferPerStream int32
+ SendPingTimeout time.Duration
+ PingTimeout time.Duration
+ WriteByteTimeout time.Duration
+ PermitProhibitedCipherSuites bool
+ CountError func(errType string)
+}
+
+// configFromServer merges configuration settings from
+// net/http.Server.HTTP2Config and http2.Server.
+func configFromServer(h1 *http.Server, h2 *Server) http2Config {
+ conf := http2Config{
+ MaxConcurrentStreams: h2.MaxConcurrentStreams,
+ MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
+ MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
+ MaxReadFrameSize: h2.MaxReadFrameSize,
+ MaxUploadBufferPerConnection: h2.MaxUploadBufferPerConnection,
+ MaxUploadBufferPerStream: h2.MaxUploadBufferPerStream,
+ SendPingTimeout: h2.ReadIdleTimeout,
+ PingTimeout: h2.PingTimeout,
+ WriteByteTimeout: h2.WriteByteTimeout,
+ PermitProhibitedCipherSuites: h2.PermitProhibitedCipherSuites,
+ CountError: h2.CountError,
+ }
+ fillNetHTTPServerConfig(&conf, h1)
+ setConfigDefaults(&conf, true)
+ return conf
+}
+
+// configFromServer merges configuration settings from h2 and h2.t1.HTTP2
+// (the net/http Transport).
+func configFromTransport(h2 *Transport) http2Config {
+ conf := http2Config{
+ MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
+ MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
+ MaxReadFrameSize: h2.MaxReadFrameSize,
+ SendPingTimeout: h2.ReadIdleTimeout,
+ PingTimeout: h2.PingTimeout,
+ WriteByteTimeout: h2.WriteByteTimeout,
+ }
+
+ // Unlike most config fields, where out-of-range values revert to the default,
+ // Transport.MaxReadFrameSize clips.
+ if conf.MaxReadFrameSize < minMaxFrameSize {
+ conf.MaxReadFrameSize = minMaxFrameSize
+ } else if conf.MaxReadFrameSize > maxFrameSize {
+ conf.MaxReadFrameSize = maxFrameSize
+ }
+
+ if h2.t1 != nil {
+ fillNetHTTPTransportConfig(&conf, h2.t1)
+ }
+ setConfigDefaults(&conf, false)
+ return conf
+}
+
+func setDefault[T ~int | ~int32 | ~uint32 | ~int64](v *T, minval, maxval, defval T) {
+ if *v < minval || *v > maxval {
+ *v = defval
+ }
+}
+
+func setConfigDefaults(conf *http2Config, server bool) {
+ setDefault(&conf.MaxConcurrentStreams, 1, math.MaxUint32, defaultMaxStreams)
+ setDefault(&conf.MaxEncoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
+ setDefault(&conf.MaxDecoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
+ if server {
+ setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, 1<<20)
+ } else {
+ setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, transportDefaultConnFlow)
+ }
+ if server {
+ setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, 1<<20)
+ } else {
+ setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, transportDefaultStreamFlow)
+ }
+ setDefault(&conf.MaxReadFrameSize, minMaxFrameSize, maxFrameSize, defaultMaxReadFrameSize)
+ setDefault(&conf.PingTimeout, 1, math.MaxInt64, 15*time.Second)
+}
+
+// adjustHTTP1MaxHeaderSize converts a limit in bytes on the size of an HTTP/1 header
+// to an HTTP/2 MAX_HEADER_LIST_SIZE value.
+func adjustHTTP1MaxHeaderSize(n int64) int64 {
+ // http2's count is in a slightly different unit and includes 32 bytes per pair.
+ // So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
+ const perFieldOverhead = 32 // per http2 spec
+ const typicalHeaders = 10 // conservative
+ return n + typicalHeaders*perFieldOverhead
+}
diff --git a/http2/config_go124.go b/http2/config_go124.go
new file mode 100644
index 0000000000..e3784123c8
--- /dev/null
+++ b/http2/config_go124.go
@@ -0,0 +1,61 @@
+// Copyright 2024 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.
+
+//go:build go1.24
+
+package http2
+
+import "net/http"
+
+// fillNetHTTPServerConfig sets fields in conf from srv.HTTP2.
+func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {
+ fillNetHTTPConfig(conf, srv.HTTP2)
+}
+
+// fillNetHTTPServerConfig sets fields in conf from tr.HTTP2.
+func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {
+ fillNetHTTPConfig(conf, tr.HTTP2)
+}
+
+func fillNetHTTPConfig(conf *http2Config, h2 *http.HTTP2Config) {
+ if h2 == nil {
+ return
+ }
+ if h2.MaxConcurrentStreams != 0 {
+ conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
+ }
+ if h2.MaxEncoderHeaderTableSize != 0 {
+ conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize)
+ }
+ if h2.MaxDecoderHeaderTableSize != 0 {
+ conf.MaxDecoderHeaderTableSize = uint32(h2.MaxDecoderHeaderTableSize)
+ }
+ if h2.MaxConcurrentStreams != 0 {
+ conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
+ }
+ if h2.MaxReadFrameSize != 0 {
+ conf.MaxReadFrameSize = uint32(h2.MaxReadFrameSize)
+ }
+ if h2.MaxReceiveBufferPerConnection != 0 {
+ conf.MaxUploadBufferPerConnection = int32(h2.MaxReceiveBufferPerConnection)
+ }
+ if h2.MaxReceiveBufferPerStream != 0 {
+ conf.MaxUploadBufferPerStream = int32(h2.MaxReceiveBufferPerStream)
+ }
+ if h2.SendPingTimeout != 0 {
+ conf.SendPingTimeout = h2.SendPingTimeout
+ }
+ if h2.PingTimeout != 0 {
+ conf.PingTimeout = h2.PingTimeout
+ }
+ if h2.WriteByteTimeout != 0 {
+ conf.WriteByteTimeout = h2.WriteByteTimeout
+ }
+ if h2.PermitProhibitedCipherSuites {
+ conf.PermitProhibitedCipherSuites = true
+ }
+ if h2.CountError != nil {
+ conf.CountError = h2.CountError
+ }
+}
diff --git a/http2/config_pre_go124.go b/http2/config_pre_go124.go
new file mode 100644
index 0000000000..060fd6c64c
--- /dev/null
+++ b/http2/config_pre_go124.go
@@ -0,0 +1,16 @@
+// Copyright 2024 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.
+
+//go:build !go1.24
+
+package http2
+
+import "net/http"
+
+// Pre-Go 1.24 fallback.
+// The Server.HTTP2 and Transport.HTTP2 config fields were added in Go 1.24.
+
+func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) {}
+
+func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) {}
diff --git a/http2/config_test.go b/http2/config_test.go
new file mode 100644
index 0000000000..b8e7a7b043
--- /dev/null
+++ b/http2/config_test.go
@@ -0,0 +1,95 @@
+// Copyright 2024 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.
+
+//go:build go1.24
+
+package http2
+
+import (
+ "net/http"
+ "testing"
+ "time"
+)
+
+func TestConfigServerSettings(t *testing.T) {
+ config := &http.HTTP2Config{
+ MaxConcurrentStreams: 1,
+ MaxDecoderHeaderTableSize: 1<<20 + 2,
+ MaxEncoderHeaderTableSize: 1<<20 + 3,
+ MaxReadFrameSize: 1<<20 + 4,
+ MaxReceiveBufferPerConnection: 64<<10 + 5,
+ MaxReceiveBufferPerStream: 64<<10 + 6,
+ }
+ const maxHeaderBytes = 4096 + 7
+ st := newServerTester(t, nil, func(s *http.Server) {
+ s.MaxHeaderBytes = maxHeaderBytes
+ s.HTTP2 = config
+ })
+ st.writePreface()
+ st.writeSettings()
+ st.wantSettings(map[SettingID]uint32{
+ SettingMaxConcurrentStreams: uint32(config.MaxConcurrentStreams),
+ SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
+ SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
+ SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
+ SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
+ })
+}
+
+func TestConfigTransportSettings(t *testing.T) {
+ config := &http.HTTP2Config{
+ MaxConcurrentStreams: 1, // ignored by Transport
+ MaxDecoderHeaderTableSize: 1<<20 + 2,
+ MaxEncoderHeaderTableSize: 1<<20 + 3,
+ MaxReadFrameSize: 1<<20 + 4,
+ MaxReceiveBufferPerConnection: 64<<10 + 5,
+ MaxReceiveBufferPerStream: 64<<10 + 6,
+ }
+ const maxHeaderBytes = 4096 + 7
+ tc := newTestClientConn(t, func(tr *http.Transport) {
+ tr.HTTP2 = config
+ tr.MaxResponseHeaderBytes = maxHeaderBytes
+ })
+ tc.wantSettings(map[SettingID]uint32{
+ SettingHeaderTableSize: uint32(config.MaxDecoderHeaderTableSize),
+ SettingInitialWindowSize: uint32(config.MaxReceiveBufferPerStream),
+ SettingMaxFrameSize: uint32(config.MaxReadFrameSize),
+ SettingMaxHeaderListSize: maxHeaderBytes + (32 * 10),
+ })
+ tc.wantWindowUpdate(0, uint32(config.MaxReceiveBufferPerConnection))
+}
+
+func TestConfigPingTimeoutServer(t *testing.T) {
+ st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
+ }, func(s *Server) {
+ s.ReadIdleTimeout = 2 * time.Second
+ s.PingTimeout = 3 * time.Second
+ })
+ st.greet()
+
+ st.advance(2 * time.Second)
+ _ = readFrame[*PingFrame](t, st)
+ st.advance(3 * time.Second)
+ st.wantClosed()
+}
+
+func TestConfigPingTimeoutTransport(t *testing.T) {
+ tc := newTestClientConn(t, func(tr *Transport) {
+ tr.ReadIdleTimeout = 2 * time.Second
+ tr.PingTimeout = 3 * time.Second
+ })
+ tc.greet()
+
+ req, _ := http.NewRequest("GET", "https://dummy.tld/", nil)
+ rt := tc.roundTrip(req)
+ tc.wantFrameType(FrameHeaders)
+
+ tc.advance(2 * time.Second)
+ tc.wantFrameType(FramePing)
+ tc.advance(3 * time.Second)
+ err := rt.err()
+ if err == nil {
+ t.Fatalf("expected connection to close")
+ }
+}
diff --git a/http2/connframes_test.go b/http2/connframes_test.go
index 7db8b74e2e..2c4532571a 100644
--- a/http2/connframes_test.go
+++ b/http2/connframes_test.go
@@ -6,7 +6,6 @@ package http2
import (
"bytes"
- "context"
"io"
"net/http"
"os"
@@ -262,6 +261,24 @@ func (tf *testConnFramer) wantRSTStream(streamID uint32, code ErrCode) {
}
}
+func (tf *testConnFramer) wantSettings(want map[SettingID]uint32) {
+ fr := readFrame[*SettingsFrame](tf.t, tf)
+ if fr.Header().Flags.Has(FlagSettingsAck) {
+ tf.t.Errorf("got SETTINGS frame with ACK set, want no ACK")
+ }
+ for wantID, wantVal := range want {
+ gotVal, ok := fr.Value(wantID)
+ if !ok {
+ tf.t.Errorf("SETTINGS: %v is not set, want %v", wantID, wantVal)
+ } else if gotVal != wantVal {
+ tf.t.Errorf("SETTINGS: %v is %v, want %v", wantID, gotVal, wantVal)
+ }
+ }
+ if tf.t.Failed() {
+ tf.t.Fatalf("%v", fr)
+ }
+}
+
func (tf *testConnFramer) wantSettingsAck() {
tf.t.Helper()
fr := readFrame[*SettingsFrame](tf.t, tf)
@@ -295,7 +312,7 @@ func (tf *testConnFramer) wantClosed() {
if err == nil {
tf.t.Fatalf("got unexpected frame (want closed connection): %v", fr)
}
- if err == context.DeadlineExceeded {
+ if err == os.ErrDeadlineExceeded {
tf.t.Fatalf("connection is not closed; want it to be")
}
}
@@ -306,7 +323,7 @@ func (tf *testConnFramer) wantIdle() {
if err == nil {
tf.t.Fatalf("got unexpected frame (want idle connection): %v", fr)
}
- if err != context.DeadlineExceeded {
+ if err != os.ErrDeadlineExceeded {
tf.t.Fatalf("got unexpected frame error (want idle connection): %v", err)
}
}
diff --git a/http2/frame.go b/http2/frame.go
index 105c3b279c..81faec7e75 100644
--- a/http2/frame.go
+++ b/http2/frame.go
@@ -1490,7 +1490,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
pf := mh.PseudoFields()
for i, hf := range pf {
switch hf.Name {
- case ":method", ":path", ":scheme", ":authority":
+ case ":method", ":path", ":scheme", ":authority", ":protocol":
isRequest = true
case ":status":
isResponse = true
@@ -1498,7 +1498,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
return pseudoHeaderError(hf.Name)
}
// Check for duplicates.
- // This would be a bad algorithm, but N is 4.
+ // This would be a bad algorithm, but N is 5.
// And this doesn't allocate.
for _, hf2 := range pf[:i] {
if hf.Name == hf2.Name {
diff --git a/http2/http2.go b/http2/http2.go
index 003e649f30..c7601c909f 100644
--- a/http2/http2.go
+++ b/http2/http2.go
@@ -19,8 +19,9 @@ import (
"bufio"
"context"
"crypto/tls"
+ "errors"
"fmt"
- "io"
+ "net"
"net/http"
"os"
"sort"
@@ -33,10 +34,11 @@ import (
)
var (
- VerboseLogs bool
- logFrameWrites bool
- logFrameReads bool
- inTests bool
+ VerboseLogs bool
+ logFrameWrites bool
+ logFrameReads bool
+ inTests bool
+ disableExtendedConnectProtocol bool
)
func init() {
@@ -49,6 +51,9 @@ func init() {
logFrameWrites = true
logFrameReads = true
}
+ if strings.Contains(e, "http2xconnect=0") {
+ disableExtendedConnectProtocol = true
+ }
}
const (
@@ -140,6 +145,10 @@ func (s Setting) Valid() error {
if s.Val < 16384 || s.Val > 1<<24-1 {
return ConnectionError(ErrCodeProtocol)
}
+ case SettingEnableConnectProtocol:
+ if s.Val != 1 && s.Val != 0 {
+ return ConnectionError(ErrCodeProtocol)
+ }
}
return nil
}
@@ -149,21 +158,23 @@ func (s Setting) Valid() error {
type SettingID uint16
const (
- SettingHeaderTableSize SettingID = 0x1
- SettingEnablePush SettingID = 0x2
- SettingMaxConcurrentStreams SettingID = 0x3
- SettingInitialWindowSize SettingID = 0x4
- SettingMaxFrameSize SettingID = 0x5
- SettingMaxHeaderListSize SettingID = 0x6
+ SettingHeaderTableSize SettingID = 0x1
+ SettingEnablePush SettingID = 0x2
+ SettingMaxConcurrentStreams SettingID = 0x3
+ SettingInitialWindowSize SettingID = 0x4
+ SettingMaxFrameSize SettingID = 0x5
+ SettingMaxHeaderListSize SettingID = 0x6
+ SettingEnableConnectProtocol SettingID = 0x8
)
var settingName = map[SettingID]string{
- SettingHeaderTableSize: "HEADER_TABLE_SIZE",
- SettingEnablePush: "ENABLE_PUSH",
- SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS",
- SettingInitialWindowSize: "INITIAL_WINDOW_SIZE",
- SettingMaxFrameSize: "MAX_FRAME_SIZE",
- SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE",
+ SettingHeaderTableSize: "HEADER_TABLE_SIZE",
+ SettingEnablePush: "ENABLE_PUSH",
+ SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS",
+ SettingInitialWindowSize: "INITIAL_WINDOW_SIZE",
+ SettingMaxFrameSize: "MAX_FRAME_SIZE",
+ SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE",
+ SettingEnableConnectProtocol: "ENABLE_CONNECT_PROTOCOL",
}
func (s SettingID) String() string {
@@ -237,13 +248,19 @@ func (cw closeWaiter) Wait() {
// Its buffered writer is lazily allocated as needed, to minimize
// idle memory usage with many connections.
type bufferedWriter struct {
- _ incomparable
- w io.Writer // immutable
- bw *bufio.Writer // non-nil when data is buffered
+ _ incomparable
+ group synctestGroupInterface // immutable
+ conn net.Conn // immutable
+ bw *bufio.Writer // non-nil when data is buffered
+ byteTimeout time.Duration // immutable, WriteByteTimeout
}
-func newBufferedWriter(w io.Writer) *bufferedWriter {
- return &bufferedWriter{w: w}
+func newBufferedWriter(group synctestGroupInterface, conn net.Conn, timeout time.Duration) *bufferedWriter {
+ return &bufferedWriter{
+ group: group,
+ conn: conn,
+ byteTimeout: timeout,
+ }
}
// bufWriterPoolBufferSize is the size of bufio.Writer's
@@ -270,7 +287,7 @@ func (w *bufferedWriter) Available() int {
func (w *bufferedWriter) Write(p []byte) (n int, err error) {
if w.bw == nil {
bw := bufWriterPool.Get().(*bufio.Writer)
- bw.Reset(w.w)
+ bw.Reset((*bufferedWriterTimeoutWriter)(w))
w.bw = bw
}
return w.bw.Write(p)
@@ -288,6 +305,38 @@ func (w *bufferedWriter) Flush() error {
return err
}
+type bufferedWriterTimeoutWriter bufferedWriter
+
+func (w *bufferedWriterTimeoutWriter) Write(p []byte) (n int, err error) {
+ return writeWithByteTimeout(w.group, w.conn, w.byteTimeout, p)
+}
+
+// writeWithByteTimeout writes to conn.
+// If more than timeout passes without any bytes being written to the connection,
+// the write fails.
+func writeWithByteTimeout(group synctestGroupInterface, conn net.Conn, timeout time.Duration, p []byte) (n int, err error) {
+ if timeout <= 0 {
+ return conn.Write(p)
+ }
+ for {
+ var now time.Time
+ if group == nil {
+ now = time.Now()
+ } else {
+ now = group.Now()
+ }
+ conn.SetWriteDeadline(now.Add(timeout))
+ nn, err := conn.Write(p[n:])
+ n += nn
+ if n == len(p) || nn == 0 || !errors.Is(err, os.ErrDeadlineExceeded) {
+ // Either we finished the write, made no progress, or hit the deadline.
+ // Whichever it is, we're done now.
+ conn.SetWriteDeadline(time.Time{})
+ return n, err
+ }
+ }
+}
+
func mustUint31(v int32) uint32 {
if v < 0 || v > 2147483647 {
panic("out of range")
diff --git a/http2/http2_test.go b/http2/http2_test.go
index b7c946b982..b1e71f1532 100644
--- a/http2/http2_test.go
+++ b/http2/http2_test.go
@@ -283,3 +283,11 @@ func TestNoUnicodeStrings(t *testing.T) {
t.Fatal(err)
}
}
+
+// must returns v if err is nil, or panics otherwise.
+func must[T any](v T, err error) T {
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
diff --git a/http2/netconn_test.go b/http2/netconn_test.go
index 8a61fbef10..0f1b5fb1f3 100644
--- a/http2/netconn_test.go
+++ b/http2/netconn_test.go
@@ -28,8 +28,11 @@ func synctestNetPipe(group *synctestGroup) (r, w *synctestNetConn) {
s2addr := net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8001"))
s1 := newSynctestNetConnHalf(s1addr)
s2 := newSynctestNetConnHalf(s2addr)
- return &synctestNetConn{group: group, loc: s1, rem: s2},
- &synctestNetConn{group: group, loc: s2, rem: s1}
+ r = &synctestNetConn{group: group, loc: s1, rem: s2}
+ w = &synctestNetConn{group: group, loc: s2, rem: s1}
+ r.peer = w
+ w.peer = r
+ return r, w
}
// A synctestNetConn is one endpoint of the connection created by synctestNetPipe.
@@ -43,6 +46,9 @@ type synctestNetConn struct {
// When set, group.Wait is automatically called before reads and after writes.
autoWait bool
+
+ // peer is the other endpoint.
+ peer *synctestNetConn
}
// Read reads data from the connection.
diff --git a/http2/server.go b/http2/server.go
index 6c349f3ec6..b55547aec6 100644
--- a/http2/server.go
+++ b/http2/server.go
@@ -29,6 +29,7 @@ import (
"bufio"
"bytes"
"context"
+ "crypto/rand"
"crypto/tls"
"errors"
"fmt"
@@ -52,10 +53,14 @@ import (
)
const (
- prefaceTimeout = 10 * time.Second
- firstSettingsTimeout = 2 * time.Second // should be in-flight with preface anyway
- handlerChunkWriteSize = 4 << 10
- defaultMaxStreams = 250 // TODO: make this 100 as the GFE seems to?
+ prefaceTimeout = 10 * time.Second
+ firstSettingsTimeout = 2 * time.Second // should be in-flight with preface anyway
+ handlerChunkWriteSize = 4 << 10
+ defaultMaxStreams = 250 // TODO: make this 100 as the GFE seems to?
+
+ // maxQueuedControlFrames is the maximum number of control frames like
+ // SETTINGS, PING and RST_STREAM that will be queued for writing before
+ // the connection is closed to prevent memory exhaustion attacks.
maxQueuedControlFrames = 10000
)
@@ -127,6 +132,22 @@ type Server struct {
// If zero or negative, there is no timeout.
IdleTimeout time.Duration
+ // ReadIdleTimeout is the timeout after which a health check using a ping
+ // frame will be carried out if no frame is received on the connection.
+ // If zero, no health check is performed.
+ ReadIdleTimeout time.Duration
+
+ // PingTimeout is the timeout after which the connection will be closed
+ // if a response to a ping is not received.
+ // If zero, a default of 15 seconds is used.
+ PingTimeout time.Duration
+
+ // WriteByteTimeout is the timeout after which a connection will be
+ // closed if no data can be written to it. The timeout begins when data is
+ // available to write, and is extended whenever any bytes are written.
+ // If zero or negative, there is no timeout.
+ WriteByteTimeout time.Duration
+
// MaxUploadBufferPerConnection is the size of the initial flow
// control window for each connections. The HTTP/2 spec does not
// allow this to be smaller than 65535 or larger than 2^32-1.
@@ -189,57 +210,6 @@ func (s *Server) afterFunc(d time.Duration, f func()) timer {
return timeTimer{time.AfterFunc(d, f)}
}
-func (s *Server) initialConnRecvWindowSize() int32 {
- if s.MaxUploadBufferPerConnection >= initialWindowSize {
- return s.MaxUploadBufferPerConnection
- }
- return 1 << 20
-}
-
-func (s *Server) initialStreamRecvWindowSize() int32 {
- if s.MaxUploadBufferPerStream > 0 {
- return s.MaxUploadBufferPerStream
- }
- return 1 << 20
-}
-
-func (s *Server) maxReadFrameSize() uint32 {
- if v := s.MaxReadFrameSize; v >= minMaxFrameSize && v <= maxFrameSize {
- return v
- }
- return defaultMaxReadFrameSize
-}
-
-func (s *Server) maxConcurrentStreams() uint32 {
- if v := s.MaxConcurrentStreams; v > 0 {
- return v
- }
- return defaultMaxStreams
-}
-
-func (s *Server) maxDecoderHeaderTableSize() uint32 {
- if v := s.MaxDecoderHeaderTableSize; v > 0 {
- return v
- }
- return initialHeaderTableSize
-}
-
-func (s *Server) maxEncoderHeaderTableSize() uint32 {
- if v := s.MaxEncoderHeaderTableSize; v > 0 {
- return v
- }
- return initialHeaderTableSize
-}
-
-// maxQueuedControlFrames is the maximum number of control frames like
-// SETTINGS, PING and RST_STREAM that will be queued for writing before
-// the connection is closed to prevent memory exhaustion attacks.
-func (s *Server) maxQueuedControlFrames() int {
- // TODO: if anybody asks, add a Server field, and remember to define the
- // behavior of negative values.
- return maxQueuedControlFrames
-}
-
type serverInternalState struct {
mu sync.Mutex
activeConns map[*serverConn]struct{}
@@ -336,7 +306,7 @@ func ConfigureServer(s *http.Server, conf *Server) error {
if s.TLSNextProto == nil {
s.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){}
}
- protoHandler := func(hs *http.Server, c *tls.Conn, h http.Handler) {
+ protoHandler := func(hs *http.Server, c net.Conn, h http.Handler, sawClientPreface bool) {
if testHookOnConn != nil {
testHookOnConn()
}
@@ -353,12 +323,31 @@ func ConfigureServer(s *http.Server, conf *Server) error {
ctx = bc.BaseContext()
}
conf.ServeConn(c, &ServeConnOpts{
- Context: ctx,
- Handler: h,
- BaseConfig: hs,
+ Context: ctx,
+ Handler: h,
+ BaseConfig: hs,
+ SawClientPreface: sawClientPreface,
})
}
- s.TLSNextProto[NextProtoTLS] = protoHandler
+ s.TLSNextProto[NextProtoTLS] = func(hs *http.Server, c *tls.Conn, h http.Handler) {
+ protoHandler(hs, c, h, false)
+ }
+ // The "unencrypted_http2" TLSNextProto key is used to pass off non-TLS HTTP/2 conns.
+ //
+ // A connection passed in this method has already had the HTTP/2 preface read from it.
+ s.TLSNextProto[nextProtoUnencryptedHTTP2] = func(hs *http.Server, c *tls.Conn, h http.Handler) {
+ nc, err := unencryptedNetConnFromTLSConn(c)
+ if err != nil {
+ if lg := hs.ErrorLog; lg != nil {
+ lg.Print(err)
+ } else {
+ log.Print(err)
+ }
+ go c.Close()
+ return
+ }
+ protoHandler(hs, nc, h, true)
+ }
return nil
}
@@ -440,13 +429,15 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
baseCtx, cancel := serverConnBaseContext(c, opts)
defer cancel()
+ http1srv := opts.baseConfig()
+ conf := configFromServer(http1srv, s)
sc := &serverConn{
srv: s,
- hs: opts.baseConfig(),
+ hs: http1srv,
conn: c,
baseCtx: baseCtx,
remoteAddrStr: c.RemoteAddr().String(),
- bw: newBufferedWriter(c),
+ bw: newBufferedWriter(s.group, c, conf.WriteByteTimeout),
handler: opts.handler(),
streams: make(map[uint32]*stream),
readFrameCh: make(chan readFrameResult),
@@ -456,9 +447,12 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
bodyReadCh: make(chan bodyReadMsg), // buffering doesn't matter either way
doneServing: make(chan struct{}),
clientMaxStreams: math.MaxUint32, // Section 6.5.2: "Initially, there is no limit to this value"
- advMaxStreams: s.maxConcurrentStreams(),
+ advMaxStreams: conf.MaxConcurrentStreams,
initialStreamSendWindowSize: initialWindowSize,
+ initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream,
maxFrameSize: initialMaxFrameSize,
+ pingTimeout: conf.PingTimeout,
+ countErrorFunc: conf.CountError,
serveG: newGoroutineLock(),
pushEnabled: true,
sawClientPreface: opts.SawClientPreface,
@@ -491,15 +485,15 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
sc.flow.add(initialWindowSize)
sc.inflow.init(initialWindowSize)
sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf)
- sc.hpackEncoder.SetMaxDynamicTableSizeLimit(s.maxEncoderHeaderTableSize())
+ sc.hpackEncoder.SetMaxDynamicTableSizeLimit(conf.MaxEncoderHeaderTableSize)
fr := NewFramer(sc.bw, c)
- if s.CountError != nil {
- fr.countError = s.CountError
+ if conf.CountError != nil {
+ fr.countError = conf.CountError
}
- fr.ReadMetaHeaders = hpack.NewDecoder(s.maxDecoderHeaderTableSize(), nil)
+ fr.ReadMetaHeaders = hpack.NewDecoder(conf.MaxDecoderHeaderTableSize, nil)
fr.MaxHeaderListSize = sc.maxHeaderListSize()
- fr.SetMaxReadFrameSize(s.maxReadFrameSize())
+ fr.SetMaxReadFrameSize(conf.MaxReadFrameSize)
sc.framer = fr
if tc, ok := c.(connectionStater); ok {
@@ -532,7 +526,7 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
// So for now, do nothing here again.
}
- if !s.PermitProhibitedCipherSuites && isBadCipher(sc.tlsState.CipherSuite) {
+ if !conf.PermitProhibitedCipherSuites && isBadCipher(sc.tlsState.CipherSuite) {
// "Endpoints MAY choose to generate a connection error
// (Section 5.4.1) of type INADEQUATE_SECURITY if one of
// the prohibited cipher suites are negotiated."
@@ -569,7 +563,7 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
opts.UpgradeRequest = nil
}
- sc.serve()
+ sc.serve(conf)
}
func serverConnBaseContext(c net.Conn, opts *ServeConnOpts) (ctx context.Context, cancel func()) {
@@ -609,6 +603,7 @@ type serverConn struct {
tlsState *tls.ConnectionState // shared by all handlers, like net/http
remoteAddrStr string
writeSched WriteScheduler
+ countErrorFunc func(errType string)
// Everything following is owned by the serve loop; use serveG.check():
serveG goroutineLock // used to verify funcs are on serve()
@@ -628,6 +623,7 @@ type serverConn struct {
streams map[uint32]*stream
unstartedHandlers []unstartedHandler
initialStreamSendWindowSize int32
+ initialStreamRecvWindowSize int32
maxFrameSize int32
peerMaxHeaderListSize uint32 // zero means unknown (default)
canonHeader map[string]string // http2-lower-case -> Go-Canonical-Case
@@ -638,9 +634,14 @@ type serverConn struct {
inGoAway bool // we've started to or sent GOAWAY
inFrameScheduleLoop bool // whether we're in the scheduleFrameWrite loop
needToSendGoAway bool // we need to schedule a GOAWAY frame write
+ pingSent bool
+ sentPingData [8]byte
goAwayCode ErrCode
shutdownTimer timer // nil until used
idleTimer timer // nil if unused
+ readIdleTimeout time.Duration
+ pingTimeout time.Duration
+ readIdleTimer timer // nil if unused
// Owned by the writeFrameAsync goroutine:
headerWriteBuf bytes.Buffer
@@ -655,11 +656,7 @@ func (sc *serverConn) maxHeaderListSize() uint32 {
if n <= 0 {
n = http.DefaultMaxHeaderBytes
}
- // http2's count is in a slightly different unit and includes 32 bytes per pair.
- // So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
- const perFieldOverhead = 32 // per http2 spec
- const typicalHeaders = 10 // conservative
- return uint32(n + typicalHeaders*perFieldOverhead)
+ return uint32(adjustHTTP1MaxHeaderSize(int64(n)))
}
func (sc *serverConn) curOpenStreams() uint32 {
@@ -923,7 +920,7 @@ func (sc *serverConn) notePanic() {
}
}
-func (sc *serverConn) serve() {
+func (sc *serverConn) serve(conf http2Config) {
sc.serveG.check()
defer sc.notePanic()
defer sc.conn.Close()
@@ -935,20 +932,24 @@ func (sc *serverConn) serve() {
sc.vlogf("http2: server connection from %v on %p", sc.conn.RemoteAddr(), sc.hs)
}
+ settings := writeSettings{
+ {SettingMaxFrameSize, conf.MaxReadFrameSize},
+ {SettingMaxConcurrentStreams, sc.advMaxStreams},
+ {SettingMaxHeaderListSize, sc.maxHeaderListSize()},
+ {SettingHeaderTableSize, conf.MaxDecoderHeaderTableSize},
+ {SettingInitialWindowSize, uint32(sc.initialStreamRecvWindowSize)},
+ }
+ if !disableExtendedConnectProtocol {
+ settings = append(settings, Setting{SettingEnableConnectProtocol, 1})
+ }
sc.writeFrame(FrameWriteRequest{
- write: writeSettings{
- {SettingMaxFrameSize, sc.srv.maxReadFrameSize()},
- {SettingMaxConcurrentStreams, sc.advMaxStreams},
- {SettingMaxHeaderListSize, sc.maxHeaderListSize()},
- {SettingHeaderTableSize, sc.srv.maxDecoderHeaderTableSize()},
- {SettingInitialWindowSize, uint32(sc.srv.initialStreamRecvWindowSize())},
- },
+ write: settings,
})
sc.unackedSettings++
// Each connection starts with initialWindowSize inflow tokens.
// If a higher value is configured, we add more tokens.
- if diff := sc.srv.initialConnRecvWindowSize() - initialWindowSize; diff > 0 {
+ if diff := conf.MaxUploadBufferPerConnection - initialWindowSize; diff > 0 {
sc.sendWindowUpdate(nil, int(diff))
}
@@ -968,11 +969,18 @@ func (sc *serverConn) serve() {
defer sc.idleTimer.Stop()
}
+ if conf.SendPingTimeout > 0 {
+ sc.readIdleTimeout = conf.SendPingTimeout
+ sc.readIdleTimer = sc.srv.afterFunc(conf.SendPingTimeout, sc.onReadIdleTimer)
+ defer sc.readIdleTimer.Stop()
+ }
+
go sc.readFrames() // closed by defer sc.conn.Close above
settingsTimer := sc.srv.afterFunc(firstSettingsTimeout, sc.onSettingsTimer)
defer settingsTimer.Stop()
+ lastFrameTime := sc.srv.now()
loopNum := 0
for {
loopNum++
@@ -986,6 +994,7 @@ func (sc *serverConn) serve() {
case res := <-sc.wroteFrameCh:
sc.wroteFrame(res)
case res := <-sc.readFrameCh:
+ lastFrameTime = sc.srv.now()
// Process any written frames before reading new frames from the client since a
// written frame could have triggered a new stream to be started.
if sc.writingFrameAsync {
@@ -1017,6 +1026,8 @@ func (sc *serverConn) serve() {
case idleTimerMsg:
sc.vlogf("connection is idle")
sc.goAway(ErrCodeNo)
+ case readIdleTimerMsg:
+ sc.handlePingTimer(lastFrameTime)
case shutdownTimerMsg:
sc.vlogf("GOAWAY close timer fired; closing conn from %v", sc.conn.RemoteAddr())
return
@@ -1039,7 +1050,7 @@ func (sc *serverConn) serve() {
// If the peer is causing us to generate a lot of control frames,
// but not reading them from us, assume they are trying to make us
// run out of memory.
- if sc.queuedControlFrames > sc.srv.maxQueuedControlFrames() {
+ if sc.queuedControlFrames > maxQueuedControlFrames {
sc.vlogf("http2: too many control frames in send queue, closing connection")
return
}
@@ -1055,12 +1066,39 @@ func (sc *serverConn) serve() {
}
}
+func (sc *serverConn) handlePingTimer(lastFrameReadTime time.Time) {
+ if sc.pingSent {
+ sc.vlogf("timeout waiting for PING response")
+ sc.conn.Close()
+ return
+ }
+
+ pingAt := lastFrameReadTime.Add(sc.readIdleTimeout)
+ now := sc.srv.now()
+ if pingAt.After(now) {
+ // We received frames since arming the ping timer.
+ // Reset it for the next possible timeout.
+ sc.readIdleTimer.Reset(pingAt.Sub(now))
+ return
+ }
+
+ sc.pingSent = true
+ // Ignore crypto/rand.Read errors: It generally can't fail, and worse case if it does
+ // is we send a PING frame containing 0s.
+ _, _ = rand.Read(sc.sentPingData[:])
+ sc.writeFrame(FrameWriteRequest{
+ write: &writePing{data: sc.sentPingData},
+ })
+ sc.readIdleTimer.Reset(sc.pingTimeout)
+}
+
type serverMessage int
// Message values sent to serveMsgCh.
var (
settingsTimerMsg = new(serverMessage)
idleTimerMsg = new(serverMessage)
+ readIdleTimerMsg = new(serverMessage)
shutdownTimerMsg = new(serverMessage)
gracefulShutdownMsg = new(serverMessage)
handlerDoneMsg = new(serverMessage)
@@ -1068,6 +1106,7 @@ var (
func (sc *serverConn) onSettingsTimer() { sc.sendServeMsg(settingsTimerMsg) }
func (sc *serverConn) onIdleTimer() { sc.sendServeMsg(idleTimerMsg) }
+func (sc *serverConn) onReadIdleTimer() { sc.sendServeMsg(readIdleTimerMsg) }
func (sc *serverConn) onShutdownTimer() { sc.sendServeMsg(shutdownTimerMsg) }
func (sc *serverConn) sendServeMsg(msg interface{}) {
@@ -1320,6 +1359,10 @@ func (sc *serverConn) wroteFrame(res frameWriteResult) {
sc.writingFrame = false
sc.writingFrameAsync = false
+ if res.err != nil {
+ sc.conn.Close()
+ }
+
wr := res.wr
if writeEndsStream(wr.write) {
@@ -1594,6 +1637,11 @@ func (sc *serverConn) processFrame(f Frame) error {
func (sc *serverConn) processPing(f *PingFrame) error {
sc.serveG.check()
if f.IsAck() {
+ if sc.pingSent && sc.sentPingData == f.Data {
+ // This is a response to a PING we sent.
+ sc.pingSent = false
+ sc.readIdleTimer.Reset(sc.readIdleTimeout)
+ }
// 6.7 PING: " An endpoint MUST NOT respond to PING frames
// containing this flag."
return nil
@@ -1757,6 +1805,9 @@ func (sc *serverConn) processSetting(s Setting) error {
sc.maxFrameSize = int32(s.Val) // the maximum valid s.Val is < 2^31
case SettingMaxHeaderListSize:
sc.peerMaxHeaderListSize = s.Val
+ case SettingEnableConnectProtocol:
+ // Receipt of this parameter by a server does not
+ // have any impact
default:
// Unknown setting: "An endpoint that receives a SETTINGS
// frame with any unknown or unsupported identifier MUST
@@ -2160,7 +2211,7 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState) *stream
st.cw.Init()
st.flow.conn = &sc.flow // link to conn-level counter
st.flow.add(sc.initialStreamSendWindowSize)
- st.inflow.init(sc.srv.initialStreamRecvWindowSize())
+ st.inflow.init(sc.initialStreamRecvWindowSize)
if sc.hs.WriteTimeout > 0 {
st.writeDeadline = sc.srv.afterFunc(sc.hs.WriteTimeout, st.onWriteTimeout)
}
@@ -2187,11 +2238,17 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
scheme: f.PseudoValue("scheme"),
authority: f.PseudoValue("authority"),
path: f.PseudoValue("path"),
+ protocol: f.PseudoValue("protocol"),
+ }
+
+ // extended connect is disabled, so we should not see :protocol
+ if disableExtendedConnectProtocol && rp.protocol != "" {
+ return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
}
isConnect := rp.method == "CONNECT"
if isConnect {
- if rp.path != "" || rp.scheme != "" || rp.authority == "" {
+ if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") {
return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol))
}
} else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") {
@@ -2215,6 +2272,9 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
if rp.authority == "" {
rp.authority = rp.header.Get("Host")
}
+ if rp.protocol != "" {
+ rp.header.Set(":protocol", rp.protocol)
+ }
rw, req, err := sc.newWriterAndRequestNoBody(st, rp)
if err != nil {
@@ -2241,6 +2301,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
type requestParam struct {
method string
scheme, authority, path string
+ protocol string
header http.Header
}
@@ -2282,7 +2343,7 @@ func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*r
var url_ *url.URL
var requestURI string
- if rp.method == "CONNECT" {
+ if rp.method == "CONNECT" && rp.protocol == "" {
url_ = &url.URL{Host: rp.authority}
requestURI = rp.authority // mimic HTTP/1 server behavior
} else {
@@ -2855,6 +2916,11 @@ func (w *responseWriter) SetWriteDeadline(deadline time.Time) error {
return nil
}
+func (w *responseWriter) EnableFullDuplex() error {
+ // We always support full duplex responses, so this is a no-op.
+ return nil
+}
+
func (w *responseWriter) Flush() {
w.FlushError()
}
@@ -3301,7 +3367,7 @@ func (sc *serverConn) countError(name string, err error) error {
if sc == nil || sc.srv == nil {
return err
}
- f := sc.srv.CountError
+ f := sc.countErrorFunc
if f == nil {
return err
}
diff --git a/http2/server_test.go b/http2/server_test.go
index 47c3c619c0..201cf0d00e 100644
--- a/http2/server_test.go
+++ b/http2/server_test.go
@@ -333,7 +333,9 @@ func newServerTesterWithRealConn(t testing.TB, handler http.HandlerFunc, opts ..
// sync waits for all goroutines to idle.
func (st *serverTester) sync() {
- st.group.Wait()
+ if st.group != nil {
+ st.group.Wait()
+ }
}
// advance advances synthetic time by a duration.
@@ -461,7 +463,8 @@ func (st *serverTester) greetAndCheckSettings(checkSetting func(s Setting) error
if f.FrameHeader.StreamID != 0 {
st.t.Fatalf("WindowUpdate StreamID = %d; want 0", f.FrameHeader.StreamID)
}
- incr := uint32(st.sc.srv.initialConnRecvWindowSize() - initialWindowSize)
+ conf := configFromServer(st.sc.hs, st.sc.srv)
+ incr := uint32(conf.MaxUploadBufferPerConnection - initialWindowSize)
if f.Increment != incr {
st.t.Fatalf("WindowUpdate increment = %d; want %d", f.Increment, incr)
}
@@ -589,11 +592,12 @@ func (st *serverTester) bodylessReq1(headers ...string) {
}
func (st *serverTester) wantFlowControlConsumed(streamID, consumed int32) {
+ conf := configFromServer(st.sc.hs, st.sc.srv)
var initial int32
if streamID == 0 {
- initial = st.sc.srv.initialConnRecvWindowSize()
+ initial = conf.MaxUploadBufferPerConnection
} else {
- initial = st.sc.srv.initialStreamRecvWindowSize()
+ initial = conf.MaxUploadBufferPerStream
}
donec := make(chan struct{})
st.sc.sendServeMsg(func(sc *serverConn) {
@@ -2894,15 +2898,10 @@ func BenchmarkServerGets(b *testing.B) {
EndStream: true,
EndHeaders: true,
})
- st.wantHeaders(wantHeader{
- streamID: 1,
- endStream: true,
- })
- st.wantData(wantData{
- streamID: 1,
- endStream: true,
- size: 0,
- })
+ st.wantFrameType(FrameHeaders)
+ if df := readFrame[*DataFrame](b, st); !df.StreamEnded() {
+ b.Fatalf("DATA didn't have END_STREAM; got %v", df)
+ }
}
}
@@ -2937,15 +2936,10 @@ func BenchmarkServerPosts(b *testing.B) {
EndHeaders: true,
})
st.writeData(id, true, nil)
- st.wantHeaders(wantHeader{
- streamID: 1,
- endStream: false,
- })
- st.wantData(wantData{
- streamID: 1,
- endStream: true,
- size: 0,
- })
+ st.wantFrameType(FrameHeaders)
+ if df := readFrame[*DataFrame](b, st); !df.StreamEnded() {
+ b.Fatalf("DATA didn't have END_STREAM; got %v", df)
+ }
}
}
@@ -3287,14 +3281,8 @@ func BenchmarkServer_GetRequest(b *testing.B) {
EndStream: true,
EndHeaders: true,
})
- st.wantHeaders(wantHeader{
- streamID: streamID,
- endStream: false,
- })
- st.wantData(wantData{
- streamID: streamID,
- endStream: true,
- })
+ st.wantFrameType(FrameHeaders)
+ st.wantFrameType(FrameData)
}
}
@@ -3325,14 +3313,8 @@ func BenchmarkServer_PostRequest(b *testing.B) {
EndHeaders: true,
})
st.writeData(streamID, true, nil)
- st.wantHeaders(wantHeader{
- streamID: streamID,
- endStream: false,
- })
- st.wantData(wantData{
- streamID: streamID,
- endStream: true,
- })
+ st.wantFrameType(FrameHeaders)
+ st.wantFrameType(FrameData)
}
}
@@ -4674,3 +4656,78 @@ func TestServerSetReadWriteDeadlineRace(t *testing.T) {
}
resp.Body.Close()
}
+
+func TestServerWriteByteTimeout(t *testing.T) {
+ const timeout = 1 * time.Second
+ st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
+ w.Write(make([]byte, 100))
+ }, func(s *Server) {
+ s.WriteByteTimeout = timeout
+ })
+ st.greet()
+
+ st.cc.(*synctestNetConn).SetReadBufferSize(1) // write one byte at a time
+ st.writeHeaders(HeadersFrameParam{
+ StreamID: 1,
+ BlockFragment: st.encodeHeader(),
+ EndStream: true,
+ EndHeaders: true,
+ })
+
+ // Read a few bytes, staying just under WriteByteTimeout.
+ for i := 0; i < 10; i++ {
+ st.advance(timeout - 1)
+ if n, err := st.cc.Read(make([]byte, 1)); n != 1 || err != nil {
+ t.Fatalf("read %v: %v, %v; want 1, nil", i, n, err)
+ }
+ }
+
+ // Wait for WriteByteTimeout.
+ // The connection should close.
+ st.advance(1 * time.Second) // timeout after writing one byte
+ st.advance(1 * time.Second) // timeout after failing to write any more bytes
+ st.wantClosed()
+}
+
+func TestServerPingSent(t *testing.T) {
+ const readIdleTimeout = 15 * time.Second
+ st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
+ }, func(s *Server) {
+ s.ReadIdleTimeout = readIdleTimeout
+ })
+ st.greet()
+
+ st.wantIdle()
+
+ st.advance(readIdleTimeout)
+ _ = readFrame[*PingFrame](t, st)
+ st.wantIdle()
+
+ st.advance(14 * time.Second)
+ st.wantIdle()
+ st.advance(1 * time.Second)
+ st.wantClosed()
+}
+
+func TestServerPingResponded(t *testing.T) {
+ const readIdleTimeout = 15 * time.Second
+ st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
+ }, func(s *Server) {
+ s.ReadIdleTimeout = readIdleTimeout
+ })
+ st.greet()
+
+ st.wantIdle()
+
+ st.advance(readIdleTimeout)
+ pf := readFrame[*PingFrame](t, st)
+ st.wantIdle()
+
+ st.advance(14 * time.Second)
+ st.wantIdle()
+
+ st.writePing(true, pf.Data)
+
+ st.advance(2 * time.Second)
+ st.wantIdle()
+}
diff --git a/http2/transport.go b/http2/transport.go
index 61f511f97a..090d0e1bdb 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -25,7 +25,6 @@ import (
"net/http"
"net/http/httptrace"
"net/textproto"
- "os"
"sort"
"strconv"
"strings"
@@ -203,6 +202,20 @@ func (t *Transport) markNewGoroutine() {
}
}
+func (t *Transport) now() time.Time {
+ if t != nil && t.transportTestHooks != nil {
+ return t.transportTestHooks.group.Now()
+ }
+ return time.Now()
+}
+
+func (t *Transport) timeSince(when time.Time) time.Duration {
+ if t != nil && t.transportTestHooks != nil {
+ return t.now().Sub(when)
+ }
+ return time.Since(when)
+}
+
// newTimer creates a new time.Timer, or a synthetic timer in tests.
func (t *Transport) newTimer(d time.Duration) timer {
if t.transportTestHooks != nil {
@@ -227,40 +240,26 @@ func (t *Transport) contextWithTimeout(ctx context.Context, d time.Duration) (co
}
func (t *Transport) maxHeaderListSize() uint32 {
- if t.MaxHeaderListSize == 0 {
+ n := int64(t.MaxHeaderListSize)
+ if t.t1 != nil && t.t1.MaxResponseHeaderBytes != 0 {
+ n = t.t1.MaxResponseHeaderBytes
+ if n > 0 {
+ n = adjustHTTP1MaxHeaderSize(n)
+ }
+ }
+ if n <= 0 {
return 10 << 20
}
- if t.MaxHeaderListSize == 0xffffffff {
+ if n >= 0xffffffff {
return 0
}
- return t.MaxHeaderListSize
-}
-
-func (t *Transport) maxFrameReadSize() uint32 {
- if t.MaxReadFrameSize == 0 {
- return 0 // use the default provided by the peer
- }
- if t.MaxReadFrameSize < minMaxFrameSize {
- return minMaxFrameSize
- }
- if t.MaxReadFrameSize > maxFrameSize {
- return maxFrameSize
- }
- return t.MaxReadFrameSize
+ return uint32(n)
}
func (t *Transport) disableCompression() bool {
return t.DisableCompression || (t.t1 != nil && t.t1.DisableCompression)
}
-func (t *Transport) pingTimeout() time.Duration {
- if t.PingTimeout == 0 {
- return 15 * time.Second
- }
- return t.PingTimeout
-
-}
-
// ConfigureTransport configures a net/http HTTP/1 Transport to use HTTP/2.
// It returns an error if t1 has already been HTTP/2-enabled.
//
@@ -296,8 +295,8 @@ func configureTransports(t1 *http.Transport) (*Transport, error) {
if !strSliceContains(t1.TLSClientConfig.NextProtos, "http/1.1") {
t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1")
}
- upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper {
- addr := authorityAddr("https", authority)
+ upgradeFn := func(scheme, authority string, c net.Conn) http.RoundTripper {
+ addr := authorityAddr(scheme, authority)
if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
go c.Close()
return erringRoundTripper{err}
@@ -308,18 +307,37 @@ func configureTransports(t1 *http.Transport) (*Transport, error) {
// was unknown)
go c.Close()
}
+ if scheme == "http" {
+ return (*unencryptedTransport)(t2)
+ }
return t2
}
- if m := t1.TLSNextProto; len(m) == 0 {
- t1.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{
- "h2": upgradeFn,
+ if t1.TLSNextProto == nil {
+ t1.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
+ }
+ t1.TLSNextProto[NextProtoTLS] = func(authority string, c *tls.Conn) http.RoundTripper {
+ return upgradeFn("https", authority, c)
+ }
+ // The "unencrypted_http2" TLSNextProto key is used to pass off non-TLS HTTP/2 conns.
+ t1.TLSNextProto[nextProtoUnencryptedHTTP2] = func(authority string, c *tls.Conn) http.RoundTripper {
+ nc, err := unencryptedNetConnFromTLSConn(c)
+ if err != nil {
+ go c.Close()
+ return erringRoundTripper{err}
}
- } else {
- m["h2"] = upgradeFn
+ return upgradeFn("http", authority, nc)
}
return t2, nil
}
+// unencryptedTransport is a Transport with a RoundTrip method that
+// always permits http:// URLs.
+type unencryptedTransport Transport
+
+func (t *unencryptedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ return (*Transport)(t).RoundTripOpt(req, RoundTripOpt{allowHTTP: true})
+}
+
func (t *Transport) connPool() ClientConnPool {
t.connPoolOnce.Do(t.initConnPool)
return t.connPoolOrDef
@@ -339,7 +357,7 @@ type ClientConn struct {
t *Transport
tconn net.Conn // usually *tls.Conn, except specialized impls
tlsState *tls.ConnectionState // nil only for specialized impls
- reused uint32 // whether conn is being reused; atomic
+ atomicReused uint32 // whether conn is being reused; atomic
singleUse bool // whether being used for a single http.Request
getConnCalled bool // used by clientConnPool
@@ -350,31 +368,54 @@ type ClientConn struct {
idleTimeout time.Duration // or 0 for never
idleTimer timer
- mu sync.Mutex // guards following
- cond *sync.Cond // hold mu; broadcast on flow/closed changes
- flow outflow // our conn-level flow control quota (cs.outflow is per stream)
- inflow inflow // peer's conn-level flow control
- doNotReuse bool // whether conn is marked to not be reused for any future requests
- closing bool
- closed bool
- seenSettings bool // true if we've seen a settings frame, false otherwise
- wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
- goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
- goAwayDebug string // goAway frame's debug data, retained as a string
- streams map[uint32]*clientStream // client-initiated
- streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
- nextStreamID uint32
- pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
- pings map[[8]byte]chan struct{} // in flight ping data to notification channel
- br *bufio.Reader
- lastActive time.Time
- lastIdle time.Time // time last idle
+ mu sync.Mutex // guards following
+ cond *sync.Cond // hold mu; broadcast on flow/closed changes
+ flow outflow // our conn-level flow control quota (cs.outflow is per stream)
+ inflow inflow // peer's conn-level flow control
+ doNotReuse bool // whether conn is marked to not be reused for any future requests
+ closing bool
+ closed bool
+ seenSettings bool // true if we've seen a settings frame, false otherwise
+ seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails
+ wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
+ goAway *GoAwayFrame // if non-nil, the GoAwayFrame we received
+ goAwayDebug string // goAway frame's debug data, retained as a string
+ streams map[uint32]*clientStream // client-initiated
+ streamsReserved int // incr by ReserveNewRequest; decr on RoundTrip
+ nextStreamID uint32
+ pendingRequests int // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
+ pings map[[8]byte]chan struct{} // in flight ping data to notification channel
+ br *bufio.Reader
+ lastActive time.Time
+ lastIdle time.Time // time last idle
// Settings from peer: (also guarded by wmu)
- maxFrameSize uint32
- maxConcurrentStreams uint32
- peerMaxHeaderListSize uint64
- peerMaxHeaderTableSize uint32
- initialWindowSize uint32
+ maxFrameSize uint32
+ maxConcurrentStreams uint32
+ peerMaxHeaderListSize uint64
+ peerMaxHeaderTableSize uint32
+ initialWindowSize uint32
+ initialStreamRecvWindowSize int32
+ readIdleTimeout time.Duration
+ pingTimeout time.Duration
+ extendedConnectAllowed bool
+
+ // rstStreamPingsBlocked works around an unfortunate gRPC behavior.
+ // gRPC strictly limits the number of PING frames that it will receive.
+ // The default is two pings per two hours, but the limit resets every time
+ // the gRPC endpoint sends a HEADERS or DATA frame. See golang/go#70575.
+ //
+ // rstStreamPingsBlocked is set after receiving a response to a PING frame
+ // bundled with an RST_STREAM (see pendingResets below), and cleared after
+ // receiving a HEADERS or DATA frame.
+ rstStreamPingsBlocked bool
+
+ // pendingResets is the number of RST_STREAM frames we have sent to the peer,
+ // without confirming that the peer has received them. When we send a RST_STREAM,
+ // we bundle it with a PING frame, unless a PING is already in flight. We count
+ // the reset stream against the connection's concurrency limit until we get
+ // a PING response. This limits the number of requests we'll try to send to a
+ // completely unresponsive connection.
+ pendingResets int
// reqHeaderMu is a 1-element semaphore channel controlling access to sending new requests.
// Write to reqHeaderMu to lock it, read from it to unlock.
@@ -432,12 +473,12 @@ type clientStream struct {
sentHeaders bool
// owned by clientConnReadLoop:
- firstByte bool // got the first response byte
- pastHeaders bool // got first MetaHeadersFrame (actual headers)
- pastTrailers bool // got optional second MetaHeadersFrame (trailers)
- num1xx uint8 // number of 1xx responses seen
- readClosed bool // peer sent an END_STREAM flag
- readAborted bool // read loop reset the stream
+ firstByte bool // got the first response byte
+ pastHeaders bool // got first MetaHeadersFrame (actual headers)
+ pastTrailers bool // got optional second MetaHeadersFrame (trailers)
+ readClosed bool // peer sent an END_STREAM flag
+ readAborted bool // read loop reset the stream
+ totalHeaderSize int64 // total size of 1xx headers seen
trailer http.Header // accumulated trailers
resTrailer *http.Header // client's Response.Trailer
@@ -499,6 +540,7 @@ func (cs *clientStream) closeReqBodyLocked() {
}
type stickyErrWriter struct {
+ group synctestGroupInterface
conn net.Conn
timeout time.Duration
err *error
@@ -508,22 +550,9 @@ func (sew stickyErrWriter) Write(p []byte) (n int, err error) {
if *sew.err != nil {
return 0, *sew.err
}
- for {
- if sew.timeout != 0 {
- sew.conn.SetWriteDeadline(time.Now().Add(sew.timeout))
- }
- nn, err := sew.conn.Write(p[n:])
- n += nn
- if n < len(p) && nn > 0 && errors.Is(err, os.ErrDeadlineExceeded) {
- // Keep extending the deadline so long as we're making progress.
- continue
- }
- if sew.timeout != 0 {
- sew.conn.SetWriteDeadline(time.Time{})
- }
- *sew.err = err
- return n, err
- }
+ n, err = writeWithByteTimeout(sew.group, sew.conn, sew.timeout, p)
+ *sew.err = err
+ return n, err
}
// noCachedConnError is the concrete type of ErrNoCachedConn, which
@@ -554,6 +583,8 @@ type RoundTripOpt struct {
// no cached connection is available, RoundTripOpt
// will return ErrNoCachedConn.
OnlyCachedConn bool
+
+ allowHTTP bool // allow http:// URLs
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -586,7 +617,14 @@ func authorityAddr(scheme string, authority string) (addr string) {
// RoundTripOpt is like RoundTrip, but takes options.
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
- if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) {
+ switch req.URL.Scheme {
+ case "https":
+ // Always okay.
+ case "http":
+ if !t.AllowHTTP && !opt.allowHTTP {
+ return nil, errors.New("http2: unencrypted HTTP/2 not enabled")
+ }
+ default:
return nil, errors.New("http2: unsupported scheme")
}
@@ -597,7 +635,7 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
return nil, err
}
- reused := !atomic.CompareAndSwapUint32(&cc.reused, 0, 1)
+ reused := !atomic.CompareAndSwapUint32(&cc.atomicReused, 0, 1)
traceGotConn(req, cc, reused)
res, err := cc.RoundTrip(req)
if err != nil && retry <= 6 {
@@ -622,6 +660,22 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
}
}
}
+ if err == errClientConnNotEstablished {
+ // This ClientConn was created recently,
+ // this is the first request to use it,
+ // and the connection is closed and not usable.
+ //
+ // In this state, cc.idleTimer will remove the conn from the pool
+ // when it fires. Stop the timer and remove it here so future requests
+ // won't try to use this connection.
+ //
+ // If the timer has already fired and we're racing it, the redundant
+ // call to MarkDead is harmless.
+ if cc.idleTimer != nil {
+ cc.idleTimer.Stop()
+ }
+ t.connPool().MarkDead(cc)
+ }
if err != nil {
t.vlogf("RoundTrip failure: %v", err)
return nil, err
@@ -640,9 +694,10 @@ func (t *Transport) CloseIdleConnections() {
}
var (
- errClientConnClosed = errors.New("http2: client conn is closed")
- errClientConnUnusable = errors.New("http2: client conn not usable")
- errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
+ errClientConnClosed = errors.New("http2: client conn is closed")
+ errClientConnUnusable = errors.New("http2: client conn not usable")
+ errClientConnNotEstablished = errors.New("http2: client conn could not be established")
+ errClientConnGotGoAway = errors.New("http2: Transport received Server's graceful shutdown GOAWAY")
)
// shouldRetryRequest is called by RoundTrip when a request fails to get
@@ -758,44 +813,38 @@ func (t *Transport) expectContinueTimeout() time.Duration {
return t.t1.ExpectContinueTimeout
}
-func (t *Transport) maxDecoderHeaderTableSize() uint32 {
- if v := t.MaxDecoderHeaderTableSize; v > 0 {
- return v
- }
- return initialHeaderTableSize
-}
-
-func (t *Transport) maxEncoderHeaderTableSize() uint32 {
- if v := t.MaxEncoderHeaderTableSize; v > 0 {
- return v
- }
- return initialHeaderTableSize
-}
-
func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) {
return t.newClientConn(c, t.disableKeepAlives())
}
func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) {
+ conf := configFromTransport(t)
cc := &ClientConn{
- t: t,
- tconn: c,
- readerDone: make(chan struct{}),
- nextStreamID: 1,
- maxFrameSize: 16 << 10, // spec default
- initialWindowSize: 65535, // spec default
- maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings.
- peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
- streams: make(map[uint32]*clientStream),
- singleUse: singleUse,
- wantSettingsAck: true,
- pings: make(map[[8]byte]chan struct{}),
- reqHeaderMu: make(chan struct{}, 1),
- }
+ t: t,
+ tconn: c,
+ readerDone: make(chan struct{}),
+ nextStreamID: 1,
+ maxFrameSize: 16 << 10, // spec default
+ initialWindowSize: 65535, // spec default
+ initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream,
+ maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings.
+ peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
+ streams: make(map[uint32]*clientStream),
+ singleUse: singleUse,
+ seenSettingsChan: make(chan struct{}),
+ wantSettingsAck: true,
+ readIdleTimeout: conf.SendPingTimeout,
+ pingTimeout: conf.PingTimeout,
+ pings: make(map[[8]byte]chan struct{}),
+ reqHeaderMu: make(chan struct{}, 1),
+ lastActive: t.now(),
+ }
+ var group synctestGroupInterface
if t.transportTestHooks != nil {
t.markNewGoroutine()
t.transportTestHooks.newclientconn(cc)
c = cc.tconn
+ group = t.group
}
if VerboseLogs {
t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr())
@@ -807,24 +856,23 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
// TODO: adjust this writer size to account for frame size +
// MTU + crypto/tls record padding.
cc.bw = bufio.NewWriter(stickyErrWriter{
+ group: group,
conn: c,
- timeout: t.WriteByteTimeout,
+ timeout: conf.WriteByteTimeout,
err: &cc.werr,
})
cc.br = bufio.NewReader(c)
cc.fr = NewFramer(cc.bw, cc.br)
- if t.maxFrameReadSize() != 0 {
- cc.fr.SetMaxReadFrameSize(t.maxFrameReadSize())
- }
+ cc.fr.SetMaxReadFrameSize(conf.MaxReadFrameSize)
if t.CountError != nil {
cc.fr.countError = t.CountError
}
- maxHeaderTableSize := t.maxDecoderHeaderTableSize()
+ maxHeaderTableSize := conf.MaxDecoderHeaderTableSize
cc.fr.ReadMetaHeaders = hpack.NewDecoder(maxHeaderTableSize, nil)
cc.fr.MaxHeaderListSize = t.maxHeaderListSize()
cc.henc = hpack.NewEncoder(&cc.hbuf)
- cc.henc.SetMaxDynamicTableSizeLimit(t.maxEncoderHeaderTableSize())
+ cc.henc.SetMaxDynamicTableSizeLimit(conf.MaxEncoderHeaderTableSize)
cc.peerMaxHeaderTableSize = initialHeaderTableSize
if cs, ok := c.(connectionStater); ok {
@@ -834,11 +882,9 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
initialSettings := []Setting{
{ID: SettingEnablePush, Val: 0},
- {ID: SettingInitialWindowSize, Val: transportDefaultStreamFlow},
- }
- if max := t.maxFrameReadSize(); max != 0 {
- initialSettings = append(initialSettings, Setting{ID: SettingMaxFrameSize, Val: max})
+ {ID: SettingInitialWindowSize, Val: uint32(cc.initialStreamRecvWindowSize)},
}
+ initialSettings = append(initialSettings, Setting{ID: SettingMaxFrameSize, Val: conf.MaxReadFrameSize})
if max := t.maxHeaderListSize(); max != 0 {
initialSettings = append(initialSettings, Setting{ID: SettingMaxHeaderListSize, Val: max})
}
@@ -848,8 +894,8 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
cc.bw.Write(clientPreface)
cc.fr.WriteSettings(initialSettings...)
- cc.fr.WriteWindowUpdate(0, transportDefaultConnFlow)
- cc.inflow.init(transportDefaultConnFlow + initialWindowSize)
+ cc.fr.WriteWindowUpdate(0, uint32(conf.MaxUploadBufferPerConnection))
+ cc.inflow.init(conf.MaxUploadBufferPerConnection + initialWindowSize)
cc.bw.Flush()
if cc.werr != nil {
cc.Close()
@@ -867,7 +913,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
}
func (cc *ClientConn) healthCheck() {
- pingTimeout := cc.t.pingTimeout()
+ pingTimeout := cc.pingTimeout
// We don't need to periodically ping in the health check, because the readLoop of ClientConn will
// trigger the healthCheck again if there is no frame received.
ctx, cancel := cc.t.contextWithTimeout(context.Background(), pingTimeout)
@@ -995,7 +1041,7 @@ func (cc *ClientConn) State() ClientConnState {
return ClientConnState{
Closed: cc.closed,
Closing: cc.closing || cc.singleUse || cc.doNotReuse || cc.goAway != nil,
- StreamsActive: len(cc.streams),
+ StreamsActive: len(cc.streams) + cc.pendingResets,
StreamsReserved: cc.streamsReserved,
StreamsPending: cc.pendingRequests,
LastIdle: cc.lastIdle,
@@ -1027,16 +1073,38 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) {
// writing it.
maxConcurrentOkay = true
} else {
- maxConcurrentOkay = int64(len(cc.streams)+cc.streamsReserved+1) <= int64(cc.maxConcurrentStreams)
+ // We can take a new request if the total of
+ // - active streams;
+ // - reservation slots for new streams; and
+ // - streams for which we have sent a RST_STREAM and a PING,
+ // but received no subsequent frame
+ // is less than the concurrency limit.
+ maxConcurrentOkay = cc.currentRequestCountLocked() < int(cc.maxConcurrentStreams)
}
st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay &&
!cc.doNotReuse &&
int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 &&
!cc.tooIdleLocked()
+
+ // If this connection has never been used for a request and is closed,
+ // then let it take a request (which will fail).
+ //
+ // This avoids a situation where an error early in a connection's lifetime
+ // goes unreported.
+ if cc.nextStreamID == 1 && cc.streamsReserved == 0 && cc.closed {
+ st.canTakeNewRequest = true
+ }
+
return
}
+// currentRequestCountLocked reports the number of concurrency slots currently in use,
+// including active streams, reserved slots, and reset streams waiting for acknowledgement.
+func (cc *ClientConn) currentRequestCountLocked() int {
+ return len(cc.streams) + cc.streamsReserved + cc.pendingResets
+}
+
func (cc *ClientConn) canTakeNewRequestLocked() bool {
st := cc.idleStateLocked()
return st.canTakeNewRequest
@@ -1049,7 +1117,7 @@ func (cc *ClientConn) tooIdleLocked() bool {
// times are compared based on their wall time. We don't want
// to reuse a connection that's been sitting idle during
// VM/laptop suspend if monotonic time was also frozen.
- return cc.idleTimeout != 0 && !cc.lastIdle.IsZero() && time.Since(cc.lastIdle.Round(0)) > cc.idleTimeout
+ return cc.idleTimeout != 0 && !cc.lastIdle.IsZero() && cc.t.timeSince(cc.lastIdle.Round(0)) > cc.idleTimeout
}
// onIdleTimeout is called from a time.AfterFunc goroutine. It will
@@ -1411,6 +1479,8 @@ func (cs *clientStream) doRequest(req *http.Request, streamf func(*clientStream)
cs.cleanupWriteRequest(err)
}
+var errExtendedConnectNotSupported = errors.New("net/http: extended connect not supported by peer")
+
// writeRequest sends a request.
//
// It returns nil after the request is written, the response read,
@@ -1426,12 +1496,31 @@ func (cs *clientStream) writeRequest(req *http.Request, streamf func(*clientStre
return err
}
+ // wait for setting frames to be received, a server can change this value later,
+ // but we just wait for the first settings frame
+ var isExtendedConnect bool
+ if req.Method == "CONNECT" && req.Header.Get(":protocol") != "" {
+ isExtendedConnect = true
+ }
+
// Acquire the new-request lock by writing to reqHeaderMu.
// This lock guards the critical section covering allocating a new stream ID
// (requires mu) and creating the stream (requires wmu).
if cc.reqHeaderMu == nil {
panic("RoundTrip on uninitialized ClientConn") // for tests
}
+ if isExtendedConnect {
+ select {
+ case <-cs.reqCancel:
+ return errRequestCanceled
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-cc.seenSettingsChan:
+ if !cc.extendedConnectAllowed {
+ return errExtendedConnectNotSupported
+ }
+ }
+ }
select {
case cc.reqHeaderMu <- struct{}{}:
case <-cs.reqCancel:
@@ -1613,6 +1702,7 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
cs.reqBodyClosed = make(chan struct{})
}
bodyClosed := cs.reqBodyClosed
+ closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil
cc.mu.Unlock()
if mustCloseBody {
cs.reqBody.Close()
@@ -1637,16 +1727,44 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
if cs.sentHeaders {
if se, ok := err.(StreamError); ok {
if se.Cause != errFromPeer {
- cc.writeStreamReset(cs.ID, se.Code, err)
+ cc.writeStreamReset(cs.ID, se.Code, false, err)
}
} else {
- cc.writeStreamReset(cs.ID, ErrCodeCancel, err)
+ // We're cancelling an in-flight request.
+ //
+ // This could be due to the server becoming unresponsive.
+ // To avoid sending too many requests on a dead connection,
+ // we let the request continue to consume a concurrency slot
+ // until we can confirm the server is still responding.
+ // We do this by sending a PING frame along with the RST_STREAM
+ // (unless a ping is already in flight).
+ //
+ // For simplicity, we don't bother tracking the PING payload:
+ // We reset cc.pendingResets any time we receive a PING ACK.
+ //
+ // We skip this if the conn is going to be closed on idle,
+ // because it's short lived and will probably be closed before
+ // we get the ping response.
+ ping := false
+ if !closeOnIdle {
+ cc.mu.Lock()
+ // rstStreamPingsBlocked works around a gRPC behavior:
+ // see comment on the field for details.
+ if !cc.rstStreamPingsBlocked {
+ if cc.pendingResets == 0 {
+ ping = true
+ }
+ cc.pendingResets++
+ }
+ cc.mu.Unlock()
+ }
+ cc.writeStreamReset(cs.ID, ErrCodeCancel, ping, err)
}
}
cs.bufPipe.CloseWithError(err) // no-op if already closed
} else {
if cs.sentHeaders && !cs.sentEndStream {
- cc.writeStreamReset(cs.ID, ErrCodeNo, nil)
+ cc.writeStreamReset(cs.ID, ErrCodeNo, false, nil)
}
cs.bufPipe.CloseWithError(errRequestCanceled)
}
@@ -1668,12 +1786,17 @@ func (cs *clientStream) cleanupWriteRequest(err error) {
// Must hold cc.mu.
func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error {
for {
- cc.lastActive = time.Now()
+ if cc.closed && cc.nextStreamID == 1 && cc.streamsReserved == 0 {
+ // This is the very first request sent to this connection.
+ // Return a fatal error which aborts the retry loop.
+ return errClientConnNotEstablished
+ }
+ cc.lastActive = cc.t.now()
if cc.closed || !cc.canTakeNewRequestLocked() {
return errClientConnUnusable
}
cc.lastIdle = time.Time{}
- if int64(len(cc.streams)) < int64(cc.maxConcurrentStreams) {
+ if cc.currentRequestCountLocked() < int(cc.maxConcurrentStreams) {
return nil
}
cc.pendingRequests++
@@ -1945,7 +2068,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
func validateHeaders(hdrs http.Header) string {
for k, vv := range hdrs {
- if !httpguts.ValidHeaderFieldName(k) {
+ if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
return fmt.Sprintf("name %q", k)
}
for _, v := range vv {
@@ -1961,6 +2084,10 @@ func validateHeaders(hdrs http.Header) string {
var errNilRequestURL = errors.New("http2: Request.URI is nil")
+func isNormalConnect(req *http.Request) bool {
+ return req.Method == "CONNECT" && req.Header.Get(":protocol") == ""
+}
+
// requires cc.wmu be held.
func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
cc.hbuf.Reset()
@@ -1981,7 +2108,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
}
var path string
- if req.Method != "CONNECT" {
+ if !isNormalConnect(req) {
path = req.URL.RequestURI()
if !validPseudoPath(path) {
orig := path
@@ -2018,7 +2145,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
m = http.MethodGet
}
f(":method", m)
- if req.Method != "CONNECT" {
+ if !isNormalConnect(req) {
f(":path", path)
f(":scheme", req.URL.Scheme)
}
@@ -2199,7 +2326,7 @@ type resAndError struct {
func (cc *ClientConn) addStreamLocked(cs *clientStream) {
cs.flow.add(int32(cc.initialWindowSize))
cs.flow.setConnFlow(&cc.flow)
- cs.inflow.init(transportDefaultStreamFlow)
+ cs.inflow.init(cc.initialStreamRecvWindowSize)
cs.ID = cc.nextStreamID
cc.nextStreamID += 2
cc.streams[cs.ID] = cs
@@ -2215,10 +2342,10 @@ func (cc *ClientConn) forgetStreamID(id uint32) {
if len(cc.streams) != slen-1 {
panic("forgetting unknown stream id")
}
- cc.lastActive = time.Now()
+ cc.lastActive = cc.t.now()
if len(cc.streams) == 0 && cc.idleTimer != nil {
cc.idleTimer.Reset(cc.idleTimeout)
- cc.lastIdle = time.Now()
+ cc.lastIdle = cc.t.now()
}
// Wake up writeRequestBody via clientStream.awaitFlowControl and
// wake up RoundTrip if there is a pending request.
@@ -2278,7 +2405,6 @@ func isEOFOrNetReadError(err error) bool {
func (rl *clientConnReadLoop) cleanup() {
cc := rl.cc
- cc.t.connPool().MarkDead(cc)
defer cc.closeConn()
defer close(cc.readerDone)
@@ -2302,6 +2428,24 @@ func (rl *clientConnReadLoop) cleanup() {
}
cc.closed = true
+ // If the connection has never been used, and has been open for only a short time,
+ // leave it in the connection pool for a little while.
+ //
+ // This avoids a situation where new connections are constantly created,
+ // added to the pool, fail, and are removed from the pool, without any error
+ // being surfaced to the user.
+ const unusedWaitTime = 5 * time.Second
+ idleTime := cc.t.now().Sub(cc.lastActive)
+ if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime {
+ cc.idleTimer = cc.t.afterFunc(unusedWaitTime-idleTime, func() {
+ cc.t.connPool().MarkDead(cc)
+ })
+ } else {
+ cc.mu.Unlock() // avoid any deadlocks in MarkDead
+ cc.t.connPool().MarkDead(cc)
+ cc.mu.Lock()
+ }
+
for _, cs := range cc.streams {
select {
case <-cs.peerClosed:
@@ -2345,7 +2489,7 @@ func (cc *ClientConn) countReadFrameError(err error) {
func (rl *clientConnReadLoop) run() error {
cc := rl.cc
gotSettings := false
- readIdleTimeout := cc.t.ReadIdleTimeout
+ readIdleTimeout := cc.readIdleTimeout
var t timer
if readIdleTimeout != 0 {
t = cc.t.afterFunc(readIdleTimeout, cc.healthCheck)
@@ -2359,7 +2503,7 @@ func (rl *clientConnReadLoop) run() error {
cc.vlogf("http2: Transport readFrame error on conn %p: (%T) %v", cc, err, err)
}
if se, ok := err.(StreamError); ok {
- if cs := rl.streamByID(se.StreamID); cs != nil {
+ if cs := rl.streamByID(se.StreamID, notHeaderOrDataFrame); cs != nil {
if se.Cause == nil {
se.Cause = cc.fr.errDetail
}
@@ -2405,13 +2549,16 @@ func (rl *clientConnReadLoop) run() error {
if VerboseLogs {
cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err)
}
+ if !cc.seenSettings {
+ close(cc.seenSettingsChan)
+ }
return err
}
}
}
func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error {
- cs := rl.streamByID(f.StreamID)
+ cs := rl.streamByID(f.StreamID, headerOrDataFrame)
if cs == nil {
// We'd get here if we canceled a request while the
// server had its response still in flight. So if this
@@ -2529,15 +2676,34 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
if f.StreamEnded() {
return nil, errors.New("1xx informational response with END_STREAM flag")
}
- cs.num1xx++
- const max1xxResponses = 5 // arbitrary bound on number of informational responses, same as net/http
- if cs.num1xx > max1xxResponses {
- return nil, errors.New("http2: too many 1xx informational responses")
- }
if fn := cs.get1xxTraceFunc(); fn != nil {
+ // If the 1xx response is being delivered to the user,
+ // then they're responsible for limiting the number
+ // of responses.
if err := fn(statusCode, textproto.MIMEHeader(header)); err != nil {
return nil, err
}
+ } else {
+ // If the user didn't examine the 1xx response, then we
+ // limit the size of all 1xx headers.
+ //
+ // This differs a bit from the HTTP/1 implementation, which
+ // limits the size of all 1xx headers plus the final response.
+ // Use the larger limit of MaxHeaderListSize and
+ // net/http.Transport.MaxResponseHeaderBytes.
+ limit := int64(cs.cc.t.maxHeaderListSize())
+ if t1 := cs.cc.t.t1; t1 != nil && t1.MaxResponseHeaderBytes > limit {
+ limit = t1.MaxResponseHeaderBytes
+ }
+ for _, h := range f.Fields {
+ cs.totalHeaderSize += int64(h.Size())
+ }
+ if cs.totalHeaderSize > limit {
+ if VerboseLogs {
+ log.Printf("http2: 1xx informational responses too large")
+ }
+ return nil, errors.New("header list too large")
+ }
}
if statusCode == 100 {
traceGot100Continue(cs.trace)
@@ -2721,7 +2887,7 @@ func (b transportResponseBody) Close() error {
func (rl *clientConnReadLoop) processData(f *DataFrame) error {
cc := rl.cc
- cs := rl.streamByID(f.StreamID)
+ cs := rl.streamByID(f.StreamID, headerOrDataFrame)
data := f.Data()
if cs == nil {
cc.mu.Lock()
@@ -2856,9 +3022,22 @@ func (rl *clientConnReadLoop) endStreamError(cs *clientStream, err error) {
cs.abortStream(err)
}
-func (rl *clientConnReadLoop) streamByID(id uint32) *clientStream {
+// Constants passed to streamByID for documentation purposes.
+const (
+ headerOrDataFrame = true
+ notHeaderOrDataFrame = false
+)
+
+// streamByID returns the stream with the given id, or nil if no stream has that id.
+// If headerOrData is true, it clears rst.StreamPingsBlocked.
+func (rl *clientConnReadLoop) streamByID(id uint32, headerOrData bool) *clientStream {
rl.cc.mu.Lock()
defer rl.cc.mu.Unlock()
+ if headerOrData {
+ // Work around an unfortunate gRPC behavior.
+ // See comment on ClientConn.rstStreamPingsBlocked for details.
+ rl.cc.rstStreamPingsBlocked = false
+ }
cs := rl.cc.streams[id]
if cs != nil && !cs.readAborted {
return cs
@@ -2952,6 +3131,21 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
case SettingHeaderTableSize:
cc.henc.SetMaxDynamicTableSize(s.Val)
cc.peerMaxHeaderTableSize = s.Val
+ case SettingEnableConnectProtocol:
+ if err := s.Valid(); err != nil {
+ return err
+ }
+ // If the peer wants to send us SETTINGS_ENABLE_CONNECT_PROTOCOL,
+ // we require that it do so in the first SETTINGS frame.
+ //
+ // When we attempt to use extended CONNECT, we wait for the first
+ // SETTINGS frame to see if the server supports it. If we let the
+ // server enable the feature with a later SETTINGS frame, then
+ // users will see inconsistent results depending on whether we've
+ // seen that frame or not.
+ if !cc.seenSettings {
+ cc.extendedConnectAllowed = s.Val == 1
+ }
default:
cc.vlogf("Unhandled Setting: %v", s)
}
@@ -2969,6 +3163,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
// connection can establish to our default.
cc.maxConcurrentStreams = defaultMaxConcurrentStreams
}
+ close(cc.seenSettingsChan)
cc.seenSettings = true
}
@@ -2977,7 +3172,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
cc := rl.cc
- cs := rl.streamByID(f.StreamID)
+ cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame)
if f.StreamID != 0 && cs == nil {
return nil
}
@@ -3006,7 +3201,7 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
}
func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error {
- cs := rl.streamByID(f.StreamID)
+ cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame)
if cs == nil {
// TODO: return error if server tries to RST_STREAM an idle stream
return nil
@@ -3081,6 +3276,12 @@ func (rl *clientConnReadLoop) processPing(f *PingFrame) error {
close(c)
delete(cc.pings, f.Data)
}
+ if cc.pendingResets > 0 {
+ // See clientStream.cleanupWriteRequest.
+ cc.pendingResets = 0
+ cc.rstStreamPingsBlocked = true
+ cc.cond.Broadcast()
+ }
return nil
}
cc := rl.cc
@@ -3103,13 +3304,20 @@ func (rl *clientConnReadLoop) processPushPromise(f *PushPromiseFrame) error {
return ConnectionError(ErrCodeProtocol)
}
-func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, err error) {
+// writeStreamReset sends a RST_STREAM frame.
+// When ping is true, it also sends a PING frame with a random payload.
+func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, ping bool, err error) {
// TODO: map err to more interesting error codes, once the
// HTTP community comes up with some. But currently for
// RST_STREAM there's no equivalent to GOAWAY frame's debug
// data, and the error codes are all pretty vague ("cancel").
cc.wmu.Lock()
cc.fr.WriteRSTStream(streamID, code)
+ if ping {
+ var payload [8]byte
+ rand.Read(payload[:])
+ cc.fr.WritePing(false, payload)
+ }
cc.bw.Flush()
cc.wmu.Unlock()
}
@@ -3263,7 +3471,7 @@ func traceGotConn(req *http.Request, cc *ClientConn, reused bool) {
cc.mu.Lock()
ci.WasIdle = len(cc.streams) == 0 && reused
if ci.WasIdle && !cc.lastActive.IsZero() {
- ci.IdleTime = time.Since(cc.lastActive)
+ ci.IdleTime = cc.t.timeSince(cc.lastActive)
}
cc.mu.Unlock()
diff --git a/http2/transport_test.go b/http2/transport_test.go
index 498e27932c..0e12e0f1c7 100644
--- a/http2/transport_test.go
+++ b/http2/transport_test.go
@@ -2559,6 +2559,9 @@ func testTransportReturnsUnusedFlowControl(t *testing.T, oneDataFrame bool) {
}
return true
},
+ func(f *PingFrame) bool {
+ return true
+ },
func(f *WindowUpdateFrame) bool {
if !oneDataFrame && !sentAdditionalData {
t.Fatalf("Got WindowUpdateFrame, don't expect one yet")
@@ -5421,3 +5424,463 @@ func TestIssue67671(t *testing.T) {
res.Body.Close()
}
}
+
+func TestTransport1xxLimits(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ opt any
+ ctxfn func(context.Context) context.Context
+ hcount int
+ limited bool
+ }{{
+ name: "default",
+ hcount: 10,
+ limited: false,
+ }, {
+ name: "MaxHeaderListSize",
+ opt: func(tr *Transport) {
+ tr.MaxHeaderListSize = 10000
+ },
+ hcount: 10,
+ limited: true,
+ }, {
+ name: "MaxResponseHeaderBytes",
+ opt: func(tr *http.Transport) {
+ tr.MaxResponseHeaderBytes = 10000
+ },
+ hcount: 10,
+ limited: true,
+ }, {
+ name: "limit by client trace",
+ ctxfn: func(ctx context.Context) context.Context {
+ count := 0
+ return httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
+ Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
+ count++
+ if count >= 10 {
+ return errors.New("too many 1xx")
+ }
+ return nil
+ },
+ })
+ },
+ hcount: 10,
+ limited: true,
+ }, {
+ name: "limit disabled by client trace",
+ opt: func(tr *Transport) {
+ tr.MaxHeaderListSize = 10000
+ },
+ ctxfn: func(ctx context.Context) context.Context {
+ return httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
+ Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
+ return nil
+ },
+ })
+ },
+ hcount: 20,
+ limited: false,
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ tc := newTestClientConn(t, test.opt)
+ tc.greet()
+
+ ctx := context.Background()
+ if test.ctxfn != nil {
+ ctx = test.ctxfn(ctx)
+ }
+ req, _ := http.NewRequestWithContext(ctx, "GET", "https://dummy.tld/", nil)
+ rt := tc.roundTrip(req)
+ tc.wantFrameType(FrameHeaders)
+
+ for i := 0; i < test.hcount; i++ {
+ if fr, err := tc.fr.ReadFrame(); err != os.ErrDeadlineExceeded {
+ t.Fatalf("after writing %v 1xx headers: read %v, %v; want idle", i, fr, err)
+ }
+ tc.writeHeaders(HeadersFrameParam{
+ StreamID: rt.streamID(),
+ EndHeaders: true,
+ EndStream: false,
+ BlockFragment: tc.makeHeaderBlockFragment(
+ ":status", "103",
+ "x-field", strings.Repeat("a", 1000),
+ ),
+ })
+ }
+ if test.limited {
+ tc.wantFrameType(FrameRSTStream)
+ } else {
+ tc.wantIdle()
+ }
+ })
+ }
+}
+
+func TestTransportSendPingWithReset(t *testing.T) {
+ tc := newTestClientConn(t, func(tr *Transport) {
+ tr.StrictMaxConcurrentStreams = true
+ })
+
+ const maxConcurrent = 3
+ tc.greet(Setting{SettingMaxConcurrentStreams, maxConcurrent})
+
+ // Start several requests.
+ var rts []*testRoundTrip
+ for i := 0; i < maxConcurrent+1; i++ {
+ req := must(http.NewRequest("GET", "https://dummy.tld/", nil))
+ rt := tc.roundTrip(req)
+ if i >= maxConcurrent {
+ tc.wantIdle()
+ continue
+ }
+ tc.wantFrameType(FrameHeaders)
+ tc.writeHeaders(HeadersFrameParam{
+ StreamID: rt.streamID(),
+ EndHeaders: true,
+ BlockFragment: tc.makeHeaderBlockFragment(
+ ":status", "200",
+ ),
+ })
+ rt.wantStatus(200)
+ rts = append(rts, rt)
+ }
+
+ // Cancel one request. We send a PING frame along with the RST_STREAM.
+ rts[0].response().Body.Close()
+ tc.wantRSTStream(rts[0].streamID(), ErrCodeCancel)
+ pf := readFrame[*PingFrame](t, tc)
+ tc.wantIdle()
+
+ // Cancel another request. No PING frame, since one is in flight.
+ rts[1].response().Body.Close()
+ tc.wantRSTStream(rts[1].streamID(), ErrCodeCancel)
+ tc.wantIdle()
+
+ // Respond to the PING.
+ // This finalizes the previous resets, and allows the pending request to be sent.
+ tc.writePing(true, pf.Data)
+ tc.wantFrameType(FrameHeaders)
+ tc.wantIdle()
+
+ // Receive a byte of data for the remaining stream, which resets our ability
+ // to send pings (see comment on ClientConn.rstStreamPingsBlocked).
+ tc.writeData(rts[2].streamID(), false, []byte{0})
+
+ // Cancel the last request. We send another PING, since none are in flight.
+ rts[2].response().Body.Close()
+ tc.wantRSTStream(rts[2].streamID(), ErrCodeCancel)
+ tc.wantFrameType(FramePing)
+ tc.wantIdle()
+}
+
+// Issue #70505: gRPC gets upset if we send more than 2 pings per HEADERS/DATA frame
+// sent by the server.
+func TestTransportSendNoMoreThanOnePingWithReset(t *testing.T) {
+ tc := newTestClientConn(t)
+ tc.greet()
+
+ makeAndResetRequest := func() {
+ t.Helper()
+ ctx, cancel := context.WithCancel(context.Background())
+ req := must(http.NewRequestWithContext(ctx, "GET", "https://dummy.tld/", nil))
+ rt := tc.roundTrip(req)
+ tc.wantFrameType(FrameHeaders)
+ cancel()
+ tc.wantRSTStream(rt.streamID(), ErrCodeCancel) // client sends RST_STREAM
+ }
+
+ // Create a request and cancel it.
+ // The client sends a PING frame along with the reset.
+ makeAndResetRequest()
+ pf1 := readFrame[*PingFrame](t, tc) // client sends PING
+
+ // Create another request and cancel it.
+ // We do not send a PING frame along with the reset,
+ // because we haven't received a HEADERS or DATA frame from the server
+ // since the last PING we sent.
+ makeAndResetRequest()
+
+ // Server belatedly responds to request 1.
+ // The server has not responded to our first PING yet.
+ tc.writeHeaders(HeadersFrameParam{
+ StreamID: 1,
+ EndHeaders: true,
+ EndStream: true,
+ BlockFragment: tc.makeHeaderBlockFragment(
+ ":status", "200",
+ ),
+ })
+
+ // Create yet another request and cancel it.
+ // We still do not send a PING frame along with the reset.
+ // We've received a HEADERS frame, but it came before the response to the PING.
+ makeAndResetRequest()
+
+ // The server responds to our PING.
+ tc.writePing(true, pf1.Data)
+
+ // Create yet another request and cancel it.
+ // Still no PING frame; we got a response to the previous one,
+ // but no HEADERS or DATA.
+ makeAndResetRequest()
+
+ // Server belatedly responds to the second request.
+ tc.writeHeaders(HeadersFrameParam{
+ StreamID: 3,
+ EndHeaders: true,
+ EndStream: true,
+ BlockFragment: tc.makeHeaderBlockFragment(
+ ":status", "200",
+ ),
+ })
+
+ // One more request.
+ // This time we send a PING frame.
+ makeAndResetRequest()
+ tc.wantFrameType(FramePing)
+}
+
+func TestTransportConnBecomesUnresponsive(t *testing.T) {
+ // We send a number of requests in series to an unresponsive connection.
+ // Each request is canceled or times out without a response.
+ // Eventually, we open a new connection rather than trying to use the old one.
+ tt := newTestTransport(t)
+
+ const maxConcurrent = 3
+
+ t.Logf("first request opens a new connection and succeeds")
+ req1 := must(http.NewRequest("GET", "https://dummy.tld/", nil))
+ rt1 := tt.roundTrip(req1)
+ tc1 := tt.getConn()
+ tc1.wantFrameType(FrameSettings)
+ tc1.wantFrameType(FrameWindowUpdate)
+ hf1 := readFrame[*HeadersFrame](t, tc1)
+ tc1.writeSettings(Setting{SettingMaxConcurrentStreams, maxConcurrent})
+ tc1.wantFrameType(FrameSettings) // ack
+ tc1.writeHeaders(HeadersFrameParam{
+ StreamID: hf1.StreamID,
+ EndHeaders: true,
+ EndStream: true,
+ BlockFragment: tc1.makeHeaderBlockFragment(
+ ":status", "200",
+ ),
+ })
+ rt1.wantStatus(200)
+ rt1.response().Body.Close()
+
+ // Send more requests.
+ // None receive a response.
+ // Each is canceled.
+ for i := 0; i < maxConcurrent; i++ {
+ t.Logf("request %v receives no response and is canceled", i)
+ ctx, cancel := context.WithCancel(context.Background())
+ req := must(http.NewRequestWithContext(ctx, "GET", "https://dummy.tld/", nil))
+ tt.roundTrip(req)
+ if tt.hasConn() {
+ t.Fatalf("new connection created; expect existing conn to be reused")
+ }
+ tc1.wantFrameType(FrameHeaders)
+ cancel()
+ tc1.wantFrameType(FrameRSTStream)
+ if i == 0 {
+ tc1.wantFrameType(FramePing)
+ }
+ tc1.wantIdle()
+ }
+
+ // The conn has hit its concurrency limit.
+ // The next request is sent on a new conn.
+ req2 := must(http.NewRequest("GET", "https://dummy.tld/", nil))
+ rt2 := tt.roundTrip(req2)
+ tc2 := tt.getConn()
+ tc2.wantFrameType(FrameSettings)
+ tc2.wantFrameType(FrameWindowUpdate)
+ hf := readFrame[*HeadersFrame](t, tc2)
+ tc2.writeSettings(Setting{SettingMaxConcurrentStreams, maxConcurrent})
+ tc2.wantFrameType(FrameSettings) // ack
+ tc2.writeHeaders(HeadersFrameParam{
+ StreamID: hf.StreamID,
+ EndHeaders: true,
+ EndStream: true,
+ BlockFragment: tc2.makeHeaderBlockFragment(
+ ":status", "200",
+ ),
+ })
+ rt2.wantStatus(200)
+ rt2.response().Body.Close()
+}
+
+// Test that the Transport can use a conn provided to it by a TLSNextProto hook.
+func TestTransportTLSNextProtoConnOK(t *testing.T) {
+ t1 := &http.Transport{}
+ t2, _ := ConfigureTransports(t1)
+ tt := newTestTransport(t, t2)
+
+ // Create a new, fake connection and pass it to the Transport via the TLSNextProto hook.
+ cli, _ := synctestNetPipe(tt.group)
+ cliTLS := tls.Client(cli, tlsConfigInsecure)
+ go func() {
+ tt.group.Join()
+ t1.TLSNextProto["h2"]("dummy.tld", cliTLS)
+ }()
+ tt.sync()
+ tc := tt.getConn()
+ tc.greet()
+
+ // Send a request on the Transport.
+ // It uses the conn we provided.
+ req := must(http.NewRequest("GET", "https://dummy.tld/", nil))
+ rt := tt.roundTrip(req)
+ tc.wantHeaders(wantHeader{
+ streamID: 1,
+ endStream: true,
+ header: http.Header{
+ ":authority": []string{"dummy.tld"},
+ ":method": []string{"GET"},
+ ":path": []string{"/"},
+ },
+ })
+ tc.writeHeaders(HeadersFrameParam{
+ StreamID: 1,
+ EndHeaders: true,
+ EndStream: true,
+ BlockFragment: tc.makeHeaderBlockFragment(
+ ":status", "200",
+ ),
+ })
+ rt.wantStatus(200)
+ rt.wantBody(nil)
+}
+
+// Test the case where a conn provided via a TLSNextProto hook immediately encounters an error.
+func TestTransportTLSNextProtoConnImmediateFailureUsed(t *testing.T) {
+ t1 := &http.Transport{}
+ t2, _ := ConfigureTransports(t1)
+ tt := newTestTransport(t, t2)
+
+ // Create a new, fake connection and pass it to the Transport via the TLSNextProto hook.
+ cli, _ := synctestNetPipe(tt.group)
+ cliTLS := tls.Client(cli, tlsConfigInsecure)
+ go func() {
+ tt.group.Join()
+ t1.TLSNextProto["h2"]("dummy.tld", cliTLS)
+ }()
+ tt.sync()
+ tc := tt.getConn()
+
+ // The connection encounters an error before we send a request that uses it.
+ tc.closeWrite()
+
+ // Send a request on the Transport.
+ //
+ // It should fail, because we have no usable connections, but not with ErrNoCachedConn.
+ req := must(http.NewRequest("GET", "https://dummy.tld/", nil))
+ rt := tt.roundTrip(req)
+ if err := rt.err(); err == nil || errors.Is(err, ErrNoCachedConn) {
+ t.Fatalf("RoundTrip with broken conn: got %v, want an error other than ErrNoCachedConn", err)
+ }
+
+ // Send the request again.
+ // This time it should fail with ErrNoCachedConn,
+ // because the dead conn has been removed from the pool.
+ rt = tt.roundTrip(req)
+ if err := rt.err(); !errors.Is(err, ErrNoCachedConn) {
+ t.Fatalf("RoundTrip after broken conn is used: got %v, want ErrNoCachedConn", err)
+ }
+}
+
+// Test the case where a conn provided via a TLSNextProto hook immediately encounters an error,
+// but no requests are sent which would use the bad connection.
+func TestTransportTLSNextProtoConnImmediateFailureUnused(t *testing.T) {
+ t1 := &http.Transport{}
+ t2, _ := ConfigureTransports(t1)
+ tt := newTestTransport(t, t2)
+
+ // Create a new, fake connection and pass it to the Transport via the TLSNextProto hook.
+ cli, _ := synctestNetPipe(tt.group)
+ cliTLS := tls.Client(cli, tlsConfigInsecure)
+ go func() {
+ tt.group.Join()
+ t1.TLSNextProto["h2"]("dummy.tld", cliTLS)
+ }()
+ tt.sync()
+ tc := tt.getConn()
+
+ // The connection encounters an error before we send a request that uses it.
+ tc.closeWrite()
+
+ // Some time passes.
+ // The dead connection is removed from the pool.
+ tc.advance(10 * time.Second)
+
+ // Send a request on the Transport.
+ //
+ // It should fail with ErrNoCachedConn, because the pool contains no conns.
+ req := must(http.NewRequest("GET", "https://dummy.tld/", nil))
+ rt := tt.roundTrip(req)
+ if err := rt.err(); !errors.Is(err, ErrNoCachedConn) {
+ t.Fatalf("RoundTrip after broken conn expires: got %v, want ErrNoCachedConn", err)
+ }
+}
+
+func TestExtendedConnectClientWithServerSupport(t *testing.T) {
+ disableExtendedConnectProtocol = false
+ ts := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get(":protocol") != "extended-connect" {
+ t.Fatalf("unexpected :protocol header received")
+ }
+ t.Log(io.Copy(w, r.Body))
+ })
+ tr := &Transport{
+ TLSClientConfig: tlsConfigInsecure,
+ AllowHTTP: true,
+ }
+ defer tr.CloseIdleConnections()
+ pr, pw := io.Pipe()
+ pwDone := make(chan struct{})
+ req, _ := http.NewRequest("CONNECT", ts.URL, pr)
+ req.Header.Set(":protocol", "extended-connect")
+ go func() {
+ pw.Write([]byte("hello, extended connect"))
+ pw.Close()
+ close(pwDone)
+ }()
+
+ res, err := tr.RoundTrip(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(body, []byte("hello, extended connect")) {
+ t.Fatal("unexpected body received")
+ }
+}
+
+func TestExtendedConnectClientWithoutServerSupport(t *testing.T) {
+ disableExtendedConnectProtocol = true
+ ts := newTestServer(t, func(w http.ResponseWriter, r *http.Request) {
+ io.Copy(w, r.Body)
+ })
+ tr := &Transport{
+ TLSClientConfig: tlsConfigInsecure,
+ AllowHTTP: true,
+ }
+ defer tr.CloseIdleConnections()
+ pr, pw := io.Pipe()
+ pwDone := make(chan struct{})
+ req, _ := http.NewRequest("CONNECT", ts.URL, pr)
+ req.Header.Set(":protocol", "extended-connect")
+ go func() {
+ pw.Write([]byte("hello, extended connect"))
+ pw.Close()
+ close(pwDone)
+ }()
+
+ _, err := tr.RoundTrip(req)
+ if !errors.Is(err, errExtendedConnectNotSupported) {
+ t.Fatalf("expected error errExtendedConnectNotSupported, got: %v", err)
+ }
+}
diff --git a/http2/unencrypted.go b/http2/unencrypted.go
new file mode 100644
index 0000000000..b2de211613
--- /dev/null
+++ b/http2/unencrypted.go
@@ -0,0 +1,32 @@
+// Copyright 2024 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 http2
+
+import (
+ "crypto/tls"
+ "errors"
+ "net"
+)
+
+const nextProtoUnencryptedHTTP2 = "unencrypted_http2"
+
+// unencryptedNetConnFromTLSConn retrieves a net.Conn wrapped in a *tls.Conn.
+//
+// TLSNextProto functions accept a *tls.Conn.
+//
+// When passing an unencrypted HTTP/2 connection to a TLSNextProto function,
+// we pass a *tls.Conn with an underlying net.Conn containing the unencrypted connection.
+// To be extra careful about mistakes (accidentally dropping TLS encryption in a place
+// where we want it), the tls.Conn contains a net.Conn with an UnencryptedNetConn method
+// that returns the actual connection we want to use.
+func unencryptedNetConnFromTLSConn(tc *tls.Conn) (net.Conn, error) {
+ conner, ok := tc.NetConn().(interface {
+ UnencryptedNetConn() net.Conn
+ })
+ if !ok {
+ return nil, errors.New("http2: TLS conn unexpectedly found in unencrypted handoff")
+ }
+ return conner.UnencryptedNetConn(), nil
+}
diff --git a/http2/write.go b/http2/write.go
index 33f61398a1..6ff6bee7e9 100644
--- a/http2/write.go
+++ b/http2/write.go
@@ -131,6 +131,16 @@ func (se StreamError) writeFrame(ctx writeContext) error {
func (se StreamError) staysWithinBuffer(max int) bool { return frameHeaderLen+4 <= max }
+type writePing struct {
+ data [8]byte
+}
+
+func (w writePing) writeFrame(ctx writeContext) error {
+ return ctx.Framer().WritePing(false, w.data)
+}
+
+func (w writePing) staysWithinBuffer(max int) bool { return frameHeaderLen+len(w.data) <= max }
+
type writePingAck struct{ pf *PingFrame }
func (w writePingAck) writeFrame(ctx writeContext) error {
diff --git a/internal/socket/zsys_openbsd_ppc64.go b/internal/socket/zsys_openbsd_ppc64.go
index cebde7634f..3c9576e2d8 100644
--- a/internal/socket/zsys_openbsd_ppc64.go
+++ b/internal/socket/zsys_openbsd_ppc64.go
@@ -4,27 +4,27 @@
package socket
type iovec struct {
- Base *byte
- Len uint64
+ Base *byte
+ Len uint64
}
type msghdr struct {
- Name *byte
- Namelen uint32
- Iov *iovec
- Iovlen uint32
- Control *byte
- Controllen uint32
- Flags int32
+ Name *byte
+ Namelen uint32
+ Iov *iovec
+ Iovlen uint32
+ Control *byte
+ Controllen uint32
+ Flags int32
}
type cmsghdr struct {
- Len uint32
- Level int32
- Type int32
+ Len uint32
+ Level int32
+ Type int32
}
const (
- sizeofIovec = 0x10
- sizeofMsghdr = 0x30
+ sizeofIovec = 0x10
+ sizeofMsghdr = 0x30
)
diff --git a/internal/socket/zsys_openbsd_riscv64.go b/internal/socket/zsys_openbsd_riscv64.go
index cebde7634f..3c9576e2d8 100644
--- a/internal/socket/zsys_openbsd_riscv64.go
+++ b/internal/socket/zsys_openbsd_riscv64.go
@@ -4,27 +4,27 @@
package socket
type iovec struct {
- Base *byte
- Len uint64
+ Base *byte
+ Len uint64
}
type msghdr struct {
- Name *byte
- Namelen uint32
- Iov *iovec
- Iovlen uint32
- Control *byte
- Controllen uint32
- Flags int32
+ Name *byte
+ Namelen uint32
+ Iov *iovec
+ Iovlen uint32
+ Control *byte
+ Controllen uint32
+ Flags int32
}
type cmsghdr struct {
- Len uint32
- Level int32
- Type int32
+ Len uint32
+ Level int32
+ Type int32
}
const (
- sizeofIovec = 0x10
- sizeofMsghdr = 0x30
+ sizeofIovec = 0x10
+ sizeofMsghdr = 0x30
)
diff --git a/quic/conn.go b/quic/conn.go
index 38e8fe8f4e..bf54409bfe 100644
--- a/quic/conn.go
+++ b/quic/conn.go
@@ -176,6 +176,16 @@ func (c *Conn) String() string {
return fmt.Sprintf("quic.Conn(%v,->%v)", c.side, c.peerAddr)
}
+// LocalAddr returns the local network address, if known.
+func (c *Conn) LocalAddr() netip.AddrPort {
+ return c.localAddr
+}
+
+// RemoteAddr returns the remote network address, if known.
+func (c *Conn) RemoteAddr() netip.AddrPort {
+ return c.peerAddr
+}
+
// confirmHandshake is called when the handshake is confirmed.
// https://www.rfc-editor.org/rfc/rfc9001#section-4.1.2
func (c *Conn) confirmHandshake(now time.Time) {
@@ -206,6 +216,9 @@ func (c *Conn) confirmHandshake(now time.Time) {
// discardKeys discards unused packet protection keys.
// https://www.rfc-editor.org/rfc/rfc9001#section-4.9
func (c *Conn) discardKeys(now time.Time, space numberSpace) {
+ if err := c.crypto[space].discardKeys(); err != nil {
+ c.abort(now, err)
+ }
switch space {
case initialSpace:
c.keysInitial.discard()
diff --git a/quic/conn_recv.go b/quic/conn_recv.go
index b1354cd3a1..dbfe34a343 100644
--- a/quic/conn_recv.go
+++ b/quic/conn_recv.go
@@ -285,6 +285,7 @@ func (c *Conn) handleFrames(now time.Time, dgram *datagram, ptype packetType, sp
__01 = packetType0RTT | packetType1RTT
___1 = packetType1RTT
)
+ hasCrypto := false
for len(payload) > 0 {
switch payload[0] {
case frameTypePadding, frameTypeAck, frameTypeAckECN,
@@ -322,6 +323,7 @@ func (c *Conn) handleFrames(now time.Time, dgram *datagram, ptype packetType, sp
if !frameOK(c, ptype, IH_1) {
return
}
+ hasCrypto = true
n = c.handleCryptoFrame(now, space, payload)
case frameTypeNewToken:
if !frameOK(c, ptype, ___1) {
@@ -406,6 +408,15 @@ func (c *Conn) handleFrames(now time.Time, dgram *datagram, ptype packetType, sp
}
payload = payload[n:]
}
+ if hasCrypto {
+ // Process TLS events after handling all frames in a packet.
+ // TLS events can cause us to drop state for a number space,
+ // so do that last, to avoid handling frames differently
+ // depending on whether they come before or after a CRYPTO frame.
+ if err := c.handleTLSEvents(now); err != nil {
+ c.abort(now, err)
+ }
+ }
return ackEliciting
}
diff --git a/quic/conn_recv_test.go b/quic/conn_recv_test.go
new file mode 100644
index 0000000000..0e94731bf7
--- /dev/null
+++ b/quic/conn_recv_test.go
@@ -0,0 +1,60 @@
+// Copyright 2024 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.
+
+//go:build go1.21
+
+package quic
+
+import (
+ "crypto/tls"
+ "testing"
+)
+
+func TestConnReceiveAckForUnsentPacket(t *testing.T) {
+ tc := newTestConn(t, serverSide, permissiveTransportParameters)
+ tc.handshake()
+ tc.writeFrames(packetType1RTT,
+ debugFrameAck{
+ ackDelay: 0,
+ ranges: []i64range[packetNumber]{{0, 10}},
+ })
+ tc.wantFrame("ACK for unsent packet causes CONNECTION_CLOSE",
+ packetType1RTT, debugFrameConnectionCloseTransport{
+ code: errProtocolViolation,
+ })
+}
+
+// Issue #70703: If a packet contains both a CRYPTO frame which causes us to
+// drop state for a number space, and also contains a valid ACK frame for that space,
+// we shouldn't complain about the ACK.
+func TestConnReceiveAckForDroppedSpace(t *testing.T) {
+ tc := newTestConn(t, serverSide, permissiveTransportParameters)
+ tc.ignoreFrame(frameTypeAck)
+ tc.ignoreFrame(frameTypeNewConnectionID)
+
+ tc.writeFrames(packetTypeInitial,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
+ })
+ tc.wantFrame("send Initial crypto",
+ packetTypeInitial, debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
+ })
+ tc.wantFrame("send Handshake crypto",
+ packetTypeHandshake, debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelHandshake],
+ })
+
+ tc.writeFrames(packetTypeHandshake,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
+ },
+ debugFrameAck{
+ ackDelay: 0,
+ ranges: []i64range[packetNumber]{{0, tc.lastPacket.num + 1}},
+ })
+ tc.wantFrame("handshake finishes",
+ packetType1RTT, debugFrameHandshakeDone{})
+ tc.wantIdle("connection is idle")
+}
diff --git a/quic/crypto_stream.go b/quic/crypto_stream.go
index a4dcb32eb7..806c963943 100644
--- a/quic/crypto_stream.go
+++ b/quic/crypto_stream.go
@@ -139,3 +139,21 @@ func (s *cryptoStream) sendData(off int64, b []byte) {
s.out.copy(off, b)
s.outunsent.sub(off, off+int64(len(b)))
}
+
+// discardKeys is called when the packet protection keys for the stream are dropped.
+func (s *cryptoStream) discardKeys() error {
+ if s.in.end-s.in.start != 0 {
+ // The peer sent some unprocessed CRYPTO data that we're about to discard.
+ // Close the connetion with a TLS unexpected_message alert.
+ // https://www.rfc-editor.org/rfc/rfc5246#section-7.2.2
+ const unexpectedMessage = 10
+ return localTransportError{
+ code: errTLSBase + unexpectedMessage,
+ reason: "excess crypto data",
+ }
+ }
+ // Discard any unacked (but presumably received) data in our output buffer.
+ s.out.discardBefore(s.out.end)
+ *s = cryptoStream{}
+ return nil
+}
diff --git a/quic/tls.go b/quic/tls.go
index e2f2e5bde1..89b31842cd 100644
--- a/quic/tls.go
+++ b/quic/tls.go
@@ -119,11 +119,7 @@ func (c *Conn) handleCrypto(now time.Time, space numberSpace, off int64, data []
default:
return errors.New("quic: internal error: received CRYPTO frame in unexpected number space")
}
- err := c.crypto[space].handleCrypto(off, data, func(b []byte) error {
+ return c.crypto[space].handleCrypto(off, data, func(b []byte) error {
return c.tls.HandleData(level, b)
})
- if err != nil {
- return err
- }
- return c.handleTLSEvents(now)
}
diff --git a/quic/tls_test.go b/quic/tls_test.go
index 9c1dd364ec..f4abdda582 100644
--- a/quic/tls_test.go
+++ b/quic/tls_test.go
@@ -615,3 +615,32 @@ func TestConnAEADLimitReached(t *testing.T) {
tc.advance(1 * time.Second)
tc.wantIdle("auth failures at limit: conn does not process additional packets")
}
+
+func TestConnKeysDiscardedWithExcessCryptoData(t *testing.T) {
+ tc := newTestConn(t, serverSide, permissiveTransportParameters)
+ tc.ignoreFrame(frameTypeAck)
+ tc.ignoreFrame(frameTypeNewConnectionID)
+ tc.ignoreFrame(frameTypeCrypto)
+
+ // One byte of excess CRYPTO data, separated from the valid data by a one-byte gap.
+ tc.writeFrames(packetTypeInitial,
+ debugFrameCrypto{
+ off: int64(len(tc.cryptoDataIn[tls.QUICEncryptionLevelInitial]) + 1),
+ data: []byte{0},
+ })
+ tc.writeFrames(packetTypeInitial,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
+ })
+
+ // We don't drop the Initial keys and discover the excess data until the client
+ // sends a Handshake packet.
+ tc.writeFrames(packetTypeHandshake,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
+ })
+ tc.wantFrame("connection closed due to excess Initial CRYPTO data",
+ packetType1RTT, debugFrameConnectionCloseTransport{
+ code: errTLSBase + 10,
+ })
+}
diff --git a/route/address.go b/route/address.go
index 5443d67223..b649f43141 100644
--- a/route/address.go
+++ b/route/address.go
@@ -170,20 +170,37 @@ func (a *Inet6Addr) marshal(b []byte) (int, error) {
// parseInetAddr parses b as an internet address for IPv4 or IPv6.
func parseInetAddr(af int, b []byte) (Addr, error) {
+ const (
+ off4 = 4 // offset of in_addr
+ off6 = 8 // offset of in6_addr
+ )
switch af {
case syscall.AF_INET:
- if len(b) < sizeofSockaddrInet {
+ if len(b) < (off4+1) || len(b) < int(b[0]) || b[0] == 0 {
return nil, errInvalidAddr
}
+ sockAddrLen := int(b[0])
a := &Inet4Addr{}
- copy(a.IP[:], b[4:8])
+ n := off4 + 4
+ if sockAddrLen < n {
+ n = sockAddrLen
+ }
+ copy(a.IP[:], b[off4:n])
return a, nil
case syscall.AF_INET6:
- if len(b) < sizeofSockaddrInet6 {
+ if len(b) < (off6+1) || len(b) < int(b[0]) || b[0] == 0 {
return nil, errInvalidAddr
}
- a := &Inet6Addr{ZoneID: int(nativeEndian.Uint32(b[24:28]))}
- copy(a.IP[:], b[8:24])
+ sockAddrLen := int(b[0])
+ n := off6 + 16
+ if sockAddrLen < n {
+ n = sockAddrLen
+ }
+ a := &Inet6Addr{}
+ if sockAddrLen == sizeofSockaddrInet6 {
+ a.ZoneID = int(nativeEndian.Uint32(b[24:28]))
+ }
+ copy(a.IP[:], b[off6:n])
if a.IP[0] == 0xfe && a.IP[1]&0xc0 == 0x80 || a.IP[0] == 0xff && (a.IP[1]&0x0f == 0x01 || a.IP[1]&0x0f == 0x02) {
// KAME based IPv6 protocol stack usually
// embeds the interface index in the
@@ -387,12 +404,16 @@ func parseAddrs(attrs uint, fn func(int, []byte) (int, Addr, error), b []byte) (
}
b = b[l:]
case syscall.AF_INET, syscall.AF_INET6:
- af = int(b[1])
- a, err := parseInetAddr(af, b)
- if err != nil {
- return nil, err
+ // #70528: if the sockaddrlen is 0, no address to parse inside,
+ // skip over the record.
+ if b[0] > 0 {
+ af = int(b[1])
+ a, err := parseInetAddr(af, b)
+ if err != nil {
+ return nil, err
+ }
+ as[i] = a
}
- as[i] = a
l := roundup(int(b[0]))
if len(b) < l {
return nil, errMessageTooShort
diff --git a/route/address_darwin_test.go b/route/address_darwin_test.go
index b819183a78..add72e37ec 100644
--- a/route/address_darwin_test.go
+++ b/route/address_darwin_test.go
@@ -42,6 +42,112 @@ var parseAddrsOnDarwinLittleEndianTests = []parseAddrsOnDarwinTest{
nil,
},
},
+ {
+ syscall.RTA_DST | syscall.RTA_GATEWAY | syscall.RTA_NETMASK,
+ parseKernelInetAddr,
+ []byte{
+ 0x10, 0x02, 0x00, 0x00, 0x64, 0x71, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+
+ 0x14, 0x12, 0x21, 0x00, 0x01, 0x08, 0x00, 0x00,
+ 0x75, 0x74, 0x75, 0x6e, 0x34, 0x33, 0x31, 0x39,
+ 0x00, 0x00, 0x00, 0x00,
+
+ 0x06, 0x02, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00,
+ },
+ []Addr{
+ &Inet4Addr{IP: [4]byte{100, 113, 0, 0}},
+ &LinkAddr{Index: 33, Name: "utun4319"},
+ &Inet4Addr{IP: [4]byte{255, 255, 0, 0}},
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ },
+ // route -n add -inet6 fd84:1b4e:6281:: -prefixlen 48 fe80::f22f:4bff:fe09:3bff%utun4319
+ // gw fe80:0000:0000:0000:f22f:4bff:fe09:3bff
+ {
+ syscall.RTA_DST | syscall.RTA_GATEWAY | syscall.RTA_NETMASK,
+ parseKernelInetAddr,
+ []byte{
+ 0x1c, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xfd, 0x84, 0x1b, 0x4e, 0x62, 0x81, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+
+ 0x1c, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xfe, 0x80, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00,
+ 0xf2, 0x2f, 0x4b, 0xff, 0xfe, 0x09, 0x3b, 0xff,
+ 0x00, 0x00, 0x00, 0x00,
+
+ 0x0e, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
+ },
+ []Addr{
+ &Inet6Addr{IP: [16]byte{0xfd, 0x84, 0x1b, 0x4e, 0x62, 0x81}},
+ &Inet6Addr{IP: [16]byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2, 0x2f, 0x4b, 0xff, 0xfe, 0x09, 0x3b, 0xff}, ZoneID: 33},
+ &Inet6Addr{IP: [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}},
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ },
+ // golang/go#70528, the kernel can produce addresses of length 0
+ {
+ syscall.RTA_DST | syscall.RTA_GATEWAY | syscall.RTA_NETMASK,
+ parseKernelInetAddr,
+ []byte{
+ 0x00, 0x1e, 0x00, 0x00,
+
+ 0x1c, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xfe, 0x80, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00,
+ 0xf2, 0x2f, 0x4b, 0xff, 0xfe, 0x09, 0x3b, 0xff,
+ 0x00, 0x00, 0x00, 0x00,
+
+ 0x0e, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
+ },
+ []Addr{
+ nil,
+ &Inet6Addr{IP: [16]byte{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2, 0x2f, 0x4b, 0xff, 0xfe, 0x09, 0x3b, 0xff}, ZoneID: 33},
+ &Inet6Addr{IP: [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}},
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ },
+ // Additional case: golang/go/issues/70528#issuecomment-2498692877
+ {
+ syscall.RTA_DST | syscall.RTA_GATEWAY | syscall.RTA_NETMASK,
+ parseKernelInetAddr,
+ []byte{
+ 0x84, 0x00, 0x05, 0x04, 0x01, 0x00, 0x00, 0x00, 0x03, 0x08, 0x00, 0x01, 0x15, 0x00, 0x00, 0x00,
+ 0x1B, 0x01, 0x00, 0x00, 0xF5, 0x5A, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
+ 0x14, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ },
+ []Addr{
+ &Inet4Addr{IP: [4]byte{0x0, 0x0, 0x0, 0x0}},
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ nil,
+ },
+ },
}
func TestParseAddrsOnDarwin(t *testing.T) {
diff --git a/route/defs_darwin.go b/route/defs_darwin.go
index ec56ca02e1..46a4ed6694 100644
--- a/route/defs_darwin.go
+++ b/route/defs_darwin.go
@@ -24,14 +24,10 @@ const (
sizeofIfmaMsghdrDarwin15 = C.sizeof_struct_ifma_msghdr
sizeofIfMsghdr2Darwin15 = C.sizeof_struct_if_msghdr2
sizeofIfmaMsghdr2Darwin15 = C.sizeof_struct_ifma_msghdr2
- sizeofIfDataDarwin15 = C.sizeof_struct_if_data
- sizeofIfData64Darwin15 = C.sizeof_struct_if_data64
sizeofRtMsghdrDarwin15 = C.sizeof_struct_rt_msghdr
sizeofRtMsghdr2Darwin15 = C.sizeof_struct_rt_msghdr2
- sizeofRtMetricsDarwin15 = C.sizeof_struct_rt_metrics
- sizeofSockaddrStorage = C.sizeof_struct_sockaddr_storage
- sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
- sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
+ sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
+ sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
)
diff --git a/route/defs_dragonfly.go b/route/defs_dragonfly.go
index 9bf202dda4..52aa700a6d 100644
--- a/route/defs_dragonfly.go
+++ b/route/defs_dragonfly.go
@@ -47,10 +47,8 @@ const (
sizeofIfaMsghdrDragonFlyBSD58 = C.sizeof_struct_ifa_msghdr_dfly58
- sizeofRtMsghdrDragonFlyBSD4 = C.sizeof_struct_rt_msghdr
- sizeofRtMetricsDragonFlyBSD4 = C.sizeof_struct_rt_metrics
+ sizeofRtMsghdrDragonFlyBSD4 = C.sizeof_struct_rt_msghdr
- sizeofSockaddrStorage = C.sizeof_struct_sockaddr_storage
- sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
- sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
+ sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
+ sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
)
diff --git a/route/defs_freebsd.go b/route/defs_freebsd.go
index abb2dc0957..68778f2d16 100644
--- a/route/defs_freebsd.go
+++ b/route/defs_freebsd.go
@@ -220,7 +220,6 @@ import "C"
const (
sizeofIfMsghdrlFreeBSD10 = C.sizeof_struct_if_msghdrl
sizeofIfaMsghdrFreeBSD10 = C.sizeof_struct_ifa_msghdr
- sizeofIfaMsghdrlFreeBSD10 = C.sizeof_struct_ifa_msghdrl
sizeofIfmaMsghdrFreeBSD10 = C.sizeof_struct_ifma_msghdr
sizeofIfAnnouncemsghdrFreeBSD10 = C.sizeof_struct_if_announcemsghdr
@@ -233,15 +232,7 @@ const (
sizeofIfMsghdrFreeBSD10 = C.sizeof_struct_if_msghdr_freebsd10
sizeofIfMsghdrFreeBSD11 = C.sizeof_struct_if_msghdr_freebsd11
- sizeofIfDataFreeBSD7 = C.sizeof_struct_if_data_freebsd7
- sizeofIfDataFreeBSD8 = C.sizeof_struct_if_data_freebsd8
- sizeofIfDataFreeBSD9 = C.sizeof_struct_if_data_freebsd9
- sizeofIfDataFreeBSD10 = C.sizeof_struct_if_data_freebsd10
- sizeofIfDataFreeBSD11 = C.sizeof_struct_if_data_freebsd11
-
- sizeofIfMsghdrlFreeBSD10Emu = C.sizeof_struct_if_msghdrl
sizeofIfaMsghdrFreeBSD10Emu = C.sizeof_struct_ifa_msghdr
- sizeofIfaMsghdrlFreeBSD10Emu = C.sizeof_struct_ifa_msghdrl
sizeofIfmaMsghdrFreeBSD10Emu = C.sizeof_struct_ifma_msghdr
sizeofIfAnnouncemsghdrFreeBSD10Emu = C.sizeof_struct_if_announcemsghdr
@@ -254,13 +245,6 @@ const (
sizeofIfMsghdrFreeBSD10Emu = C.sizeof_struct_if_msghdr_freebsd10
sizeofIfMsghdrFreeBSD11Emu = C.sizeof_struct_if_msghdr_freebsd11
- sizeofIfDataFreeBSD7Emu = C.sizeof_struct_if_data_freebsd7
- sizeofIfDataFreeBSD8Emu = C.sizeof_struct_if_data_freebsd8
- sizeofIfDataFreeBSD9Emu = C.sizeof_struct_if_data_freebsd9
- sizeofIfDataFreeBSD10Emu = C.sizeof_struct_if_data_freebsd10
- sizeofIfDataFreeBSD11Emu = C.sizeof_struct_if_data_freebsd11
-
- sizeofSockaddrStorage = C.sizeof_struct_sockaddr_storage
- sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
- sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
+ sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
+ sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
)
diff --git a/route/defs_netbsd.go b/route/defs_netbsd.go
index 8e89934c5a..fb60f43c83 100644
--- a/route/defs_netbsd.go
+++ b/route/defs_netbsd.go
@@ -23,10 +23,8 @@ const (
sizeofIfaMsghdrNetBSD7 = C.sizeof_struct_ifa_msghdr
sizeofIfAnnouncemsghdrNetBSD7 = C.sizeof_struct_if_announcemsghdr
- sizeofRtMsghdrNetBSD7 = C.sizeof_struct_rt_msghdr
- sizeofRtMetricsNetBSD7 = C.sizeof_struct_rt_metrics
+ sizeofRtMsghdrNetBSD7 = C.sizeof_struct_rt_msghdr
- sizeofSockaddrStorage = C.sizeof_struct_sockaddr_storage
- sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
- sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
+ sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
+ sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
)
diff --git a/route/defs_openbsd.go b/route/defs_openbsd.go
index 8f3218bc63..471558d9ef 100644
--- a/route/defs_openbsd.go
+++ b/route/defs_openbsd.go
@@ -21,7 +21,6 @@ import "C"
const (
sizeofRtMsghdr = C.sizeof_struct_rt_msghdr
- sizeofSockaddrStorage = C.sizeof_struct_sockaddr_storage
- sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
- sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
+ sizeofSockaddrInet = C.sizeof_struct_sockaddr_in
+ sizeofSockaddrInet6 = C.sizeof_struct_sockaddr_in6
)
diff --git a/route/zsys_darwin.go b/route/zsys_darwin.go
index 56a0c66f44..adaa460026 100644
--- a/route/zsys_darwin.go
+++ b/route/zsys_darwin.go
@@ -9,14 +9,10 @@ const (
sizeofIfmaMsghdrDarwin15 = 0x10
sizeofIfMsghdr2Darwin15 = 0xa0
sizeofIfmaMsghdr2Darwin15 = 0x14
- sizeofIfDataDarwin15 = 0x60
- sizeofIfData64Darwin15 = 0x80
sizeofRtMsghdrDarwin15 = 0x5c
sizeofRtMsghdr2Darwin15 = 0x5c
- sizeofRtMetricsDarwin15 = 0x38
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_dragonfly.go b/route/zsys_dragonfly.go
index f7c7a60cd6..209cb20af8 100644
--- a/route/zsys_dragonfly.go
+++ b/route/zsys_dragonfly.go
@@ -11,10 +11,8 @@ const (
sizeofIfaMsghdrDragonFlyBSD58 = 0x18
- sizeofRtMsghdrDragonFlyBSD4 = 0x98
- sizeofRtMetricsDragonFlyBSD4 = 0x70
+ sizeofRtMsghdrDragonFlyBSD4 = 0x98
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_freebsd_386.go b/route/zsys_freebsd_386.go
index 3f985c7ee9..ec617772b2 100644
--- a/route/zsys_freebsd_386.go
+++ b/route/zsys_freebsd_386.go
@@ -6,7 +6,6 @@ package route
const (
sizeofIfMsghdrlFreeBSD10 = 0x68
sizeofIfaMsghdrFreeBSD10 = 0x14
- sizeofIfaMsghdrlFreeBSD10 = 0x6c
sizeofIfmaMsghdrFreeBSD10 = 0x10
sizeofIfAnnouncemsghdrFreeBSD10 = 0x18
@@ -19,18 +18,10 @@ const (
sizeofIfMsghdrFreeBSD10 = 0x64
sizeofIfMsghdrFreeBSD11 = 0xa8
- sizeofIfDataFreeBSD7 = 0x50
- sizeofIfDataFreeBSD8 = 0x50
- sizeofIfDataFreeBSD9 = 0x50
- sizeofIfDataFreeBSD10 = 0x54
- sizeofIfDataFreeBSD11 = 0x98
-
// MODIFIED BY HAND FOR 386 EMULATION ON AMD64
// 386 EMULATION USES THE UNDERLYING RAW DATA LAYOUT
- sizeofIfMsghdrlFreeBSD10Emu = 0xb0
sizeofIfaMsghdrFreeBSD10Emu = 0x14
- sizeofIfaMsghdrlFreeBSD10Emu = 0xb0
sizeofIfmaMsghdrFreeBSD10Emu = 0x10
sizeofIfAnnouncemsghdrFreeBSD10Emu = 0x18
@@ -43,13 +34,6 @@ const (
sizeofIfMsghdrFreeBSD10Emu = 0xa8
sizeofIfMsghdrFreeBSD11Emu = 0xa8
- sizeofIfDataFreeBSD7Emu = 0x98
- sizeofIfDataFreeBSD8Emu = 0x98
- sizeofIfDataFreeBSD9Emu = 0x98
- sizeofIfDataFreeBSD10Emu = 0x98
- sizeofIfDataFreeBSD11Emu = 0x98
-
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_freebsd_amd64.go b/route/zsys_freebsd_amd64.go
index 9293393698..3d7f31d13e 100644
--- a/route/zsys_freebsd_amd64.go
+++ b/route/zsys_freebsd_amd64.go
@@ -6,7 +6,6 @@ package route
const (
sizeofIfMsghdrlFreeBSD10 = 0xb0
sizeofIfaMsghdrFreeBSD10 = 0x14
- sizeofIfaMsghdrlFreeBSD10 = 0xb0
sizeofIfmaMsghdrFreeBSD10 = 0x10
sizeofIfAnnouncemsghdrFreeBSD10 = 0x18
@@ -19,15 +18,7 @@ const (
sizeofIfMsghdrFreeBSD10 = 0xa8
sizeofIfMsghdrFreeBSD11 = 0xa8
- sizeofIfDataFreeBSD7 = 0x98
- sizeofIfDataFreeBSD8 = 0x98
- sizeofIfDataFreeBSD9 = 0x98
- sizeofIfDataFreeBSD10 = 0x98
- sizeofIfDataFreeBSD11 = 0x98
-
- sizeofIfMsghdrlFreeBSD10Emu = 0xb0
sizeofIfaMsghdrFreeBSD10Emu = 0x14
- sizeofIfaMsghdrlFreeBSD10Emu = 0xb0
sizeofIfmaMsghdrFreeBSD10Emu = 0x10
sizeofIfAnnouncemsghdrFreeBSD10Emu = 0x18
@@ -40,13 +31,6 @@ const (
sizeofIfMsghdrFreeBSD10Emu = 0xa8
sizeofIfMsghdrFreeBSD11Emu = 0xa8
- sizeofIfDataFreeBSD7Emu = 0x98
- sizeofIfDataFreeBSD8Emu = 0x98
- sizeofIfDataFreeBSD9Emu = 0x98
- sizeofIfDataFreeBSD10Emu = 0x98
- sizeofIfDataFreeBSD11Emu = 0x98
-
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_freebsd_arm.go b/route/zsys_freebsd_arm.go
index a2bdb4ad3b..931afa3931 100644
--- a/route/zsys_freebsd_arm.go
+++ b/route/zsys_freebsd_arm.go
@@ -6,7 +6,6 @@ package route
const (
sizeofIfMsghdrlFreeBSD10 = 0x68
sizeofIfaMsghdrFreeBSD10 = 0x14
- sizeofIfaMsghdrlFreeBSD10 = 0x6c
sizeofIfmaMsghdrFreeBSD10 = 0x10
sizeofIfAnnouncemsghdrFreeBSD10 = 0x18
@@ -19,15 +18,7 @@ const (
sizeofIfMsghdrFreeBSD10 = 0x70
sizeofIfMsghdrFreeBSD11 = 0xa8
- sizeofIfDataFreeBSD7 = 0x60
- sizeofIfDataFreeBSD8 = 0x60
- sizeofIfDataFreeBSD9 = 0x60
- sizeofIfDataFreeBSD10 = 0x60
- sizeofIfDataFreeBSD11 = 0x98
-
- sizeofIfMsghdrlFreeBSD10Emu = 0x68
sizeofIfaMsghdrFreeBSD10Emu = 0x14
- sizeofIfaMsghdrlFreeBSD10Emu = 0x6c
sizeofIfmaMsghdrFreeBSD10Emu = 0x10
sizeofIfAnnouncemsghdrFreeBSD10Emu = 0x18
@@ -40,13 +31,6 @@ const (
sizeofIfMsghdrFreeBSD10Emu = 0x70
sizeofIfMsghdrFreeBSD11Emu = 0xa8
- sizeofIfDataFreeBSD7Emu = 0x60
- sizeofIfDataFreeBSD8Emu = 0x60
- sizeofIfDataFreeBSD9Emu = 0x60
- sizeofIfDataFreeBSD10Emu = 0x60
- sizeofIfDataFreeBSD11Emu = 0x98
-
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_freebsd_arm64.go b/route/zsys_freebsd_arm64.go
index 9293393698..3d7f31d13e 100644
--- a/route/zsys_freebsd_arm64.go
+++ b/route/zsys_freebsd_arm64.go
@@ -6,7 +6,6 @@ package route
const (
sizeofIfMsghdrlFreeBSD10 = 0xb0
sizeofIfaMsghdrFreeBSD10 = 0x14
- sizeofIfaMsghdrlFreeBSD10 = 0xb0
sizeofIfmaMsghdrFreeBSD10 = 0x10
sizeofIfAnnouncemsghdrFreeBSD10 = 0x18
@@ -19,15 +18,7 @@ const (
sizeofIfMsghdrFreeBSD10 = 0xa8
sizeofIfMsghdrFreeBSD11 = 0xa8
- sizeofIfDataFreeBSD7 = 0x98
- sizeofIfDataFreeBSD8 = 0x98
- sizeofIfDataFreeBSD9 = 0x98
- sizeofIfDataFreeBSD10 = 0x98
- sizeofIfDataFreeBSD11 = 0x98
-
- sizeofIfMsghdrlFreeBSD10Emu = 0xb0
sizeofIfaMsghdrFreeBSD10Emu = 0x14
- sizeofIfaMsghdrlFreeBSD10Emu = 0xb0
sizeofIfmaMsghdrFreeBSD10Emu = 0x10
sizeofIfAnnouncemsghdrFreeBSD10Emu = 0x18
@@ -40,13 +31,6 @@ const (
sizeofIfMsghdrFreeBSD10Emu = 0xa8
sizeofIfMsghdrFreeBSD11Emu = 0xa8
- sizeofIfDataFreeBSD7Emu = 0x98
- sizeofIfDataFreeBSD8Emu = 0x98
- sizeofIfDataFreeBSD9Emu = 0x98
- sizeofIfDataFreeBSD10Emu = 0x98
- sizeofIfDataFreeBSD11Emu = 0x98
-
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_freebsd_riscv64.go b/route/zsys_freebsd_riscv64.go
index 9293393698..3d7f31d13e 100644
--- a/route/zsys_freebsd_riscv64.go
+++ b/route/zsys_freebsd_riscv64.go
@@ -6,7 +6,6 @@ package route
const (
sizeofIfMsghdrlFreeBSD10 = 0xb0
sizeofIfaMsghdrFreeBSD10 = 0x14
- sizeofIfaMsghdrlFreeBSD10 = 0xb0
sizeofIfmaMsghdrFreeBSD10 = 0x10
sizeofIfAnnouncemsghdrFreeBSD10 = 0x18
@@ -19,15 +18,7 @@ const (
sizeofIfMsghdrFreeBSD10 = 0xa8
sizeofIfMsghdrFreeBSD11 = 0xa8
- sizeofIfDataFreeBSD7 = 0x98
- sizeofIfDataFreeBSD8 = 0x98
- sizeofIfDataFreeBSD9 = 0x98
- sizeofIfDataFreeBSD10 = 0x98
- sizeofIfDataFreeBSD11 = 0x98
-
- sizeofIfMsghdrlFreeBSD10Emu = 0xb0
sizeofIfaMsghdrFreeBSD10Emu = 0x14
- sizeofIfaMsghdrlFreeBSD10Emu = 0xb0
sizeofIfmaMsghdrFreeBSD10Emu = 0x10
sizeofIfAnnouncemsghdrFreeBSD10Emu = 0x18
@@ -40,13 +31,6 @@ const (
sizeofIfMsghdrFreeBSD10Emu = 0xa8
sizeofIfMsghdrFreeBSD11Emu = 0xa8
- sizeofIfDataFreeBSD7Emu = 0x98
- sizeofIfDataFreeBSD8Emu = 0x98
- sizeofIfDataFreeBSD9Emu = 0x98
- sizeofIfDataFreeBSD10Emu = 0x98
- sizeofIfDataFreeBSD11Emu = 0x98
-
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_netbsd.go b/route/zsys_netbsd.go
index eaffe8c408..90ce707d47 100644
--- a/route/zsys_netbsd.go
+++ b/route/zsys_netbsd.go
@@ -8,10 +8,8 @@ const (
sizeofIfaMsghdrNetBSD7 = 0x18
sizeofIfAnnouncemsghdrNetBSD7 = 0x18
- sizeofRtMsghdrNetBSD7 = 0x78
- sizeofRtMetricsNetBSD7 = 0x50
+ sizeofRtMsghdrNetBSD7 = 0x78
- sizeofSockaddrStorage = 0x80
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/route/zsys_openbsd.go b/route/zsys_openbsd.go
index b11b812680..64fbdd98fb 100644
--- a/route/zsys_openbsd.go
+++ b/route/zsys_openbsd.go
@@ -6,7 +6,6 @@ package route
const (
sizeofRtMsghdr = 0x60
- sizeofSockaddrStorage = 0x100
- sizeofSockaddrInet = 0x10
- sizeofSockaddrInet6 = 0x1c
+ sizeofSockaddrInet = 0x10
+ sizeofSockaddrInet6 = 0x1c
)
diff --git a/websocket/websocket.go b/websocket/websocket.go
index 923a5780ec..ac76165ceb 100644
--- a/websocket/websocket.go
+++ b/websocket/websocket.go
@@ -8,7 +8,7 @@
// This package currently lacks some features found in an alternative
// and more actively maintained WebSocket package:
//
-// https://pkg.go.dev/nhooyr.io/websocket
+// https://pkg.go.dev/github.com/coder/websocket
package websocket // import "golang.org/x/net/websocket"
import (