AdGuardHome/internal/next/websvc/websvc.go
2024-11-08 19:21:44 +03:00

336 lines
8.5 KiB
Go

// Package websvc contains the AdGuard Home HTTP API service.
//
// NOTE: Packages other than cmd must not import this package, as it imports
// most other packages.
//
// TODO(a.garipov): Add tests.
package websvc
import (
"context"
"crypto/tls"
"fmt"
"io"
"io/fs"
"log/slog"
"net"
"net/http"
"net/netip"
"net/url"
"runtime"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil/httputil"
"github.com/AdguardTeam/golibs/netutil/urlutil"
)
// ConfigManager is the configuration manager interface.
type ConfigManager interface {
DNS() (svc agh.ServiceWithConfig[*dnssvc.Config])
Web() (svc agh.ServiceWithConfig[*Config])
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
UpdateWeb(ctx context.Context, c *Config) (err error)
}
// Service is the AdGuard Home web service. A nil *Service is a valid
// [agh.Service] that does nothing.
type Service struct {
logger *slog.Logger
confMgr ConfigManager
frontend fs.FS
tls *tls.Config
pprof *server
start time.Time
overrideAddr netip.AddrPort
servers []*server
timeout time.Duration
pprofPort uint16
forceHTTPS bool
}
// server is a wrapper around http.Server with additional information.
type server struct {
logURL *url.URL
http.Server
}
// New returns a new properly initialized *Service. If c is nil, svc is a nil
// *Service that does nothing. The fields of c must not be modified after
// calling New.
//
// TODO(a.garipov): Get rid of this special handling of nil or explain it
// better.
func New(c *Config) (svc *Service, err error) {
if c == nil {
return nil, nil
}
svc = &Service{
logger: c.Logger,
confMgr: c.ConfigManager,
frontend: c.Frontend,
tls: c.TLS,
start: c.Start,
overrideAddr: c.OverrideAddress,
timeout: c.Timeout,
forceHTTPS: c.ForceHTTPS,
}
mux := newMux(svc)
if svc.overrideAddr != (netip.AddrPort{}) {
svc.servers = []*server{newServer(svc.logger, svc.overrideAddr, nil, mux, c.Timeout)}
} else {
for _, a := range c.Addresses {
svc.servers = append(svc.servers, newServer(svc.logger, a, nil, mux, c.Timeout))
}
for _, a := range c.SecureAddresses {
svc.servers = append(svc.servers, newServer(svc.logger, a, c.TLS, mux, c.Timeout))
}
}
svc.setupPprof(c.Pprof)
return svc, nil
}
// setupPprof sets the pprof properties of svc.
func (svc *Service) setupPprof(c *PprofConfig) {
if !c.Enabled {
// Set to zero explicitly in case pprof used to be enabled before a
// reconfiguration took place.
runtime.SetBlockProfileRate(0)
runtime.SetMutexProfileFraction(0)
return
}
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
pprofMux := http.NewServeMux()
httputil.RoutePprof(pprofMux)
svc.pprofPort = c.Port
addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)
// TODO(a.garipov): Consider making pprof timeout configurable.
svc.pprof = newServer(svc.logger, addr, nil, pprofMux, 10*time.Minute)
}
// newServer returns a new *server with the given parameters.
func newServer(
logger *slog.Logger,
addr netip.AddrPort,
tlsConf *tls.Config,
h http.Handler,
timeout time.Duration,
) (srv *server) {
addrStr := addr.String()
logURL := &url.URL{
Scheme: urlutil.SchemeHTTP,
Host: addrStr,
}
if tlsConf != nil {
logURL.Scheme = urlutil.SchemeHTTPS
}
l := logger.With("addr", logURL)
return &server{
logURL: logURL,
Server: http.Server{
Addr: addrStr,
Handler: h,
TLSConfig: tlsConf,
ReadTimeout: timeout,
WriteTimeout: timeout,
IdleTimeout: timeout,
ReadHeaderTimeout: timeout,
ErrorLog: slog.NewLogLogger(l.Handler(), slog.LevelError),
},
}
}
// newMux returns a new HTTP request multiplexer for the AdGuard Home web
// service.
func newMux(svc *Service) (mux *http.ServeMux) {
mux = http.NewServeMux()
routes := []struct {
handler http.HandlerFunc
pattern string
isJSON bool
}{{
handler: svc.handleGetHealthCheck,
pattern: routePatternHealthCheck,
isJSON: false,
}, {
handler: http.FileServer(http.FS(svc.frontend)).ServeHTTP,
pattern: routePatternFrontend,
isJSON: false,
}, {
handler: svc.handleGetSettingsAll,
pattern: routePatternGetV1SettingsAll,
isJSON: true,
}, {
handler: svc.handlePatchSettingsDNS,
pattern: routePatternPatchV1SettingsDNS,
isJSON: true,
}, {
handler: svc.handlePatchSettingsHTTP,
pattern: routePatternPatchV1SettingsHTTP,
isJSON: true,
}, {
handler: svc.handleGetV1SystemInfo,
pattern: routePatternGetV1SystemInfo,
isJSON: true,
}}
logMw := httputil.NewLogMiddleware(svc.logger, slog.LevelDebug)
for _, r := range routes {
var hdlr http.Handler
if r.isJSON {
hdlr = jsonMw(r.handler)
} else {
hdlr = r.handler
}
mux.Handle(r.pattern, logMw.Wrap(hdlr))
}
return mux
}
// addrs returns all addresses on which this server serves the HTTP API. addrs
// must not be called simultaneously with Start. If svc was initialized with
// ":0" addresses, addrs will not return the actual bound ports until Start is
// finished.
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
if svc.overrideAddr != (netip.AddrPort{}) {
return []netip.AddrPort{svc.overrideAddr}, nil
}
for _, srv := range svc.servers {
// Use MustParseAddrPort, since no errors should technically happen
// here, because all servers must have a valid address.
addrPort := netip.MustParseAddrPort(srv.Addr)
// [srv.Serve] will set TLSConfig to an almost empty value, so, instead
// of relying only on the nilness of TLSConfig, check the length of the
// certificates field as well.
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
addrs = append(addrs, addrPort)
} else {
secureAddrs = append(secureAddrs, addrPort)
}
}
return addrs, secureAddrs
}
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "OK")
}
// type check
var _ agh.ServiceWithConfig[*Config] = (*Service)(nil)
// Start implements the [agh.Service] interface for *Service. svc may be nil.
// After Start exits, all HTTP servers have tried to start, possibly failing and
// writing error messages to the log.
//
// TODO(a.garipov): Use the context.
func (svc *Service) Start(ctx context.Context) (err error) {
if svc == nil {
return nil
}
pprofEnabled := svc.pprof != nil
srvNum := len(svc.servers) + mathutil.BoolToNumber[int](pprofEnabled)
wg := &sync.WaitGroup{}
wg.Add(srvNum)
for _, srv := range svc.servers {
go serve(ctx, svc.logger, srv, wg)
}
if pprofEnabled {
go serve(ctx, svc.logger, svc.pprof, wg)
}
wg.Wait()
return nil
}
// serve starts and runs srv and writes all errors into its log.
func serve(ctx context.Context, logger *slog.Logger, srv *server, wg *sync.WaitGroup) {
defer slogutil.RecoverAndLog(ctx, logger)
var l net.Listener
var err error
addr := srv.Addr
if srv.TLSConfig == nil {
l, err = net.Listen("tcp", addr)
} else {
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
}
if err != nil {
logger.WarnContext(ctx, "binding", "tcp_addr", addr, slogutil.KeyError, err)
}
// Update the server's address in case the address had the port zero, which
// would mean that a random available port was automatically chosen.
srv.Addr = l.Addr().String()
srv.logURL.Host = srv.Addr
logger = logger.With("addr", srv.logURL)
logger.InfoContext(ctx, "starting")
l = &waitListener{
Listener: l,
firstAcceptWG: wg,
}
err = srv.Serve(l)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.WarnContext(ctx, "starting server", "tcp_addr", addr, slogutil.KeyError, err)
}
}
// Shutdown implements the [agh.Service] interface for *Service. svc may be
// nil.
func (svc *Service) Shutdown(ctx context.Context) (err error) {
if svc == nil {
return nil
}
defer func() { err = errors.Annotate(err, "shutting down: %w") }()
var errs []error
for _, srv := range svc.servers {
shutdownErr := srv.Shutdown(ctx)
if shutdownErr != nil {
errs = append(errs, fmt.Errorf("srv %s: %w", srv.Addr, shutdownErr))
}
}
if svc.pprof != nil {
shutdownErr := svc.pprof.Shutdown(ctx)
if shutdownErr != nil {
errs = append(errs, fmt.Errorf("pprof srv %s: %w", svc.pprof.Addr, shutdownErr))
}
}
return errors.Join(errs...)
}