157 lines
3.8 KiB
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...)
|
||
|
}
|