199 lines
4.8 KiB
Go
199 lines
4.8 KiB
Go
// Package httpp3 contains HTTP/3 utilities.
|
|
package httpp3
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bluenviron/gortsplib/v5/pkg/readbuffer"
|
|
"github.com/quic-go/quic-go/http3"
|
|
"github.com/quic-go/webtransport-go"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
)
|
|
|
|
const (
|
|
// WebTransport requires certificates valid for at most 14 days.
|
|
certValidity = 14 * 24 * time.Hour
|
|
certRotation = 13 * 24 * time.Hour
|
|
)
|
|
|
|
func generateWebTransportCert() (*tls.Certificate, error) {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate key: %w", err)
|
|
}
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("generate serial: %w", err)
|
|
}
|
|
|
|
now := time.Now()
|
|
template := x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{CommonName: "mediamtx"},
|
|
NotBefore: now,
|
|
NotAfter: now.Add(certValidity),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
}
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create certificate: %w", err)
|
|
}
|
|
|
|
keyDER, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal key: %w", err)
|
|
}
|
|
|
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
|
|
|
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse certificate: %w", err)
|
|
}
|
|
|
|
return &cert, nil
|
|
}
|
|
|
|
// Server is a wrapper around http3.Server that provides:
|
|
// - net.PacketConn creation and destruction
|
|
// - JIT TLS certificate generation and rotation (WebTransport-compatible: ECDSA P-256, 14-day validity)
|
|
type Server struct {
|
|
Address string
|
|
UDPReadBufferSize uint
|
|
EnableWebTransport bool
|
|
Handler http.Handler
|
|
Parent logger.Writer
|
|
|
|
ln net.PacketConn
|
|
inner *http3.Server
|
|
wtServer *webtransport.Server
|
|
cert *tls.Certificate
|
|
certMu sync.RWMutex
|
|
terminate chan struct{}
|
|
}
|
|
|
|
// Initialize initializes a Server.
|
|
func (s *Server) Initialize() error {
|
|
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true") //nolint:errcheck
|
|
|
|
s.terminate = make(chan struct{})
|
|
|
|
var err error
|
|
s.ln, err = net.ListenPacket("udp", s.Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.UDPReadBufferSize != 0 {
|
|
err = readbuffer.SetReadBuffer(s.ln.(*net.UDPConn), int(s.UDPReadBufferSize))
|
|
if err != nil {
|
|
s.ln.Close()
|
|
return err
|
|
}
|
|
}
|
|
|
|
cert, err := generateWebTransportCert()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.cert = cert
|
|
|
|
tlsConfig := &tls.Config{
|
|
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
s.certMu.RLock()
|
|
defer s.certMu.RUnlock()
|
|
return s.cert, nil
|
|
},
|
|
}
|
|
|
|
s.inner = &http3.Server{
|
|
Handler: s.Handler,
|
|
TLSConfig: tlsConfig,
|
|
}
|
|
|
|
if s.EnableWebTransport {
|
|
// webtransport.Server.Serve calls quic.ListenEarly directly with s.H3.TLSConfig,
|
|
// bypassing the http3.ConfigureTLSConfig call that http3.Server.Serve does internally.
|
|
// Pre-configure to set NextProtos = ["h3"] so QUIC ALPN negotiation succeeds.
|
|
s.inner.TLSConfig = http3.ConfigureTLSConfig(tlsConfig)
|
|
webtransport.ConfigureHTTP3Server(s.inner)
|
|
s.wtServer = &webtransport.Server{
|
|
H3: s.inner,
|
|
CheckOrigin: func(_ *http.Request) bool { return true },
|
|
}
|
|
go s.wtServer.Serve(s.ln) //nolint:errcheck
|
|
} else {
|
|
go s.inner.Serve(s.ln) //nolint:errcheck
|
|
}
|
|
|
|
go s.rotateCert()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) rotateCert() {
|
|
t := time.NewTimer(certRotation)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
cert, err := generateWebTransportCert()
|
|
if err != nil {
|
|
s.Parent.Log(logger.Error, "failed to rotate WebTransport certificate: %v", err)
|
|
} else {
|
|
s.certMu.Lock()
|
|
s.cert = cert
|
|
s.certMu.Unlock()
|
|
s.Parent.Log(logger.Info, "WebTransport certificate rotated")
|
|
}
|
|
t.Reset(certRotation)
|
|
|
|
case <-s.terminate:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close closes all resources and waits for all routines to return.
|
|
func (s *Server) Close() {
|
|
close(s.terminate)
|
|
s.ln.Close()
|
|
if s.wtServer != nil {
|
|
s.wtServer.Close() //nolint:errcheck
|
|
} else {
|
|
s.inner.Close() //nolint:errcheck
|
|
}
|
|
}
|
|
|
|
// Certificate returns the current TLS certificate.
|
|
func (s *Server) Certificate() *tls.Certificate {
|
|
s.certMu.RLock()
|
|
defer s.certMu.RUnlock()
|
|
return s.cert
|
|
}
|
|
|
|
// Upgrade upgrades an HTTP/3 request to a WebTransport session.
|
|
// Only valid when EnableWebTransport is true.
|
|
func (s *Server) Upgrade(w http.ResponseWriter, r *http.Request) (*webtransport.Session, error) {
|
|
return s.wtServer.Upgrade(w, r)
|
|
}
|