AdGuardHome/internal/next/websvc/server.go

157 lines
3.8 KiB
Go

package websvc
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net"
"net/http"
"net/netip"
"net/url"
"sync"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil/urlutil"
)
// server contains an *http.Server as well as entities and data associated with
// it.
//
// TODO(a.garipov): Join with similar structs in other projects and move to
// golibs/netutil/httputil.
//
// TODO(a.garipov): Once the above standardization is complete, consider
// merging debugsvc and websvc into a single httpsvc.
type server struct {
// mu protects http, logger, tcpListener, and url.
mu *sync.Mutex
http *http.Server
logger *slog.Logger
tcpListener *net.TCPListener
url *url.URL
tlsConf *tls.Config
initialAddr netip.AddrPort
}
// loggerKeyServer is the key used by [server] to identify itself.
const loggerKeyServer = "server"
// newServer returns a *server that is ready to serve HTTP queries. The TCP
// listener is not started. handler must not be nil.
func newServer(
baseLogger *slog.Logger,
initialAddr netip.AddrPort,
tlsConf *tls.Config,
handler http.Handler,
timeout time.Duration,
) (s *server) {
u := &url.URL{
Scheme: urlutil.SchemeHTTP,
Host: initialAddr.String(),
}
if tlsConf != nil {
u.Scheme = urlutil.SchemeHTTPS
}
logger := baseLogger.With(loggerKeyServer, u)
return &server{
mu: &sync.Mutex{},
http: &http.Server{
Handler: handler,
ReadTimeout: timeout,
ReadHeaderTimeout: timeout,
WriteTimeout: timeout,
IdleTimeout: timeout,
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
},
logger: logger,
url: u,
tlsConf: tlsConf,
initialAddr: initialAddr,
}
}
// localAddr returns the local address of the server if the server has started
// listening; otherwise, it returns nil.
func (s *server) localAddr() (addr net.Addr) {
s.mu.Lock()
defer s.mu.Unlock()
if l := s.tcpListener; l != nil {
return l.Addr()
}
return nil
}
// serve starts s. baseLogger is used as a base logger for s. If s fails to
// serve with anything other than [http.ErrServerClosed], it causes an unhandled
// panic. It is intended to be used as a goroutine.
//
// TODO(a.garipov): Improve error handling.
func (s *server) serve(ctx context.Context, baseLogger *slog.Logger) {
l, err := net.ListenTCP("tcp", net.TCPAddrFromAddrPort(s.initialAddr))
if err != nil {
s.logger.ErrorContext(ctx, "listening tcp", slogutil.KeyError, err)
panic(fmt.Errorf("websvc: listening tcp: %w", err))
}
func() {
s.mu.Lock()
defer s.mu.Unlock()
s.tcpListener = l
// Reassign the address in case the port was zero.
s.url.Host = l.Addr().String()
s.logger = baseLogger.With(loggerKeyServer, s.url)
s.http.ErrorLog = slog.NewLogLogger(s.logger.Handler(), slog.LevelError)
}()
s.logger.InfoContext(ctx, "starting")
defer s.logger.InfoContext(ctx, "started")
err = s.http.Serve(l)
if err == nil || errors.Is(err, http.ErrServerClosed) {
return
}
s.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
panic(fmt.Errorf("websvc: serving: %w", err))
}
// shutdown shuts s down.
func (s *server) shutdown(ctx context.Context) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
var errs []error
err = s.http.Shutdown(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("shutting down server %s: %w", s.url, err))
}
// Close the listener separately, as it might not have been closed if the
// context has been canceled.
//
// NOTE: The listener could remain uninitialized if [net.ListenTCP] failed
// in [s.serve].
if l := s.tcpListener; l != nil {
err = l.Close()
if err != nil && !errors.Is(err, net.ErrClosed) {
errs = append(errs, fmt.Errorf("closing listener for server %s: %w", s.url, err))
}
}
return errors.Join(errs...)
}