Files
T

112 lines
2.5 KiB
Go

package xmpp
import (
"context"
"fmt"
"log/slog"
"net"
"time"
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
"git.i3omb.com/gronod/ha-n95-local-control/internal/session"
)
// Clock abstracts time for the ping loop so tests can advance the clock without
// sleeping.
type Clock interface {
Now() time.Time
After(d time.Duration) <-chan time.Time
}
type realClock struct{}
func (realClock) Now() time.Time { return time.Now() }
func (realClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
// Server holds the runtime dependencies for the XMPP listener.
type Server struct {
cfg config.Config
registry *session.Registry
bus *session.Bus
clock Clock
diag session.DiagnosticSink
ln net.Listener
}
// NewServer creates a server. If clock is nil, real time is used.
func NewServer(cfg config.Config, registry *session.Registry, bus *session.Bus, clock Clock, diag session.DiagnosticSink) *Server {
if clock == nil {
clock = realClock{}
}
return &Server{
cfg: cfg,
registry: registry,
bus: bus,
clock: clock,
diag: diag,
}
}
// Serve starts the plaintext XMPP listener and blocks until ctx is cancelled.
// It returns nil on graceful shutdown and the listen error on failure.
func (s *Server) Serve(ctx context.Context) error {
addr := net.JoinHostPort(s.cfg.BindAddress, fmt.Sprintf("%d", s.cfg.PortXMPP))
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("%s: %w", addr, err)
}
s.ln = ln
errCh := make(chan error, 1)
go func() {
errCh <- s.acceptLoop(ctx)
}()
select {
case <-ctx.Done():
_ = ln.Close()
return nil
case err := <-errCh:
return err
}
}
func (s *Server) acceptLoop(ctx context.Context) error {
for {
conn, err := s.ln.Accept()
if err != nil {
if ctx.Err() != nil {
return nil
}
return err
}
c := &Conn{
netConn: conn,
cfg: s.cfg,
registry: s.registry,
bus: s.bus,
clock: s.clock,
diag: s.diag,
}
go c.serve()
}
}
// Shutdown writes </stream:stream> best-effort on every current generation and
// closes the underlying sockets. It is safe to call on a nil Server.
func (s *Server) Shutdown(ctx context.Context) error {
if s == nil || s.registry == nil {
return nil
}
s.registry.CloseAll()
if s.ln != nil {
_ = s.ln.Close()
}
return nil
}
// logStanza logs a redacted copy of a stanza at debug level.
func logStanza(dir string, b []byte) {
slog.Debug("xmpp", "dir", dir, "stanza", string(Redact(b)))
}