Files
mediamtx/internal/protocols/httpp/server.go
T
Alessandro RosandGitHub a56c635f8e fix deadlock when changing configuration through file and API (#6077) (#6101)
When changing configuration in parallel by editing the configuration
file and calling the API, the server could get into a deadlock that
prevented any further action. This is fixed.
2026-08-17 11:14:01 +00:00

225 lines
4.4 KiB
Go

// Package httpp contains HTTP utilities.
package httpp
import (
"context"
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"time"
"golang.org/x/net/http2"
"github.com/bluenviron/mediamtx/internal/certloader"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/packetdumper"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
)
const (
shutdownTimeout = 2 * time.Second
)
type nilWriter struct{}
func (nilWriter) Write(p []byte) (int, error) {
return len(p), nil
}
// Server is a wrapper around http.Server that provides:
// - net.Listener creation and destruction
// - TLS initialization and hot reload
// - exit on panic
// - logging
// - server header
// - filtering of invalid requests
type Server struct {
Address string
AllowOrigins []string
DumpPackets bool
DumpPacketsPrefix string
ReadTimeout time.Duration
WriteTimeout time.Duration
Encryption bool
ServerCert string
ServerKey string
AllowAutoCert bool
GetCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)
Handler http.Handler
Parent logger.Writer
ln net.Listener
inner *http.Server
loader *certloader.CertLoader
tracker *handlerTracker
}
// Initialize initializes a Server.
func (s *Server) Initialize() error {
if s.ReadTimeout == 0 {
return fmt.Errorf("invalid ReadTimeout")
}
if s.WriteTimeout == 0 {
return fmt.Errorf("invalid WriteTimeout")
}
var tlsConfig *tls.Config
if s.Encryption {
var getCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error)
if s.GetCertificate != nil {
getCertificate = s.GetCertificate
} else {
if s.ServerCert == "" {
return fmt.Errorf("server cert is missing")
}
s.loader = &certloader.CertLoader{
CertPath: s.ServerCert,
KeyPath: s.ServerKey,
AllowAuto: s.AllowAutoCert,
Parent: s.Parent,
}
err := s.loader.Initialize()
if err != nil {
return err
}
getCertificate = s.loader.GetCertificate
}
tlsConfig = &tls.Config{
GetCertificate: getCertificate,
}
}
var network string
var address string
if strings.HasPrefix(s.Address, "unix://") {
network = "unix"
address = s.Address[len("unix://"):]
} else {
network, address = restrictnetwork.Restrict("tcp", s.Address)
}
if network == "unix" {
os.Remove(address)
}
h := s.Handler
h = &handlerOrigin{h, s.AllowOrigins}
h = &handlerServerHeader{h}
h = &handlerFilterRequests{h}
h = &handlerLogger{h, s.Parent}
h = &handlerExitOnPanic{h}
h = &handlerWriteTimeout{h, s.WriteTimeout}
s.tracker = &handlerTracker{h: h}
h = s.tracker
s.inner = &http.Server{
Handler: h,
TLSConfig: tlsConfig,
// applied before reading any request
ReadTimeout: s.ReadTimeout,
// applied after HTTP handler has returned
IdleTimeout: 30 * time.Second,
ErrorLog: log.New(&nilWriter{}, "", 0),
}
if tlsConfig != nil {
err := http2.ConfigureServer(s.inner, &http2.Server{})
if err != nil {
if s.loader != nil {
s.loader.Close()
}
return err
}
}
listen := func(network string, address string) (net.Listener, error) {
ln, err := net.Listen(network, address)
if err != nil {
return nil, err
}
if s.DumpPackets {
ln = &packetdumper.Listener{
Wrapped: ln,
Prefix: s.DumpPacketsPrefix,
}
}
return ln, nil
}
tlsListen := func(network string, laddr string, config *tls.Config) (net.Listener, error) {
ln, err := listen(network, laddr)
if err != nil {
return nil, err
}
if s.DumpPackets {
ln = &packetdumper.TLSListener{
Wrapped: ln,
TLSConfig: config,
}
} else {
ln = tls.NewListener(ln, config)
}
return ln, nil
}
if tlsConfig != nil {
var err error
s.ln, err = tlsListen(network, address, tlsConfig)
if err != nil {
if s.loader != nil {
s.loader.Close()
}
return err
}
} else {
var err error
s.ln, err = listen(network, address)
if err != nil {
if s.loader != nil {
s.loader.Close()
}
return err
}
}
if network == "unix" {
os.Chmod(address, 0o755) //nolint:errcheck
}
go s.inner.Serve(s.ln)
return nil
}
// Close closes all resources and waits for all routines to return.
func (s *Server) Close() {
s.ln.Close()
ctx, ctxCancel := context.WithTimeout(context.Background(), shutdownTimeout)
s.inner.Shutdown(ctx) //nolint:errcheck
ctxCancel()
s.tracker.close()
if s.loader != nil {
s.loader.Close()
}
}