| // Copyright 2025 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 http3 |
| |
| import ( |
| "context" |
| "net/http" |
| "net/url" |
| "strconv" |
| "sync" |
| |
| "golang.org/x/net/http/httpguts" |
| "golang.org/x/net/quic" |
| ) |
| |
| // A Server is an HTTP/3 server. |
| // The zero value for Server is a valid server. |
| type Server struct { |
| // Handler to invoke for requests, http.DefaultServeMux if nil. |
| Handler http.Handler |
| |
| // Config is the QUIC configuration used by the server. |
| // The Config may be nil. |
| // |
| // ListenAndServe may clone and modify the Config. |
| // The Config must not be modified after calling ListenAndServe. |
| Config *quic.Config |
| |
| initOnce sync.Once |
| } |
| |
| func (s *Server) init() { |
| s.initOnce.Do(func() { |
| s.Config = initConfig(s.Config) |
| if s.Handler == nil { |
| s.Handler = http.DefaultServeMux |
| } |
| }) |
| } |
| |
| // ListenAndServe listens on the UDP network address addr |
| // and then calls Serve to handle requests on incoming connections. |
| func (s *Server) ListenAndServe(addr string) error { |
| s.init() |
| e, err := quic.Listen("udp", addr, s.Config) |
| if err != nil { |
| return err |
| } |
| return s.Serve(e) |
| } |
| |
| // Serve accepts incoming connections on the QUIC endpoint e, |
| // and handles requests from those connections. |
| func (s *Server) Serve(e *quic.Endpoint) error { |
| s.init() |
| for { |
| qconn, err := e.Accept(context.Background()) |
| if err != nil { |
| return err |
| } |
| go newServerConn(qconn, s.Handler) |
| } |
| } |
| |
| type serverConn struct { |
| qconn *quic.Conn |
| |
| genericConn // for handleUnidirectionalStream |
| enc qpackEncoder |
| dec qpackDecoder |
| handler http.Handler |
| } |
| |
| func newServerConn(qconn *quic.Conn, handler http.Handler) { |
| sc := &serverConn{ |
| qconn: qconn, |
| handler: handler, |
| } |
| sc.enc.init() |
| |
| // Create control stream and send SETTINGS frame. |
| // TODO: Time out on creating stream. |
| controlStream, err := newConnStream(context.Background(), sc.qconn, streamTypeControl) |
| if err != nil { |
| return |
| } |
| controlStream.writeSettings() |
| controlStream.Flush() |
| |
| sc.acceptStreams(sc.qconn, sc) |
| } |
| |
| func (sc *serverConn) handleControlStream(st *stream) error { |
| // "A SETTINGS frame MUST be sent as the first frame of each control stream [...]" |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2 |
| if err := st.readSettings(func(settingsType, settingsValue int64) error { |
| switch settingsType { |
| case settingsMaxFieldSectionSize: |
| _ = settingsValue // TODO |
| case settingsQPACKMaxTableCapacity: |
| _ = settingsValue // TODO |
| case settingsQPACKBlockedStreams: |
| _ = settingsValue // TODO |
| default: |
| // Unknown settings types are ignored. |
| } |
| return nil |
| }); err != nil { |
| return err |
| } |
| |
| for { |
| ftype, err := st.readFrameHeader() |
| if err != nil { |
| return err |
| } |
| switch ftype { |
| case frameTypeCancelPush: |
| // "If a server receives a CANCEL_PUSH frame for a push ID |
| // that has not yet been mentioned by a PUSH_PROMISE frame, |
| // this MUST be treated as a connection error of type H3_ID_ERROR." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-8 |
| return &connectionError{ |
| code: errH3IDError, |
| message: "CANCEL_PUSH for unsent push ID", |
| } |
| case frameTypeGoaway: |
| return errH3NoError |
| default: |
| // Unknown frames are ignored. |
| if err := st.discardUnknownFrame(ftype); err != nil { |
| return err |
| } |
| } |
| } |
| } |
| |
| func (sc *serverConn) handleEncoderStream(*stream) error { |
| // TODO |
| return nil |
| } |
| |
| func (sc *serverConn) handleDecoderStream(*stream) error { |
| // TODO |
| return nil |
| } |
| |
| func (sc *serverConn) handlePushStream(*stream) error { |
| // "[...] if a server receives a client-initiated push stream, |
| // this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.2-3 |
| return &connectionError{ |
| code: errH3StreamCreationError, |
| message: "client created push stream", |
| } |
| } |
| |
| func (sc *serverConn) parseRequest(st *stream) (*http.Request, error) { |
| req := &http.Request{ |
| URL: &url.URL{}, |
| Proto: "HTTP/3.0", |
| ProtoMajor: 3, |
| RemoteAddr: sc.qconn.RemoteAddr().String(), |
| } |
| ftype, err := st.readFrameHeader() |
| if err != nil { |
| return nil, err |
| } |
| if ftype != frameTypeHeaders { |
| return nil, err |
| } |
| req.Header = make(http.Header) |
| var dec qpackDecoder |
| if err := dec.decode(st, func(_ indexType, name, value string) error { |
| switch name { |
| case ":method": |
| req.Method = value |
| case ":scheme": |
| req.URL.Scheme = value |
| case ":path": |
| req.URL.Path = value |
| case ":authority": |
| req.URL.Host = value |
| default: |
| req.Header.Add(name, value) |
| } |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| if err := st.endFrame(); err != nil { |
| return nil, err |
| } |
| req.Body = &bodyReader{ |
| st: st, |
| remain: -1, |
| } |
| return req, nil |
| } |
| |
| func (sc *serverConn) handleRequestStream(st *stream) error { |
| req, err := sc.parseRequest(st) |
| if err != nil { |
| return err |
| } |
| defer req.Body.Close() |
| |
| rw := &responseWriter{ |
| st: st, |
| headers: make(http.Header), |
| isHeadResp: req.Method == "HEAD", |
| bw: &bodyWriter{ |
| st: st, |
| remain: -1, |
| flush: false, |
| name: "response", |
| }, |
| } |
| defer rw.close() |
| |
| // TODO: handle panic coming from the HTTP handler. |
| sc.handler.ServeHTTP(rw, req) |
| return nil |
| } |
| |
| // abort closes the connection with an error. |
| func (sc *serverConn) abort(err error) { |
| if e, ok := err.(*connectionError); ok { |
| sc.qconn.Abort(&quic.ApplicationError{ |
| Code: uint64(e.code), |
| Reason: e.message, |
| }) |
| } else { |
| sc.qconn.Abort(err) |
| } |
| } |
| |
| type responseWriter struct { |
| st *stream |
| bw *bodyWriter |
| mu sync.Mutex |
| headers http.Header |
| // TODO: support 1xx status |
| wroteHeader bool // Non-1xx header has been (logically) written. |
| isHeadResp bool // response is for a HEAD request. |
| } |
| |
| func (rw *responseWriter) Header() http.Header { |
| return rw.headers |
| } |
| |
| // Caller must hold rw.mu. If rw.wroteHeader is true, calling this method is a |
| // no-op. |
| func (rw *responseWriter) writeHeaderLockedOnce(statusCode int) { |
| // TODO: support trailer header. |
| if rw.wroteHeader { |
| return |
| } |
| enc := &qpackEncoder{} |
| enc.init() |
| encHeaders := enc.encode(func(f func(itype indexType, name, value string)) { |
| f(mayIndex, ":status", strconv.Itoa(statusCode)) |
| for name, values := range rw.headers { |
| if !httpguts.ValidHeaderFieldName(name) { |
| continue |
| } |
| for _, val := range values { |
| if !httpguts.ValidHeaderFieldValue(val) { |
| continue |
| } |
| // Issue #71374: Consider supporting never-indexed fields. |
| f(mayIndex, name, val) |
| } |
| } |
| }) |
| rw.st.writeVarint(int64(frameTypeHeaders)) |
| rw.st.writeVarint(int64(len(encHeaders))) |
| rw.st.Write(encHeaders) |
| rw.wroteHeader = true |
| } |
| |
| func (rw *responseWriter) WriteHeader(statusCode int) { |
| rw.mu.Lock() |
| defer rw.mu.Unlock() |
| rw.writeHeaderLockedOnce(statusCode) |
| } |
| |
| func (rw *responseWriter) Write(b []byte) (int, error) { |
| rw.mu.Lock() |
| defer rw.mu.Unlock() |
| rw.writeHeaderLockedOnce(http.StatusOK) |
| if rw.isHeadResp { |
| return 0, nil |
| } |
| return rw.bw.Write(b) |
| } |
| |
| func (rw *responseWriter) Flush() { |
| rw.bw.st.Flush() |
| } |
| |
| func (rw *responseWriter) close() error { |
| rw.mu.Lock() |
| defer rw.mu.Unlock() |
| rw.writeHeaderLockedOnce(http.StatusOK) |
| return rw.st.stream.Close() |
| } |