package internalhttp import ( "context" "errors" "fmt" "net" "net/http" "sync" "time" "galaxy/authsession/internal/service/blockuser" "galaxy/authsession/internal/service/getsession" "galaxy/authsession/internal/service/listusersessions" "galaxy/authsession/internal/service/revokeallusersessions" "galaxy/authsession/internal/service/revokedevicesession" "galaxy/authsession/internal/telemetry" "go.uber.org/zap" ) const ( defaultAddr = ":8081" defaultReadHeaderTimeout = 2 * time.Second defaultReadTimeout = 10 * time.Second defaultIdleTimeout = time.Minute defaultRequestTimeout = 3 * time.Second ) // GetSessionUseCase describes the trusted internal get-session service // consumed by the HTTP transport layer. type GetSessionUseCase interface { // Execute loads one device session for trusted internal callers. Execute(ctx context.Context, input getsession.Input) (getsession.Result, error) } // ListUserSessionsUseCase describes the trusted internal list-user-sessions // service consumed by the HTTP transport layer. type ListUserSessionsUseCase interface { // Execute lists all sessions of one user for trusted internal callers. Execute(ctx context.Context, input listusersessions.Input) (listusersessions.Result, error) } // RevokeDeviceSessionUseCase describes the trusted internal single-session // revoke service consumed by the HTTP transport layer. type RevokeDeviceSessionUseCase interface { // Execute revokes one device session and returns the frozen // acknowledgement. Execute(ctx context.Context, input revokedevicesession.Input) (revokedevicesession.Result, error) } // RevokeAllUserSessionsUseCase describes the trusted internal bulk-revoke // service consumed by the HTTP transport layer. type RevokeAllUserSessionsUseCase interface { // Execute revokes all active sessions of one user and returns the frozen // acknowledgement. Execute(ctx context.Context, input revokeallusersessions.Input) (revokeallusersessions.Result, error) } // BlockUserUseCase describes the trusted internal block-user service consumed // by the HTTP transport layer. type BlockUserUseCase interface { // Execute applies a block state to one subject and returns the frozen // acknowledgement. Execute(ctx context.Context, input blockuser.Input) (blockuser.Result, error) } // Config describes the trusted internal HTTP listener owned by authsession. type Config struct { // Addr is the TCP listen address used by the trusted internal HTTP server. Addr string // ReadHeaderTimeout bounds how long the listener may spend reading request // headers before the server rejects the connection. ReadHeaderTimeout time.Duration // ReadTimeout bounds how long the listener may spend reading one trusted // internal request. ReadTimeout time.Duration // IdleTimeout bounds how long the listener keeps an idle keep-alive // connection open. IdleTimeout time.Duration // RequestTimeout bounds one application-layer internal use-case call. RequestTimeout time.Duration } // Validate reports whether cfg contains a usable internal HTTP listener // configuration. func (cfg Config) Validate() error { switch { case cfg.Addr == "": return errors.New("internal HTTP addr must not be empty") case cfg.ReadHeaderTimeout <= 0: return errors.New("internal HTTP read header timeout must be positive") case cfg.ReadTimeout <= 0: return errors.New("internal HTTP read timeout must be positive") case cfg.IdleTimeout <= 0: return errors.New("internal HTTP idle timeout must be positive") case cfg.RequestTimeout <= 0: return errors.New("internal HTTP request timeout must be positive") default: return nil } } // DefaultConfig returns the default trusted internal HTTP listener settings. func DefaultConfig() Config { return Config{ Addr: defaultAddr, ReadHeaderTimeout: defaultReadHeaderTimeout, ReadTimeout: defaultReadTimeout, IdleTimeout: defaultIdleTimeout, RequestTimeout: defaultRequestTimeout, } } // Dependencies describes the collaborators used by the trusted internal HTTP // transport layer. type Dependencies struct { // GetSession executes the trusted internal get-session use case. GetSession GetSessionUseCase // ListUserSessions executes the trusted internal list-user-sessions use // case. ListUserSessions ListUserSessionsUseCase // RevokeDeviceSession executes the trusted internal single-session revoke // use case. RevokeDeviceSession RevokeDeviceSessionUseCase // RevokeAllUserSessions executes the trusted internal bulk-revoke use case. RevokeAllUserSessions RevokeAllUserSessionsUseCase // BlockUser executes the trusted internal block-user use case. BlockUser BlockUserUseCase // Logger writes structured transport logs. When nil, a no-op logger is // used. Logger *zap.Logger // Telemetry records OpenTelemetry spans and low-cardinality HTTP metrics. // When nil, the transport still serves requests with no-op providers. Telemetry *telemetry.Runtime } // Server owns the trusted internal HTTP listener exposed by authsession. type Server struct { cfg Config handler http.Handler logger *zap.Logger stateMu sync.RWMutex server *http.Server listener net.Listener } // NewServer constructs one trusted internal HTTP server for cfg and deps. func NewServer(cfg Config, deps Dependencies) (*Server, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("new internal HTTP server: %w", err) } handler, err := newHandlerWithConfig(cfg, deps) if err != nil { return nil, fmt.Errorf("new internal HTTP server: %w", err) } logger := deps.Logger if logger == nil { logger = zap.NewNop() } logger = logger.Named("internal_http") return &Server{ cfg: cfg, handler: handler, logger: logger, }, nil } // Run binds the configured listener and serves the trusted internal HTTP // surface until Shutdown closes the server. func (s *Server) Run(ctx context.Context) error { if ctx == nil { return errors.New("run internal HTTP server: nil context") } if err := ctx.Err(); err != nil { return err } listener, err := net.Listen("tcp", s.cfg.Addr) if err != nil { return fmt.Errorf("run internal HTTP server: listen on %q: %w", s.cfg.Addr, err) } server := &http.Server{ Handler: s.handler, ReadHeaderTimeout: s.cfg.ReadHeaderTimeout, ReadTimeout: s.cfg.ReadTimeout, IdleTimeout: s.cfg.IdleTimeout, } s.stateMu.Lock() s.server = server s.listener = listener s.stateMu.Unlock() s.logger.Info("internal HTTP server started", zap.String("addr", listener.Addr().String())) defer func() { s.stateMu.Lock() s.server = nil s.listener = nil s.stateMu.Unlock() }() err = server.Serve(listener) switch { case err == nil: return nil case errors.Is(err, http.ErrServerClosed): s.logger.Info("internal HTTP server stopped") return nil default: return fmt.Errorf("run internal HTTP server: serve on %q: %w", s.cfg.Addr, err) } } // Shutdown gracefully stops the trusted internal HTTP server within ctx. func (s *Server) Shutdown(ctx context.Context) error { if ctx == nil { return errors.New("shutdown internal HTTP server: nil context") } s.stateMu.RLock() server := s.server s.stateMu.RUnlock() if server == nil { return nil } if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("shutdown internal HTTP server: %w", err) } return nil } func normalizeDependencies(deps Dependencies) (Dependencies, error) { switch { case deps.GetSession == nil: return Dependencies{}, errors.New("get session use case must not be nil") case deps.ListUserSessions == nil: return Dependencies{}, errors.New("list user sessions use case must not be nil") case deps.RevokeDeviceSession == nil: return Dependencies{}, errors.New("revoke device session use case must not be nil") case deps.RevokeAllUserSessions == nil: return Dependencies{}, errors.New("revoke all user sessions use case must not be nil") case deps.BlockUser == nil: return Dependencies{}, errors.New("block user use case must not be nil") case deps.Logger == nil: deps.Logger = zap.NewNop() } deps.Logger = deps.Logger.Named("internal_http") return deps, nil }