Files
goclaw/internal/gateway/server.go
T
viettranx 843b550651 feat: runtime packages UI, pkg-helper, configurable shell deny groups (#244)
Runtime package management with security hardening:

- pkg-helper: root-privileged daemon for apk install/uninstall via Unix socket
- HTTP API: /v1/packages (list/install/uninstall/runtimes), admin role required for writes
- Shell deny groups: 15 configurable groups (per-agent overrides via context)
- Packages UI: Web page for managing system/pip/npm packages with confirmation dialogs
- Docker: privilege separation (root entrypoint → su-exec drop), init for zombie reaping
- Security: umask socket creation, persist file validation, deny pattern hardening
  (Node.js fetch/http, Python from/import, curl localhost, sensitive env vars)
- Auth: empty gateway token → admin role (dev/single-user mode)
2026-03-17 19:50:26 +07:00

672 lines
22 KiB
Go

package gateway
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/nextlevelbuilder/goclaw/internal/agent"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/config"
httpapi "github.com/nextlevelbuilder/goclaw/internal/http"
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
"github.com/nextlevelbuilder/goclaw/internal/permissions"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/internal/tools"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// Server is the main gateway server handling WebSocket and HTTP connections.
type Server struct {
cfg *config.Config
eventPub bus.EventPublisher
agents *agent.Router
sessions store.SessionStore
tools *tools.Registry
router *MethodRouter
policyEngine *permissions.PolicyEngine
pairingService store.PairingStore
agentsHandler *httpapi.AgentsHandler // agent CRUD API
skillsHandler *httpapi.SkillsHandler // skill management API
tracesHandler *httpapi.TracesHandler // LLM trace listing API
wakeHandler *httpapi.WakeHandler // external wake/trigger API
mcpHandler *httpapi.MCPHandler // MCP server management API
customToolsHandler *httpapi.CustomToolsHandler // custom tool CRUD API
channelInstancesHandler *httpapi.ChannelInstancesHandler // channel instance CRUD API
providersHandler *httpapi.ProvidersHandler // provider CRUD API
delegationsHandler *httpapi.DelegationsHandler // delegation history API
teamEventsHandler *httpapi.TeamEventsHandler // team event history API
builtinToolsHandler *httpapi.BuiltinToolsHandler // builtin tool management API
pendingMessagesHandler *httpapi.PendingMessagesHandler // pending messages API
secureCLIHandler *httpapi.SecureCLIHandler // secure CLI credential CRUD API
packagesHandler *httpapi.PackagesHandler // runtime package management API
memoryHandler *httpapi.MemoryHandler // memory management API
kgHandler *httpapi.KnowledgeGraphHandler // knowledge graph API
oauthHandler *httpapi.OAuthHandler // OAuth endpoints
filesHandler *httpapi.FilesHandler // workspace file serving
storageHandler *httpapi.StorageHandler // storage file management
mediaUploadHandler *httpapi.MediaUploadHandler // media upload endpoint
mediaServeHandler *httpapi.MediaServeHandler // media serve endpoint
activityHandler *httpapi.ActivityHandler // activity audit log API
usageHandler *httpapi.UsageHandler // usage analytics API
apiKeysHandler *httpapi.APIKeysHandler // API key management
apiKeyStore store.APIKeyStore // for API key auth lookup
docsHandler *httpapi.DocsHandler // OpenAPI spec + Swagger UI
agentStore store.AgentStore // for context injection in tools_invoke
msgBus *bus.MessageBus // for MCP bridge media delivery
upgrader websocket.Upgrader
rateLimiter *RateLimiter
clients map[string]*Client
mu sync.RWMutex
startedAt time.Time
version string
db interface{ PingContext(context.Context) error } // for health check DB ping
logTee *LogTee // optional; auto-unsubscribes clients on disconnect
httpServer *http.Server
mux *http.ServeMux
}
// NewServer creates a new gateway server.
func NewServer(cfg *config.Config, eventPub bus.EventPublisher, agents *agent.Router, sess store.SessionStore, toolsReg ...*tools.Registry) *Server {
s := &Server{
cfg: cfg,
eventPub: eventPub,
agents: agents,
sessions: sess,
clients: make(map[string]*Client),
startedAt: time.Now(),
}
s.upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: s.checkOrigin,
}
if len(toolsReg) > 0 && toolsReg[0] != nil {
s.tools = toolsReg[0]
}
// Initialize rate limiter.
// rate_limit_rpm > 0 → enabled at that RPM
// rate_limit_rpm == 0 → disabled (default, backward compat)
// rate_limit_rpm < 0 → disabled explicitly
s.rateLimiter = NewRateLimiter(cfg.Gateway.RateLimitRPM, 5)
s.router = NewMethodRouter(s)
return s
}
// RateLimiter returns the server's rate limiter for use by method handlers.
func (s *Server) RateLimiter() *RateLimiter { return s.rateLimiter }
// checkOrigin validates WebSocket connection origin against the allowed origins whitelist.
// If no origins are configured, all origins are allowed (backward compatibility / dev mode).
// Empty Origin header (non-browser clients like CLI/SDK) is always allowed.
func (s *Server) checkOrigin(r *http.Request) bool {
allowed := s.cfg.Gateway.AllowedOrigins
if len(allowed) == 0 {
return true // no config = allow all (backward compat)
}
origin := r.Header.Get("Origin")
if origin == "" {
return true // non-browser clients (CLI, SDK, channels)
}
for _, a := range allowed {
if origin == a || a == "*" {
return true
}
}
slog.Warn("security.cors_rejected", "origin", origin)
return false
}
// BuildMux creates and caches the HTTP mux with all routes registered.
// Call this before Start() if you need the mux for additional listeners (e.g. Tailscale).
func (s *Server) BuildMux() *http.ServeMux {
if s.mux != nil {
return s.mux
}
mux := http.NewServeMux()
// WebSocket endpoint
mux.HandleFunc("/ws", s.handleWebSocket)
// HTTP API endpoints
mux.HandleFunc("/health", s.handleHealth)
// OpenAI-compatible chat completions
isManaged := s.agentStore != nil
chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged)
if s.rateLimiter.Enabled() {
chatHandler.SetRateLimiter(s.rateLimiter.Allow)
}
mux.Handle("/v1/chat/completions", chatHandler)
// OpenResponses protocol
responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token)
mux.Handle("/v1/responses", responsesHandler)
// Direct tool invocation
if s.tools != nil {
toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore)
mux.Handle("/v1/tools/invoke", toolsHandler)
}
// Agent CRUD + shares API
if s.agentsHandler != nil {
s.agentsHandler.RegisterRoutes(mux)
}
// Skill management API
if s.skillsHandler != nil {
s.skillsHandler.RegisterRoutes(mux)
}
// LLM trace listing API
if s.tracesHandler != nil {
s.tracesHandler.RegisterRoutes(mux)
}
// External wake/trigger API
if s.wakeHandler != nil {
s.wakeHandler.RegisterRoutes(mux)
}
// MCP server management API
if s.mcpHandler != nil {
s.mcpHandler.RegisterRoutes(mux)
}
// Custom tool CRUD API
if s.customToolsHandler != nil {
s.customToolsHandler.RegisterRoutes(mux)
}
// Secure CLI credential CRUD API
if s.secureCLIHandler != nil {
s.secureCLIHandler.RegisterRoutes(mux)
}
// Channel instance CRUD API
if s.channelInstancesHandler != nil {
s.channelInstancesHandler.RegisterRoutes(mux)
}
// Provider & model CRUD API
if s.providersHandler != nil {
s.providersHandler.RegisterRoutes(mux)
}
// Delegation history API
if s.delegationsHandler != nil {
s.delegationsHandler.RegisterRoutes(mux)
}
// Team event history API
if s.teamEventsHandler != nil {
s.teamEventsHandler.RegisterRoutes(mux)
}
// Builtin tool management API
if s.builtinToolsHandler != nil {
s.builtinToolsHandler.RegisterRoutes(mux)
}
// Pending messages API
if s.pendingMessagesHandler != nil {
s.pendingMessagesHandler.RegisterRoutes(mux)
}
// Memory management API
if s.memoryHandler != nil {
s.memoryHandler.RegisterRoutes(mux)
}
// Knowledge graph API
if s.kgHandler != nil {
s.kgHandler.RegisterRoutes(mux)
}
// Workspace file serving (available in all modes)
if s.filesHandler != nil {
s.filesHandler.RegisterRoutes(mux)
}
// Storage file management (browse/delete workspace files)
if s.storageHandler != nil {
s.storageHandler.RegisterRoutes(mux)
}
// Media upload endpoint (available in all modes)
if s.mediaUploadHandler != nil {
s.mediaUploadHandler.RegisterRoutes(mux)
}
// Media serve endpoint (available in all modes)
if s.mediaServeHandler != nil {
s.mediaServeHandler.RegisterRoutes(mux)
}
if s.apiKeysHandler != nil {
s.apiKeysHandler.RegisterRoutes(mux)
}
if s.activityHandler != nil {
s.activityHandler.RegisterRoutes(mux)
}
if s.usageHandler != nil {
s.usageHandler.RegisterRoutes(mux)
}
if s.packagesHandler != nil {
s.packagesHandler.RegisterRoutes(mux)
}
// API documentation (OpenAPI spec + Swagger UI)
if s.docsHandler != nil {
s.docsHandler.RegisterRoutes(mux)
}
// OAuth endpoints (available in all modes)
if s.oauthHandler != nil {
s.oauthHandler.RegisterRoutes(mux)
}
// MCP bridge: expose GoClaw tools to Claude CLI via streamable-http.
// Only listens on localhost (CLI runs on the same machine).
// Protected by gateway token when configured.
// Agent context (X-Agent-ID, X-User-ID) is injected from request headers.
if s.tools != nil {
bridgeHandler := mcpbridge.NewBridgeServer(s.tools, "1.0.0", s.msgBus)
var handler http.Handler = bridgeContextMiddleware(s.cfg.Gateway.Token, bridgeHandler)
if s.cfg.Gateway.Token != "" {
handler = tokenAuthMiddleware(s.cfg.Gateway.Token, handler)
} else {
slog.Warn("security.mcp_bridge: no gateway token configured, MCP bridge tools are unauthenticated")
}
mux.Handle("/mcp/bridge", handler)
}
s.mux = mux
return mux
}
// bridgeContextMiddleware extracts X-Agent-ID and X-User-ID headers from the
// MCP bridge request and injects them into the context so bridge tools can
// access agent/user scope. When a gateway token is configured, the context
// headers must be accompanied by a valid X-Bridge-Sig HMAC to prevent forgery.
func bridgeContextMiddleware(gatewayToken string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
agentIDStr := r.Header.Get("X-Agent-ID")
userID := r.Header.Get("X-User-ID")
channel := r.Header.Get("X-Channel")
chatID := r.Header.Get("X-Chat-ID")
peerKind := r.Header.Get("X-Peer-Kind")
if agentIDStr != "" || userID != "" {
// Reject context headers when no gateway token — prevents unauthenticated impersonation.
if gatewayToken == "" {
slog.Warn("security.mcp_bridge: no gateway token, ignoring context headers",
"agent_id", agentIDStr, "user_id", userID)
next.ServeHTTP(w, r)
return
}
// Verify HMAC signature over all context fields.
sig := r.Header.Get("X-Bridge-Sig")
if !providers.VerifyBridgeContext(gatewayToken, agentIDStr, userID, channel, chatID, peerKind, sig) {
slog.Warn("security.mcp_bridge: invalid bridge context signature",
"agent_id", agentIDStr, "user_id", userID)
http.Error(w, `{"error":"invalid bridge context signature"}`, http.StatusForbidden)
return
}
if agentIDStr != "" {
if id, err := uuid.Parse(agentIDStr); err == nil {
ctx = store.WithAgentID(ctx, id)
}
}
if userID != "" {
ctx = store.WithUserID(ctx, userID)
}
}
// Inject channel routing context for tools like message, cron, etc.
if channel != "" {
ctx = tools.WithToolChannel(ctx, channel)
}
if chatID != "" {
ctx = tools.WithToolChatID(ctx, chatID)
}
if peerKind != "" {
ctx = tools.WithToolPeerKind(ctx, peerKind)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// tokenAuthMiddleware wraps an http.Handler with Bearer token authentication.
func tokenAuthMiddleware(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
provided := strings.TrimPrefix(auth, "Bearer ")
if !strings.HasPrefix(auth, "Bearer ") || subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Start begins listening for WebSocket and HTTP connections.
func (s *Server) Start(ctx context.Context) error {
mux := s.BuildMux()
addr := fmt.Sprintf("%s:%d", s.cfg.Gateway.Host, s.cfg.Gateway.Port)
s.httpServer = &http.Server{
Addr: addr,
Handler: mux,
}
slog.Info("gateway starting", "addr", addr)
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s.httpServer.Shutdown(shutdownCtx)
}()
if err := s.httpServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("gateway server: %w", err)
}
return nil
}
// handleWebSocket upgrades HTTP to WebSocket and manages the connection.
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
slog.Error("websocket upgrade failed", "error", err)
return
}
client := NewClient(conn, s, clientIP(r))
s.registerClient(client)
defer func() {
s.unregisterClient(client)
client.Close()
}()
client.Run(r.Context())
}
// handleHealth returns a simple health check response.
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok","protocol":%d}`, protocol.ProtocolVersion)
}
// clientIP extracts the real client IP from the request, checking proxy headers first.
func clientIP(r *http.Request) string {
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
if i := strings.IndexByte(fwd, ','); i > 0 {
return strings.TrimSpace(fwd[:i])
}
return strings.TrimSpace(fwd)
}
host, _, _ := net.SplitHostPort(r.RemoteAddr)
return host
}
// Router returns the method router for registering additional handlers.
func (s *Server) Router() *MethodRouter { return s.router }
// SetPolicyEngine sets the permission policy engine for RPC method authorization.
func (s *Server) SetPolicyEngine(pe *permissions.PolicyEngine) { s.policyEngine = pe }
// SetPairingService sets the pairing service for channel authentication.
func (s *Server) SetPairingService(ps store.PairingStore) { s.pairingService = ps }
// SetAgentsHandler sets the agent CRUD handler.
func (s *Server) SetAgentsHandler(h *httpapi.AgentsHandler) { s.agentsHandler = h }
// SetSkillsHandler sets the skill management handler.
func (s *Server) SetSkillsHandler(h *httpapi.SkillsHandler) { s.skillsHandler = h }
// SetTracesHandler sets the LLM trace listing handler.
func (s *Server) SetTracesHandler(h *httpapi.TracesHandler) { s.tracesHandler = h }
// SetWakeHandler sets the external wake/trigger handler.
func (s *Server) SetWakeHandler(h *httpapi.WakeHandler) { s.wakeHandler = h }
// SetMCPHandler sets the MCP server management handler.
func (s *Server) SetMCPHandler(h *httpapi.MCPHandler) { s.mcpHandler = h }
// SetCustomToolsHandler sets the custom tool CRUD handler.
func (s *Server) SetCustomToolsHandler(h *httpapi.CustomToolsHandler) { s.customToolsHandler = h }
// SetChannelInstancesHandler sets the channel instance CRUD handler.
func (s *Server) SetChannelInstancesHandler(h *httpapi.ChannelInstancesHandler) {
s.channelInstancesHandler = h
}
// SetProvidersHandler sets the provider CRUD handler.
func (s *Server) SetProvidersHandler(h *httpapi.ProvidersHandler) { s.providersHandler = h }
// SetDelegationsHandler sets the delegation history handler.
func (s *Server) SetDelegationsHandler(h *httpapi.DelegationsHandler) { s.delegationsHandler = h }
// SetTeamEventsHandler sets the team event history handler.
func (s *Server) SetTeamEventsHandler(h *httpapi.TeamEventsHandler) { s.teamEventsHandler = h }
// SetPendingMessagesHandler sets the pending messages handler.
func (s *Server) SetPendingMessagesHandler(h *httpapi.PendingMessagesHandler) {
s.pendingMessagesHandler = h
}
// SetBuiltinToolsHandler sets the builtin tool management handler.
func (s *Server) SetBuiltinToolsHandler(h *httpapi.BuiltinToolsHandler) {
s.builtinToolsHandler = h
}
// SetSecureCLIHandler sets the secure CLI credential CRUD handler.
func (s *Server) SetSecureCLIHandler(h *httpapi.SecureCLIHandler) { s.secureCLIHandler = h }
// SetPackagesHandler sets the runtime package management handler.
func (s *Server) SetPackagesHandler(h *httpapi.PackagesHandler) { s.packagesHandler = h }
// SetOAuthHandler sets the OAuth handler (available in all modes).
func (s *Server) SetOAuthHandler(h *httpapi.OAuthHandler) { s.oauthHandler = h }
// SetAPIKeysHandler sets the API key management handler.
func (s *Server) SetAPIKeysHandler(h *httpapi.APIKeysHandler) { s.apiKeysHandler = h }
// SetAPIKeyStore sets the API key store for token-based auth lookup.
func (s *Server) SetAPIKeyStore(st store.APIKeyStore) { s.apiKeyStore = st }
// SetFilesHandler sets the workspace file serving handler.
func (s *Server) SetFilesHandler(h *httpapi.FilesHandler) { s.filesHandler = h }
// SetStorageHandler sets the storage file management handler.
func (s *Server) SetStorageHandler(h *httpapi.StorageHandler) { s.storageHandler = h }
// SetMediaUploadHandler sets the media upload handler.
func (s *Server) SetMediaUploadHandler(h *httpapi.MediaUploadHandler) { s.mediaUploadHandler = h }
// SetMediaServeHandler sets the media serve handler.
func (s *Server) SetMediaServeHandler(h *httpapi.MediaServeHandler) { s.mediaServeHandler = h }
// SetMemoryHandler sets the memory management handler.
func (s *Server) SetMemoryHandler(h *httpapi.MemoryHandler) { s.memoryHandler = h }
// SetKnowledgeGraphHandler sets the knowledge graph handler.
func (s *Server) SetKnowledgeGraphHandler(h *httpapi.KnowledgeGraphHandler) { s.kgHandler = h }
// SetActivityHandler sets the activity audit log handler.
func (s *Server) SetActivityHandler(h *httpapi.ActivityHandler) { s.activityHandler = h }
// SetUsageHandler sets the usage analytics handler.
func (s *Server) SetUsageHandler(h *httpapi.UsageHandler) { s.usageHandler = h }
// SetDocsHandler sets the OpenAPI spec + Swagger UI handler.
func (s *Server) SetDocsHandler(h *httpapi.DocsHandler) { s.docsHandler = h }
// SetAgentStore sets the agent store for context injection in tools_invoke.
func (s *Server) SetAgentStore(as store.AgentStore) { s.agentStore = as }
// SetMessageBus sets the message bus for MCP bridge media delivery.
func (s *Server) SetMessageBus(mb *bus.MessageBus) { s.msgBus = mb }
// SetVersion sets the server version for health responses.
func (s *Server) SetVersion(v string) { s.version = v }
// SetDB sets the database connection for health check pings.
func (s *Server) SetDB(db interface{ PingContext(context.Context) error }) { s.db = db }
// StartedAt returns the server start time.
func (s *Server) StartedAt() time.Time { return s.startedAt }
// Version returns the server version string.
func (s *Server) Version() string { return s.version }
// ClientList returns a snapshot of all connected clients.
func (s *Server) ClientList() []*Client {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]*Client, 0, len(s.clients))
for _, c := range s.clients {
list = append(list, c)
}
return list
}
// BroadcastEvent sends an event to all connected clients.
func (s *Server) BroadcastEvent(event protocol.EventFrame) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, client := range s.clients {
client.SendEvent(event)
}
}
// DisconnectByPairing force-closes WebSocket connections authenticated via the
// given pairing senderID and channel. Called after revoking a paired device so
// that the revoked client cannot continue operating with its old role.
func (s *Server) DisconnectByPairing(senderID, channel string) {
s.mu.RLock()
var targets []*Client
for _, c := range s.clients {
if c.pairedSenderID == senderID && c.pairedChannel == channel {
targets = append(targets, c)
}
}
s.mu.RUnlock()
for _, c := range targets {
slog.Info("disconnecting revoked paired device", "client", c.id, "sender_id", senderID, "channel", channel)
c.conn.Close()
}
}
func (s *Server) registerClient(c *Client) {
s.mu.Lock()
defer s.mu.Unlock()
s.clients[c.id] = c
// Subscribe to bus events for this client (skip internal cache events)
s.eventPub.Subscribe(c.id, func(event bus.Event) {
if strings.HasPrefix(event.Name, "cache.") {
return // internal event, don't forward to WS clients
}
c.SendEvent(*protocol.NewEvent(event.Name, event.Payload))
})
slog.Info("client connected", "id", c.id)
}
func (s *Server) unregisterClient(c *Client) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.clients, c.id)
s.eventPub.Unsubscribe(c.id)
if s.logTee != nil {
s.logTee.Unsubscribe(c.id)
}
slog.Info("client disconnected", "id", c.id)
}
// SetLogTee attaches a LogTee so that disconnecting clients are auto-unsubscribed.
func (s *Server) SetLogTee(lt *LogTee) {
s.logTee = lt
}
// StartTestServer creates a listener on :0 (random port) and returns the
// actual address and a start function. Used for integration tests.
func StartTestServer(s *Server, ctx context.Context) (addr string, start func()) {
mux := http.NewServeMux()
mux.HandleFunc("/ws", s.handleWebSocket)
mux.HandleFunc("/health", s.handleHealth)
isManaged := s.agentStore != nil
chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged)
if s.rateLimiter.Enabled() {
chatHandler.SetRateLimiter(s.rateLimiter.Allow)
}
mux.Handle("/v1/chat/completions", chatHandler)
responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token)
mux.Handle("/v1/responses", responsesHandler)
if s.tools != nil {
toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore)
mux.Handle("/v1/tools/invoke", toolsHandler)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic("listen: " + err.Error())
}
s.httpServer = &http.Server{Handler: mux}
addr = ln.Addr().String()
start = func() {
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
s.httpServer.Shutdown(shutdownCtx)
}()
s.httpServer.Serve(ln)
}
return addr, start
}