Files

435 lines
11 KiB
Go
Raw Permalink Normal View History

2026-08-19 20:50:03 +01:00
package httpserver
2022-02-10 18:19:34 +01:00
import (
"context"
"crypto/tls"
2025-09-10 10:19:04 +01:00
"errors"
2022-02-10 18:19:34 +01:00
"net"
"net/http"
2025-09-10 10:19:04 +01:00
"net/netip"
"net/url"
2025-09-10 10:19:04 +01:00
"strconv"
"sync"
"sync/atomic"
"syscall"
2026-04-23 13:34:32 +03:00
"time"
2022-02-10 18:19:34 +01:00
"go.uber.org/zap"
2024-09-27 06:37:32 +08:00
2026-08-19 20:49:00 +01:00
"github.com/status-im/status-go/internal/panics"
2022-02-10 18:19:34 +01:00
)
2026-04-23 13:34:32 +03:00
// backgroundShutdownTimeout bounds how long Server.ToBackground waits for
// active HTTP handlers to drain before force-closing remaining connections.
const backgroundShutdownTimeout = 300 * time.Millisecond
// teardownShutdownTimeout bounds the same drain at teardown. It is generous
// enough for normal in-flight requests to finish, but a handler that never
// returns must not be able to wedge Stop — and with it the logout path that
// holds the backend mutex.
const teardownShutdownTimeout = 3 * time.Second
// tcpListenConfig sets SO_REUSEADDR so that after the previous listener is fully
// closed, the kernel can allow binding the same cached ephemeral port again (e.g.
// TIME_WAIT) without a long delay. It does not help if Stop returns before that
// socket is closed — Stop waits on serveWg for that ordering.
var tcpListenConfig = net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
var optErr error
if err := c.Control(func(fd uintptr) {
optErr = setReuseAddr(fd)
}); err != nil {
return err
}
return optErr
},
}
2025-09-10 10:19:04 +01:00
type Config struct {
Cert *tls.Certificate
AddrPort netip.AddrPort
// AdvertizeHost and AdvertizePort define the a different host/port to be advertized in media URLs than the one server listen on.
// Can be used when status-go is running behind a NAT/PAT. If empty/zero, the actual listening address is used.
AdvertizeHost string
AdvertizePort int
}
type Server struct {
mu sync.Mutex
2025-09-10 10:19:04 +01:00
listener net.Listener
server *http.Server
logger *zap.Logger
handlers HandlerPatternMap
config *Config
// address is the host and port the server is listening on
address *net.TCPAddr
// running is true if the server was started and Serve is active; read via IsRunning().
running atomic.Bool
// cachedPort stores the port from the first successful bind when AddrPort used port 0 (ephemeral).
// Reused on pause/resume so URLs remain valid across ToBackground/ToForeground cycles.
cachedPort int
// serveWg waits for the serve goroutine to finish all defers after http.Server.Serve returns,
// so Stop does not return while the previous listener may still be tearing down (avoids EADDRINUSE
// when rebinding the same cached ephemeral port).
serveWg sync.WaitGroup
// serveDone is closed when the current serve goroutine has fully exited.
// The dead-listener rebind path waits on the instance it captured — never
// on serveWg, which a concurrent Start could grow while the mutex is
// released, making the wait block on an unrelated healthy server.
serveDone chan struct{}
*timeoutManager
}
func NewServer(logger *zap.Logger, config *Config) *Server {
return &Server{
logger: logger,
2025-09-10 10:19:04 +01:00
config: config,
timeoutManager: newTimeoutManager(),
2022-10-12 11:00:14 +01:00
}
}
2022-02-10 18:19:34 +01:00
2025-09-10 10:19:04 +01:00
func (s *Server) GetAddrPort() string {
host := ""
if s.address != nil {
host = s.address.IP.String()
} else if s.config != nil {
host = s.config.AddrPort.Addr().String()
2025-09-10 10:19:04 +01:00
}
if s.config != nil && s.config.Cert != nil && len(s.config.Cert.Leaf.DNSNames) > 0 {
host = s.config.Cert.Leaf.DNSNames[0]
}
if s.config != nil && s.config.AdvertizeHost != "" {
host = s.config.AdvertizeHost
}
port := s.GetPort()
if host == "" || port == 0 {
return ""
}
return net.JoinHostPort(host, strconv.Itoa(port))
}
2025-09-10 10:19:04 +01:00
func (s *Server) GetPort() int {
if s.config != nil && s.config.AdvertizePort != 0 {
return s.config.AdvertizePort
}
if s.address != nil {
return s.address.Port
}
if s.cachedPort != 0 {
return s.cachedPort
}
if s.config != nil && s.config.AddrPort.Port() != 0 {
return int(s.config.AddrPort.Port())
}
return 0
}
func (s *Server) GetListeningAddrPort() string {
if s.address == nil {
return ""
}
return s.address.String()
}
func (s *Server) GetCert() *tls.Certificate {
2025-09-10 10:19:04 +01:00
return s.config.Cert
}
func (s *Server) GetLogger() *zap.Logger {
return s.logger
}
// getBindAddrPort returns the address to bind to. When config used port 0 (ephemeral)
// and we have a cached port from a previous run, reuse it so URLs stay stable across pause/resume.
func (s *Server) getBindAddrPort() netip.AddrPort {
if s.cachedPort != 0 && s.config.AddrPort.Port() == 0 {
return netip.AddrPortFrom(s.config.AddrPort.Addr(), uint16(s.cachedPort))
}
return s.config.AddrPort
}
func (s *Server) createListener() (net.Listener, error) {
addr := s.getBindAddrPort()
addrStr := addr.String()
ln, err := tcpListenConfig.Listen(context.Background(), "tcp", addrStr)
if err != nil {
return nil, err
}
if s.config.Cert == nil {
return ln, nil
}
serverName := addr.Addr().String()
2025-09-10 10:19:04 +01:00
if len(s.config.Cert.Leaf.DNSNames) > 0 {
serverName = s.config.Cert.Leaf.DNSNames[0]
}
cfg := &tls.Config{
2025-09-10 10:19:04 +01:00
Certificates: []tls.Certificate{*s.config.Cert},
ServerName: serverName,
MinVersion: tls.VersionTLS12,
}
return tls.NewListener(ln, cfg), nil
}
2025-09-10 10:19:04 +01:00
func (s *Server) listen() error {
s.address = nil
2022-02-10 18:19:34 +01:00
2025-09-10 10:19:04 +01:00
var err error
s.listener, err = s.createListener()
2022-02-10 18:19:34 +01:00
if err != nil {
2025-09-10 10:19:04 +01:00
s.logger.Error("failed to start server", zap.Error(err))
return err
2022-02-10 18:19:34 +01:00
}
2025-09-10 10:19:04 +01:00
s.address = s.listener.Addr().(*net.TCPAddr)
if s.config.AddrPort.Port() == 0 {
s.cachedPort = s.address.Port
}
s.StartTimeout(func() {
err := s.Stop()
if err != nil {
2023-03-21 12:36:34 +00:00
s.logger.Error("server termination fail", zap.Error(err))
}
})
2025-09-10 10:19:04 +01:00
return nil
}
func (s *Server) serve(currentServer *http.Server, currentListener net.Listener) {
2026-08-19 20:49:00 +01:00
defer panics.LogOnPanic()
2025-09-10 10:19:04 +01:00
defer func() {
s.mu.Lock()
defer s.mu.Unlock()
// If a newer Start() already replaced server/listener, do not clobber
// the latest running instance state.
if s.server == currentServer && s.listener == currentListener {
s.running.Store(false)
s.address = nil
}
2025-09-10 10:19:04 +01:00
}()
err := currentServer.Serve(currentListener)
2025-09-10 10:19:04 +01:00
if errors.Is(err, http.ErrServerClosed) {
2022-02-10 18:19:34 +01:00
return
}
2025-09-10 10:19:04 +01:00
s.logger.Error("server failed unexpectedly, restarting", zap.Error(err))
return
2022-02-10 18:19:34 +01:00
}
func (s *Server) resetServer() {
s.StopTimeout()
s.server = new(http.Server)
2025-09-10 10:19:04 +01:00
s.address = nil
}
func (s *Server) applyHandlers() {
if s.server == nil {
s.server = new(http.Server)
}
mux := http.NewServeMux()
for p, h := range s.handlers {
mux.HandleFunc(p, h)
}
s.server.Handler = mux
}
// listenerAlive verifies the bound listener actually accepts connections.
// On iOS a process suspension can kill the listening socket without the accept
// loop ever returning an error, leaving `running` true while every client
// connect is refused (observed when the app is suspended on the login screen,
// before the pausable-services bridge has anything to drive).
//
// A raw TCP dial is sufficient and safe with a TLS listener: crypto/tls only
// handshakes on the first read/write of an accepted conn, and net/http runs
// that in a per-connection goroutine — an immediate close surfaces (at most) a
// per-conn handshake error, never an Accept error, so Serve keeps running.
func (s *Server) listenerAlive() bool {
addr := s.GetListeningAddrPort()
if addr == "" {
return false
}
conn, err := net.DialTimeout("tcp", addr, time.Second)
if err != nil {
return false
}
_ = conn.Close()
return true
}
2022-02-10 18:19:34 +01:00
func (s *Server) Start() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.running.Load() {
if s.listenerAlive() {
return nil
}
s.logger.Warn("server marked running but listener is dead; rebinding")
currentServer := s.server
done := s.serveDone
s.running.Store(false)
_ = currentServer.Close()
// The serve goroutine's deferred cleanup takes s.mu — release it while
// waiting, and wait only on the captured instance, never on serveWg
// (a concurrent Start could grow it with a healthy new server).
s.mu.Unlock()
if done != nil {
<-done
}
s.mu.Lock()
// Re-validate after the unlocked window: a concurrent Start may have
// already rebound — in that case this call's work is done.
if s.running.Load() {
return nil
}
2025-09-10 10:19:04 +01:00
}
// Once Shutdown has been called on a server, it may not be reused;
s.resetServer()
s.applyHandlers()
2025-09-10 10:19:04 +01:00
err := s.listen()
if err != nil {
return err
}
// Mark running synchronously to avoid pause/play races where ToBackground
// can run before serve() goroutine has a chance to set the state.
s.running.Store(true)
done := make(chan struct{})
s.serveDone = done
s.serveWg.Add(1)
go func() {
2026-08-19 20:49:00 +01:00
defer panics.LogOnPanic()
defer close(done)
defer s.serveWg.Done()
s.serve(s.server, s.listener)
}()
2022-02-10 18:19:34 +01:00
return nil
}
func (s *Server) Stop() error {
ctx, cancel := context.WithTimeout(context.Background(), teardownShutdownTimeout)
defer cancel()
err := s.stopWith(ctx)
if shouldForceCloseOnShutdownError(err) {
s.logger.Warn("server graceful shutdown did not complete in time; forced close",
zap.Error(err), zap.Duration("timeout", teardownShutdownTimeout))
return nil
}
return err
2026-04-23 13:34:32 +03:00
}
// stopWith performs the graceful shutdown sequence bounded by ctx. If ctx
// expires or is canceled before active connections drain, the server is
// force-closed via Close so the caller isn't blocked by a slow or stuck
// handler. Callers always pass a deadline: a generous one at teardown, a short
// one for lifecycle events like pause/ToBackground.
2026-04-23 13:34:32 +03:00
func (s *Server) stopWith(ctx context.Context) error {
s.mu.Lock()
s.StopTimeout()
if !s.running.Load() || s.server == nil {
s.mu.Unlock()
return nil
2022-02-10 18:19:34 +01:00
}
// Capture the current instance and release the lock before Shutdown.
// Shutdown waits for Serve() to return, and Serve() may update state in
// its defer path under the same mutex.
currentServer := s.server
s.running.Store(false)
s.mu.Unlock()
2026-04-23 13:34:32 +03:00
err := currentServer.Shutdown(ctx)
if shouldForceCloseOnShutdownError(err) {
_ = currentServer.Close()
}
s.serveWg.Wait()
return err
2022-02-10 18:19:34 +01:00
}
2026-04-23 13:34:32 +03:00
func shouldForceCloseOnShutdownError(err error) bool {
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)
}
func (s *Server) IsRunning() bool {
return s.running.Load()
}
2022-02-10 18:19:34 +01:00
func (s *Server) ToForeground() {
if err := s.Start(); err != nil {
s.logger.Error("server start failed during foreground transition", zap.Error(err))
2022-02-10 18:19:34 +01:00
}
}
func (s *Server) ToBackground() {
2026-04-23 13:34:32 +03:00
ctx, cancel := context.WithTimeout(context.Background(), backgroundShutdownTimeout)
defer cancel()
if err := s.stopWith(ctx); err != nil {
if shouldForceCloseOnShutdownError(err) {
s.logger.Warn("server graceful shutdown did not complete in time; forced close",
zap.Error(err), zap.Duration("timeout", backgroundShutdownTimeout))
return
}
s.logger.Error("server shutdown failed during background transition", zap.Error(err))
2022-02-10 18:19:34 +01:00
}
}
func (s *Server) SetHandlers(handlers HandlerPatternMap) {
s.handlers = handlers
}
2022-08-06 14:26:16 +01:00
func (s *Server) AddHandlers(handlers HandlerPatternMap) {
if s.handlers == nil {
s.handlers = make(HandlerPatternMap)
}
for name := range handlers {
s.handlers[name] = handlers[name]
}
}
func (s *Server) MakeBaseURL() *url.URL {
hostPort := s.GetAddrPort()
if hostPort == "" {
2025-09-10 10:19:04 +01:00
return &url.URL{}
}
scheme := "http"
if s.config != nil && s.config.Cert != nil {
scheme = "https"
}
return &url.URL{
2025-09-10 10:19:04 +01:00
Scheme: scheme,
Host: hostPort,
}
}