package http3
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/qlogwriter"
)
const NextProtoH3 = "h3"
type StreamType uint64
const (
streamTypeControlStream = 0
streamTypePushStream = 1
streamTypeQPACKEncoderStream = 2
streamTypeQPACKDecoderStream = 3
)
type QUICListener interface {
Accept (context .Context ) (*quic .Conn , error )
Addr () net .Addr
io .Closer
}
var _ QUICListener = &quic .EarlyListener {}
func ConfigureTLSConfig (tlsConf *tls .Config ) *tls .Config {
_, _ = tlsConf .DecryptTicket (nil , tls .ConnectionState {})
config := tlsConf .Clone ()
config .NextProtos = []string {NextProtoH3 }
if gfc := config .GetConfigForClient ; gfc != nil {
config .GetConfigForClient = func (ch *tls .ClientHelloInfo ) (*tls .Config , error ) {
conf , err := gfc (ch )
if conf == nil || err != nil {
return conf , err
}
return ConfigureTLSConfig (conf ), nil
}
}
return config
}
type contextKey struct {
name string
}
func (k *contextKey ) String () string { return "quic-go/http3 context value " + k .name }
var ServerContextKey = &contextKey {"http3-server" }
var RemoteAddrContextKey = &contextKey {"remote-addr" }
type listener struct {
ln *QUICListener
port int
createdLocally bool
}
type Server struct {
Addr string
Port int
TLSConfig *tls .Config
QUICConfig *quic .Config
Handler http .Handler
EnableDatagrams bool
MaxHeaderBytes int
AdditionalSettings map [uint64 ]uint64
IdleTimeout time .Duration
ConnContext func (ctx context .Context , c *quic .Conn ) context .Context
Logger *slog .Logger
mutex sync .RWMutex
listeners []listener
closed bool
closeCtx context .Context
closeCancel context .CancelFunc
graceCtx context .Context
graceCancel context .CancelFunc
connCount atomic .Int64
connHandlingDone chan struct {}
altSvcHeader string
}
func (s *Server ) ListenAndServe () error {
ln , err := s .setupListenerForConn (s .TLSConfig , nil )
if err != nil {
return err
}
defer s .removeListener (ln )
return s .serveListener (*ln )
}
func (s *Server ) ListenAndServeTLS (certFile , keyFile string ) error {
var err error
certs := make ([]tls .Certificate , 1 )
certs [0 ], err = tls .LoadX509KeyPair (certFile , keyFile )
if err != nil {
return err
}
ln , err := s .setupListenerForConn (&tls .Config {Certificates : certs }, nil )
if err != nil {
return err
}
defer s .removeListener (ln )
return s .serveListener (*ln )
}
func (s *Server ) Serve (conn net .PacketConn ) error {
ln , err := s .setupListenerForConn (s .TLSConfig , conn )
if err != nil {
return err
}
defer s .removeListener (ln )
return s .serveListener (*ln )
}
func (s *Server ) init () {
if s .closeCtx == nil {
s .closeCtx , s .closeCancel = context .WithCancel (context .Background ())
s .graceCtx , s .graceCancel = context .WithCancel (s .closeCtx )
}
s .connHandlingDone = make (chan struct {}, 1 )
}
func (s *Server ) decreaseConnCount () {
if s .connCount .Add (-1 ) == 0 && s .graceCtx .Err () != nil {
close (s .connHandlingDone )
}
}
func (s *Server ) ServeQUICConn (conn *quic .Conn ) error {
s .mutex .Lock ()
if s .closed {
s .mutex .Unlock ()
return http .ErrServerClosed
}
s .init ()
s .mutex .Unlock ()
s .connCount .Add (1 )
defer s .decreaseConnCount ()
return s .handleConn (conn )
}
func (s *Server ) ServeListener (ln QUICListener ) error {
s .mutex .Lock ()
if err := s .addListener (&ln , false ); err != nil {
s .mutex .Unlock ()
return err
}
s .mutex .Unlock ()
defer s .removeListener (&ln )
return s .serveListener (ln )
}
func (s *Server ) serveListener (ln QUICListener ) error {
for {
conn , err := ln .Accept (s .graceCtx )
if errors .Is (err , quic .ErrServerClosed ) || s .graceCtx .Err () != nil {
return http .ErrServerClosed
}
if err != nil {
return err
}
s .connCount .Add (1 )
go func () {
defer s .decreaseConnCount ()
if err := s .handleConn (conn ); err != nil {
if s .Logger != nil {
s .Logger .Debug ("handling connection failed" , "error" , err )
}
}
}()
}
}
var errServerWithoutTLSConfig = errors .New ("use of http3.Server without TLSConfig" )
func (s *Server ) setupListenerForConn (tlsConf *tls .Config , conn net .PacketConn ) (*QUICListener , error ) {
if tlsConf == nil {
return nil , errServerWithoutTLSConfig
}
baseConf := ConfigureTLSConfig (tlsConf )
quicConf := s .QUICConfig
if quicConf == nil {
quicConf = &quic .Config {Allow0RTT : true }
} else {
quicConf = s .QUICConfig .Clone ()
}
if s .EnableDatagrams {
quicConf .EnableDatagrams = true
}
s .mutex .Lock ()
defer s .mutex .Unlock ()
closed := s .closed
if closed {
return nil , http .ErrServerClosed
}
var ln QUICListener
var err error
if conn == nil {
addr := s .Addr
if addr == "" {
addr = ":https"
}
ln , err = quic .ListenAddrEarly (addr , baseConf , quicConf )
} else {
ln , err = quic .ListenEarly (conn , baseConf , quicConf )
}
if err != nil {
return nil , err
}
if err := s .addListener (&ln , true ); err != nil {
return nil , err
}
return &ln , nil
}
func extractPort(addr string ) (int , error ) {
_ , portStr , err := net .SplitHostPort (addr )
if err != nil {
return 0 , err
}
portInt , err := net .LookupPort ("tcp" , portStr )
if err != nil {
return 0 , err
}
return portInt , nil
}
func (s *Server ) generateAltSvcHeader () {
if len (s .listeners ) == 0 {
s .altSvcHeader = ""
return
}
var altSvc []string
addPort := func (port int ) {
altSvc = append (altSvc , fmt .Sprintf (`%s=":%d"; ma=2592000` , NextProtoH3 , port ))
}
if s .Port != 0 {
addPort (s .Port )
} else {
validPortsFound := false
for _ , info := range s .listeners {
if info .port != 0 {
addPort (info .port )
validPortsFound = true
}
}
if !validPortsFound {
if port , err := extractPort (s .Addr ); err == nil {
addPort (port )
}
}
}
s .altSvcHeader = strings .Join (altSvc , "," )
}
func (s *Server ) addListener (l *QUICListener , createdLocally bool ) error {
if s .closed {
return http .ErrServerClosed
}
s .init ()
laddr := (*l ).Addr ()
if port , err := extractPort (laddr .String ()); err == nil {
s .listeners = append (s .listeners , listener {ln : l , port : port , createdLocally : createdLocally })
} else {
logger := s .Logger
if logger == nil {
logger = slog .Default ()
}
logger .Error ("Unable to extract port from listener, will not be announced using SetQUICHeaders" , "local addr" , laddr , "error" , err )
s .listeners = append (s .listeners , listener {ln : l , port : 0 , createdLocally : createdLocally })
}
s .generateAltSvcHeader ()
return nil
}
func (s *Server ) removeListener (l *QUICListener ) {
s .mutex .Lock ()
defer s .mutex .Unlock ()
s .listeners = slices .DeleteFunc (s .listeners , func (info listener ) bool {
return info .ln == l
})
s .generateAltSvcHeader ()
}
func (s *Server ) NewRawServerConn (conn *quic .Conn ) (*RawServerConn , error ) {
hconn , _ , _ , err := s .newRawServerConn (conn )
if err != nil {
return nil , err
}
return hconn , nil
}
func (s *Server ) newRawServerConn (conn *quic .Conn ) (*RawServerConn , *quic .SendStream , qlogwriter .Recorder , error ) {
var qlogger qlogwriter .Recorder
if qlogTrace := conn .QlogTrace (); qlogTrace != nil && qlogTrace .SupportsSchemas (qlog .EventSchema ) {
qlogger = qlogTrace .AddProducer ()
}
connCtx := conn .Context ()
connCtx = context .WithValue (connCtx , ServerContextKey , s )
connCtx = context .WithValue (connCtx , http .LocalAddrContextKey , conn .LocalAddr ())
connCtx = context .WithValue (connCtx , RemoteAddrContextKey , conn .RemoteAddr ())
if s .ConnContext != nil {
connCtx = s .ConnContext (connCtx , conn )
if connCtx == nil {
panic ("http3: ConnContext returned nil" )
}
}
hconn := newRawServerConn (
conn ,
s .EnableDatagrams ,
s .IdleTimeout ,
qlogger ,
s .Logger ,
connCtx ,
s .Handler ,
s .maxHeaderBytes (),
)
ctrlStr , err := hconn .openControlStream (&settingsFrame {
MaxFieldSectionSize : int64 (s .maxHeaderBytes ()),
Datagram : s .EnableDatagrams ,
ExtendedConnect : true ,
Other : s .AdditionalSettings ,
})
if err != nil {
return nil , nil , nil , fmt .Errorf ("opening the control stream failed: %w" , err )
}
return hconn , ctrlStr , qlogger , nil
}
func (s *Server ) handleConn (conn *quic .Conn ) error {
hconn , ctrlStr , qlogger , err := s .newRawServerConn (conn )
if err != nil {
return err
}
var wg sync .WaitGroup
wg .Add (1 )
go func () {
defer wg .Done ()
for {
str , err := conn .AcceptUniStream (context .Background ())
if err != nil {
return
}
go hconn .HandleUnidirectionalStream (str )
}
}()
var nextStreamID quic .StreamID
var handleErr error
var inGracefulShutdown bool
ctx := s .graceCtx
for {
str , err := conn .AcceptStream (ctx )
if err != nil {
if conn .Context ().Err () != nil {
var appErr *quic .ApplicationError
if !errors .As (err , &appErr ) || appErr .ErrorCode != quic .ApplicationErrorCode (ErrCodeNoError ) {
handleErr = fmt .Errorf ("accepting stream failed: %w" , err )
}
break
}
if s .closeCtx .Err () != nil {
hconn .CloseWithError (quic .ApplicationErrorCode (ErrCodeNoError ), "" )
handleErr = http .ErrServerClosed
break
}
inGracefulShutdown = s .graceCtx .Err () != nil
if !inGracefulShutdown {
var appErr *quic .ApplicationError
if !errors .As (err , &appErr ) || appErr .ErrorCode != quic .ApplicationErrorCode (ErrCodeNoError ) {
handleErr = fmt .Errorf ("accepting stream failed: %w" , err )
}
break
}
if qlogger != nil {
qlogger .RecordEvent (qlog .FrameCreated {
StreamID : ctrlStr .StreamID (),
Frame : qlog .Frame {Frame : qlog .GoAwayFrame {StreamID : nextStreamID }},
})
}
wg .Add (1 )
go func () {
defer wg .Done ()
_, _ = ctrlStr .Write ((&goAwayFrame {StreamID : nextStreamID }).Append (nil ))
}()
ctx = s .closeCtx
continue
}
if inGracefulShutdown {
str .CancelRead (quic .StreamErrorCode (ErrCodeRequestRejected ))
str .CancelWrite (quic .StreamErrorCode (ErrCodeRequestRejected ))
continue
}
nextStreamID = str .StreamID () + 4
wg .Add (1 )
go func () {
defer wg .Done ()
hconn .HandleRequestStream (str )
}()
}
wg .Wait ()
return handleErr
}
func (s *Server ) maxHeaderBytes () int {
if s .MaxHeaderBytes <= 0 {
return http .DefaultMaxHeaderBytes
}
return s .MaxHeaderBytes
}
func (s *Server ) Close () error {
s .mutex .Lock ()
defer s .mutex .Unlock ()
s .closed = true
if s .closeCtx == nil {
return nil
}
s .closeCancel ()
var err error
for _ , l := range s .listeners {
if l .createdLocally {
if cerr := (*l .ln ).Close (); cerr != nil && err == nil {
err = cerr
}
}
}
if s .connCount .Load () == 0 {
return err
}
<-s .connHandlingDone
return err
}
func (s *Server ) Shutdown (ctx context .Context ) error {
s .mutex .Lock ()
s .closed = true
if s .closeCtx == nil {
s .mutex .Unlock ()
return nil
}
s .graceCancel ()
var closeErrs []error
for _ , l := range s .listeners {
if l .createdLocally {
if err := (*l .ln ).Close (); err != nil {
closeErrs = append (closeErrs , err )
}
}
}
s .mutex .Unlock ()
if len (closeErrs ) > 0 {
return errors .Join (closeErrs ...)
}
if s .connCount .Load () == 0 {
return s .Close ()
}
select {
case <- s .connHandlingDone :
return s .Close ()
case <- ctx .Done ():
_ = s .Close ()
return ctx .Err ()
}
}
var ErrNoAltSvcPort = errors .New ("no port can be announced, specify it explicitly using Server.Port or Server.Addr" )
func (s *Server ) SetQUICHeaders (hdr http .Header ) error {
s .mutex .RLock ()
defer s .mutex .RUnlock ()
if s .altSvcHeader == "" {
return ErrNoAltSvcPort
}
hdr ["Alt-Svc" ] = append (hdr ["Alt-Svc" ], s .altSvcHeader )
return nil
}
func ListenAndServeQUIC (addr , certFile , keyFile string , handler http .Handler ) error {
server := &Server {
Addr : addr ,
Handler : handler ,
}
return server .ListenAndServeTLS (certFile , keyFile )
}
func ListenAndServeTLS (addr , certFile , keyFile string , handler http .Handler ) error {
var err error
certs := make ([]tls .Certificate , 1 )
certs [0 ], err = tls .LoadX509KeyPair (certFile , keyFile )
if err != nil {
return err
}
config := &tls .Config {
Certificates : certs ,
}
if addr == "" {
addr = ":https"
}
udpAddr , err := net .ResolveUDPAddr ("udp" , addr )
if err != nil {
return err
}
udpConn , err := net .ListenUDP ("udp" , udpAddr )
if err != nil {
return err
}
defer udpConn .Close ()
if handler == nil {
handler = http .DefaultServeMux
}
quicServer := &Server {
TLSConfig : config ,
Handler : handler ,
}
hErr := make (chan error , 1 )
qErr := make (chan error , 1 )
go func () {
hErr <- http .ListenAndServeTLS (addr , certFile , keyFile , http .HandlerFunc (func (w http .ResponseWriter , r *http .Request ) {
quicServer .SetQUICHeaders (w .Header ())
handler .ServeHTTP (w , r )
}))
}()
go func () {
qErr <- quicServer .Serve (udpConn )
}()
select {
case err := <- hErr :
quicServer .Close ()
return err
case err := <- qErr :
return err
}
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .