1package app
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8 "os"
9 "sync"
10 "time"
11
12 "github.com/bnema/zerowrap"
13
14 "github.com/bnema/gordon/internal/adapters/in/http/middleware"
15 "github.com/bnema/gordon/internal/adapters/localadmin"
16)
17
18// localAdminServer owns the daemon's owner-only admin Unix socket: one HTTP
19// listener, one local-only authority handler, and the socket file it created.
20type localAdminServer struct {
21 srv *http.Server
22 path string
23 info os.FileInfo
24 log zerowrap.Logger
25 once sync.Once
26}
27
28// startLocalAdminServer binds the owner-only admin socket, composes the
29// local-only authority over the admin handler, and serves it in the
30// background. Any startup error aborts daemon startup; a later serve error is
31// reported on errChan so the daemon fails rather than silently losing local
32// administration.
33func startLocalAdminServer(svc *services, errChan chan<- error, log zerowrap.Logger) (*localAdminServer, error) {
34 if svc == nil || svc.adminHandler == nil {
35 return nil, nil
36 }
37
38 dir, err := localadmin.EnsureRuntimeDir()
39 if err != nil {
40 return nil, fmt.Errorf("local admin socket directory: %w", err)
41 }
42 ln, info, err := localadmin.Listen(dir)
43 if err != nil {
44 return nil, fmt.Errorf("local admin socket: %w", err)
45 }
46
47 handler := middleware.Chain(
48 middleware.PanicRecovery(log),
49 middleware.SecurityHeaders,
50 )(svc.adminHandler.LocalAuthority())
51
52 // No WriteTimeout: app log streaming over the socket stays open for as long
53 // as the client follows it.
54 srv := &http.Server{
55 Handler: handler,
56 ReadHeaderTimeout: 10 * time.Second,
57 IdleTimeout: 120 * time.Second,
58 MaxHeaderBytes: 1 << 20,
59 }
60
61 s := &localAdminServer{
62 srv: srv,
63 path: localadmin.SocketPath(dir),
64 info: info,
65 log: log,
66 }
67
68 go func() {
69 if serveErr := srv.Serve(ln); serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
70 wrapped := fmt.Errorf("local admin socket server: %w", serveErr)
71 select {
72 case errChan <- wrapped:
73 default:
74 log.Error().Err(serveErr).Msg("local admin socket server error")
75 }
76 }
77 }()
78
79 log.Info().Str("path", s.path).Msg("local admin socket listening")
80 return s, nil
81}
82
83// Close drains and stops the socket server, then removes the socket file only
84// when it is still the file this listener bound. It is safe to call twice.
85func (s *localAdminServer) Close() {
86 if s == nil {
87 return
88 }
89 s.once.Do(func() {
90 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
91 defer cancel()
92 if err := s.srv.Shutdown(ctx); err != nil {
93 s.log.Warn().Err(err).Msg("local admin socket shutdown error")
94 if closeErr := s.srv.Close(); closeErr != nil {
95 s.log.Warn().Err(closeErr).Msg("failed to force-close local admin socket")
96 }
97 }
98 if err := localadmin.RemoveOwnedSocket(s.path, s.info); err != nil {
99 s.log.Warn().Err(err).Str("path", s.path).Msg("failed to remove local admin socket")
100 return
101 }
102 s.log.Info().Msg("local admin socket stopped")
103 })
104}