package webingress import ( "context" "crypto/tls" "errors" "net" "net/http" "strings" "sync" "time" ) type ListenerConfig struct { RuntimeConfig HTTPSAddr string TLSCertFile string TLSKeyFile string Binder FabricBinder } type ListenerStatus struct { SchemaVersion string `json:"schema_version"` Running bool `json:"running"` HTTPSRunning bool `json:"https_running"` HTTPSAddr string `json:"https_addr,omitempty"` Reason string `json:"reason,omitempty"` Errors []string `json:"errors,omitempty"` ObservedAt string `json:"observed_at"` } type Manager struct { mu sync.Mutex https *http.Server status ListenerStatus now func() time.Time } func NewManager() *Manager { return &Manager{now: time.Now} } func (m *Manager) Apply(ctx context.Context, cfg ListenerConfig) ListenerStatus { m.mu.Lock() defer m.mu.Unlock() _ = m.stopLocked(ctx) runtime := Runtime{Config: cfg.RuntimeConfig, Binder: cfg.Binder, Now: m.now} status := ListenerStatus{ SchemaVersion: "rap.web_ingress.listener_status.v1", Reason: "started", ObservedAt: m.observedAt(), } errorsOut := []string{} if strings.TrimSpace(cfg.HTTPSAddr) == "" { cfg.HTTPSAddr = ":443" } if cfg.TLSCertFile == "" || cfg.TLSKeyFile == "" { errorsOut = append(errorsOut, "https:tls_cert_file_and_key_file_required") } else if server, addr, err := startHTTPSServer(ctx, cfg.HTTPSAddr, cfg.TLSCertFile, cfg.TLSKeyFile, runtime.HTTPSHandler()); err == nil { m.https = server status.HTTPSRunning = true status.HTTPSAddr = addr } else { errorsOut = append(errorsOut, "https:"+err.Error()) } status.Running = status.HTTPSRunning if len(errorsOut) > 0 { status.Errors = errorsOut if status.Running { status.Reason = "partial" } else { status.Reason = "blocked" } } m.status = status return status } func (m *Manager) Stop(ctx context.Context) ListenerStatus { m.mu.Lock() defer m.mu.Unlock() _ = m.stopLocked(ctx) m.status = ListenerStatus{ SchemaVersion: "rap.web_ingress.listener_status.v1", Reason: "stopped", ObservedAt: m.observedAt(), } return m.status } func (m *Manager) Status() ListenerStatus { m.mu.Lock() defer m.mu.Unlock() if m.status.SchemaVersion == "" { return ListenerStatus{ SchemaVersion: "rap.web_ingress.listener_status.v1", Reason: "not_started", ObservedAt: m.observedAt(), } } return m.status } func (m *Manager) stopLocked(ctx context.Context) error { var out error if m.https != nil { out = errors.Join(out, m.https.Shutdown(ctx)) m.https = nil } return out } func (m *Manager) observedAt() string { now := time.Now().UTC() if m.now != nil { now = m.now().UTC() } return now.Format(time.RFC3339Nano) } func startHTTPSServer(ctx context.Context, addr, certFile, keyFile string, handler http.Handler) (*http.Server, string, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, "", err } listener, err := net.Listen("tcp", addr) if err != nil { return nil, "", err } server := &http.Server{ Handler: handler, ReadHeaderTimeout: 5 * time.Second, TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}}, } go func() { <-ctx.Done() _ = server.Shutdown(context.Background()) }() go func() { if err := server.ServeTLS(listener, "", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { _ = server.Close() } }() return server, listener.Addr().String(), nil }