Skip to content

Commit 163d836

Browse files
committed
internal/http3: add Server
Add the general structure of an HTTP/3 server. The server currently accepts QUIC connections and establishes a control stream on them, but does not handle requests. For golang/go#70914 Change-Id: I28193ddacef028233248601979b0b45ad844205a Reviewed-on: https://go-review.googlesource.com/c/net/+/646617 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
1 parent 447f458 commit 163d836

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed

internal/http3/conn_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,9 @@ func runConnTest(t *testing.T, f func(testing.TB, *testQUICConn)) {
146146
tc := newTestClientConn(t)
147147
f(t, tc.testQUICConn)
148148
})
149+
runSynctestSubtest(t, "server", func(t testing.TB) {
150+
ts := newTestServer(t)
151+
tc := ts.connect()
152+
f(t, tc.testQUICConn)
153+
})
149154
}

internal/http3/server.go

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http3
8+
9+
import (
10+
"context"
11+
"net/http"
12+
"sync"
13+
14+
"golang.org/x/net/quic"
15+
)
16+
17+
// A Server is an HTTP/3 server.
18+
// The zero value for Server is a valid server.
19+
type Server struct {
20+
// Handler to invoke for requests, http.DefaultServeMux if nil.
21+
Handler http.Handler
22+
23+
// Config is the QUIC configuration used by the server.
24+
// The Config may be nil.
25+
//
26+
// ListenAndServe may clone and modify the Config.
27+
// The Config must not be modified after calling ListenAndServe.
28+
Config *quic.Config
29+
30+
initOnce sync.Once
31+
}
32+
33+
func (s *Server) init() {
34+
s.initOnce.Do(func() {
35+
s.Config = initConfig(s.Config)
36+
if s.Handler == nil {
37+
s.Handler = http.DefaultServeMux
38+
}
39+
})
40+
}
41+
42+
// ListenAndServe listens on the UDP network address addr
43+
// and then calls Serve to handle requests on incoming connections.
44+
func (s *Server) ListenAndServe(addr string) error {
45+
s.init()
46+
e, err := quic.Listen("udp", addr, s.Config)
47+
if err != nil {
48+
return err
49+
}
50+
return s.Serve(e)
51+
}
52+
53+
// Serve accepts incoming connections on the QUIC endpoint e,
54+
// and handles requests from those connections.
55+
func (s *Server) Serve(e *quic.Endpoint) error {
56+
s.init()
57+
for {
58+
qconn, err := e.Accept(context.Background())
59+
if err != nil {
60+
return err
61+
}
62+
go newServerConn(qconn)
63+
}
64+
}
65+
66+
type serverConn struct {
67+
qconn *quic.Conn
68+
69+
genericConn // for handleUnidirectionalStream
70+
enc qpackEncoder
71+
dec qpackDecoder
72+
}
73+
74+
func newServerConn(qconn *quic.Conn) {
75+
sc := &serverConn{
76+
qconn: qconn,
77+
}
78+
sc.enc.init()
79+
80+
// Create control stream and send SETTINGS frame.
81+
// TODO: Time out on creating stream.
82+
controlStream, err := newConnStream(context.Background(), sc.qconn, streamTypeControl)
83+
if err != nil {
84+
return
85+
}
86+
controlStream.writeSettings()
87+
controlStream.Flush()
88+
89+
// Accept streams on the connection.
90+
for {
91+
st, err := sc.qconn.AcceptStream(context.Background())
92+
if err != nil {
93+
return // connection closed
94+
}
95+
if st.IsReadOnly() {
96+
go sc.handleUnidirectionalStream(newStream(st), sc)
97+
} else {
98+
go sc.handleRequestStream(newStream(st))
99+
}
100+
}
101+
}
102+
103+
func (sc *serverConn) handleControlStream(st *stream) error {
104+
// "A SETTINGS frame MUST be sent as the first frame of each control stream [...]"
105+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2
106+
if err := st.readSettings(func(settingsType, settingsValue int64) error {
107+
switch settingsType {
108+
case settingsMaxFieldSectionSize:
109+
_ = settingsValue // TODO
110+
case settingsQPACKMaxTableCapacity:
111+
_ = settingsValue // TODO
112+
case settingsQPACKBlockedStreams:
113+
_ = settingsValue // TODO
114+
default:
115+
// Unknown settings types are ignored.
116+
}
117+
return nil
118+
}); err != nil {
119+
return err
120+
}
121+
122+
for {
123+
ftype, err := st.readFrameHeader()
124+
if err != nil {
125+
return err
126+
}
127+
switch ftype {
128+
case frameTypeCancelPush:
129+
// "If a server receives a CANCEL_PUSH frame for a push ID
130+
// that has not yet been mentioned by a PUSH_PROMISE frame,
131+
// this MUST be treated as a connection error of type H3_ID_ERROR."
132+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-8
133+
return &connectionError{
134+
code: errH3IDError,
135+
message: "CANCEL_PUSH for unsent push ID",
136+
}
137+
case frameTypeGoaway:
138+
return errH3NoError
139+
default:
140+
// Unknown frames are ignored.
141+
if err := st.discardUnknownFrame(ftype); err != nil {
142+
return err
143+
}
144+
}
145+
}
146+
}
147+
148+
func (sc *serverConn) handleEncoderStream(*stream) error {
149+
// TODO
150+
return nil
151+
}
152+
153+
func (sc *serverConn) handleDecoderStream(*stream) error {
154+
// TODO
155+
return nil
156+
}
157+
158+
func (sc *serverConn) handlePushStream(*stream) error {
159+
// "[...] if a server receives a client-initiated push stream,
160+
// this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR."
161+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.2-3
162+
return &connectionError{
163+
code: errH3StreamCreationError,
164+
message: "client created push stream",
165+
}
166+
}
167+
168+
func (sc *serverConn) handleRequestStream(st *stream) {
169+
// TODO
170+
return
171+
}
172+
173+
// abort closes the connection with an error.
174+
func (sc *serverConn) abort(err error) {
175+
if e, ok := err.(*connectionError); ok {
176+
sc.qconn.Abort(&quic.ApplicationError{
177+
Code: uint64(e.code),
178+
Reason: e.message,
179+
})
180+
} else {
181+
sc.qconn.Abort(err)
182+
}
183+
}

internal/http3/server_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24 && goexperiment.synctest
6+
7+
package http3
8+
9+
import (
10+
"net/netip"
11+
"testing"
12+
"testing/synctest"
13+
14+
"golang.org/x/net/internal/quic/quicwire"
15+
"golang.org/x/net/quic"
16+
)
17+
18+
func TestServerReceivePushStream(t *testing.T) {
19+
// "[...] if a server receives a client-initiated push stream,
20+
// this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR."
21+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.2-3
22+
runSynctest(t, func(t testing.TB) {
23+
ts := newTestServer(t)
24+
tc := ts.connect()
25+
tc.newStream(streamTypePush)
26+
tc.wantClosed("invalid client-created push stream", errH3StreamCreationError)
27+
})
28+
}
29+
30+
func TestServerCancelPushForUnsentPromise(t *testing.T) {
31+
runSynctest(t, func(t testing.TB) {
32+
ts := newTestServer(t)
33+
tc := ts.connect()
34+
tc.greet()
35+
36+
const pushID = 100
37+
tc.control.writeVarint(int64(frameTypeCancelPush))
38+
tc.control.writeVarint(int64(quicwire.SizeVarint(pushID)))
39+
tc.control.writeVarint(pushID)
40+
tc.control.Flush()
41+
42+
tc.wantClosed("client canceled never-sent push ID", errH3IDError)
43+
})
44+
}
45+
46+
type testServer struct {
47+
t testing.TB
48+
s *Server
49+
tn testNet
50+
*testQUICEndpoint
51+
52+
addr netip.AddrPort
53+
}
54+
55+
type testQUICEndpoint struct {
56+
t testing.TB
57+
e *quic.Endpoint
58+
}
59+
60+
func (te *testQUICEndpoint) dial() {
61+
}
62+
63+
type testServerConn struct {
64+
ts *testServer
65+
66+
*testQUICConn
67+
control *testQUICStream
68+
}
69+
70+
func newTestServer(t testing.TB) *testServer {
71+
t.Helper()
72+
ts := &testServer{
73+
t: t,
74+
s: &Server{
75+
Config: &quic.Config{
76+
TLSConfig: testTLSConfig,
77+
},
78+
},
79+
}
80+
e := ts.tn.newQUICEndpoint(t, ts.s.Config)
81+
ts.addr = e.LocalAddr()
82+
go ts.s.Serve(e)
83+
return ts
84+
}
85+
86+
func (ts *testServer) connect() *testServerConn {
87+
ts.t.Helper()
88+
config := &quic.Config{TLSConfig: testTLSConfig}
89+
e := ts.tn.newQUICEndpoint(ts.t, nil)
90+
qconn, err := e.Dial(ts.t.Context(), "udp", ts.addr.String(), config)
91+
if err != nil {
92+
ts.t.Fatal(err)
93+
}
94+
tc := &testServerConn{
95+
ts: ts,
96+
testQUICConn: newTestQUICConn(ts.t, qconn),
97+
}
98+
synctest.Wait()
99+
return tc
100+
}
101+
102+
// greet performs initial connection handshaking with the server.
103+
func (tc *testServerConn) greet() {
104+
// Client creates a control stream.
105+
tc.control = tc.newStream(streamTypeControl)
106+
tc.control.writeVarint(int64(frameTypeSettings))
107+
tc.control.writeVarint(0) // size
108+
tc.control.Flush()
109+
synctest.Wait()
110+
}

0 commit comments

Comments
 (0)