-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhttp_server.go
More file actions
246 lines (211 loc) · 9.4 KB
/
Copy pathhttp_server.go
File metadata and controls
246 lines (211 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
package nara
import (
"bytes"
"embed"
"fmt"
"io/fs"
"net"
"net/http"
"strings"
"time"
"github.com/sirupsen/logrus"
)
//go:embed all:nara-web/public
var staticContent embed.FS
// responseLogger wraps ResponseWriter to capture status code
type responseLogger struct {
http.ResponseWriter
status int
}
func (rl *responseLogger) WriteHeader(code int) {
rl.status = code
rl.ResponseWriter.WriteHeader(code)
}
// Flush implements http.Flusher interface by delegating to underlying ResponseWriter
func (rl *responseLogger) Flush() {
if f, ok := rl.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// loggingMiddleware wraps an http.HandlerFunc with request/response logging
func (network *Network) loggingMiddleware(path string, handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Wrap response writer to capture status
wrapped := &responseLogger{ResponseWriter: w, status: 200}
handler(wrapped, r)
// Batch log the request (aggregated every 3s by LogService)
if network.logService != nil {
network.logService.BatchHTTP(r.Method, path, wrapped.status)
}
}
}
func (network *Network) startHttpServer(httpAddr string) error {
listen_interface := httpAddr
if listen_interface == "" {
listen_interface = ":8080"
}
listener, err := net.Listen("tcp", listen_interface)
if err != nil {
return fmt.Errorf("listen error: %w", err)
}
port := listener.Addr().(*net.TCPAddr).Port
logrus.Printf("Listening for HTTP on port %d", port)
// Create a mux for handlers (so we can reuse with mesh server)
mux := network.createHTTPMux(true) // includeUI = true
// Store the actual listening address for tests
actualAddr := fmt.Sprintf(":%d", port)
network.httpServer = &http.Server{
Handler: mux,
Addr: actualAddr,
}
go func() {
if err := network.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
logrus.WithError(err).Error("HTTP server error")
}
}()
return nil
}
// createHTTPMux creates an HTTP mux with all handlers
// includeUI: whether to include web UI handlers (false for mesh-only server)
func (network *Network) createHTTPMux(includeUI bool) *http.ServeMux {
mux := http.NewServeMux()
var publicFS fs.FS
// Mesh endpoints - available on both local and mesh servers
// These require Ed25519 authentication (except /ping which needs to be fast)
mux.HandleFunc("/events/sync", network.loggingMiddleware("/events/sync", network.meshAuthMiddleware("/events/sync", network.httpEventsSyncHandler)))
mux.HandleFunc("/gossip/zine", network.loggingMiddleware("/gossip/zine", network.meshAuthMiddleware("/gossip/zine", network.httpGossipZineHandler)))
mux.HandleFunc("/dm", network.loggingMiddleware("/dm", network.meshAuthMiddleware("/dm", network.httpDMHandler)))
mux.HandleFunc("/world/relay", network.loggingMiddleware("/world/relay", network.meshAuthMiddleware("/world/relay", network.httpWorldRelayHandler)))
mux.HandleFunc("/mesh/message", network.loggingMiddleware("/mesh/message", network.meshAuthMiddleware("/mesh/message", network.httpMeshMessageHandler)))
mux.HandleFunc("/ping", network.loggingMiddleware("/ping", network.httpPingHandler)) // No auth - latency critical
mux.HandleFunc("/coordinates", network.loggingMiddleware("/coordinates", network.httpCoordinatesHandler))
mux.HandleFunc("/peer/query", network.loggingMiddleware("/peer/query", network.httpPeerQueryHandler)) // No auth - used for peer discovery
// Checkpoint sync endpoint - serves all checkpoints for boot recovery
// Available on mesh for distributed timeline recovery
mux.HandleFunc("/api/checkpoints/all", network.httpCheckpointsAllHandler)
// Event import endpoint - allows owner to restore events from backup
// No mesh auth required - uses soul-based signature verification
mux.HandleFunc("/api/events/import", network.loggingMiddleware("/api/events/import", network.httpEventsImportHandler))
if includeUI {
// Prepare static FS
var err error
publicFS, err = fs.Sub(staticContent, "nara-web/public")
if err != nil {
logrus.Errorf("failed to load embedded UI assets: %v", err)
}
// Profile pages: serve SPA for /nara/{name}
mux.HandleFunc("/nara/", func(w http.ResponseWriter, r *http.Request) {
if data, err := fs.ReadFile(staticContent, "nara-web/public/inspector.html"); err == nil {
http.ServeContent(w, r, "inspector.html", time.Now(), bytes.NewReader(data))
return
}
http.NotFound(w, r)
})
// Profile JSON data: /profile/{name}.json
mux.HandleFunc("/profile/", network.httpProfileJsonHandler)
// Web UI endpoints - only on local server
mux.HandleFunc("/api.json", network.httpApiJsonHandler)
mux.HandleFunc("/narae.json", network.httpNaraeJsonHandler)
mux.HandleFunc("/metrics", network.httpMetricsHandler)
mux.HandleFunc("/status/", network.httpStatusJsonHandler)
mux.HandleFunc("/events", network.httpEventsSSEHandler)
mux.HandleFunc("/social/clout", network.httpCloutHandler)
mux.HandleFunc("/social/recent", network.httpRecentEventsHandler)
mux.HandleFunc("/social/teases", network.httpTeaseCountsHandler)
mux.HandleFunc("/world/start", network.httpWorldStartHandler)
mux.HandleFunc("/world/journeys", network.httpWorldJourneysHandler)
mux.HandleFunc("/network/map", network.httpNetworkMapHandler)
mux.HandleFunc("/proximity", network.httpProximityHandler)
// Stash API endpoints
mux.HandleFunc("/api/stash/status", network.httpStashStatusHandler)
mux.HandleFunc("/api/stash/update", network.httpStashUpdateHandler)
mux.HandleFunc("/api/stash/recover", network.httpStashRecoverHandler)
mux.HandleFunc("/api/stash/confidants", network.httpStashConfidantsHandler)
// Inspector API endpoints
mux.HandleFunc("/api/inspector/events", network.local.inspectorEventsHandler)
mux.HandleFunc("/api/inspector/checkpoints", network.local.inspectorCheckpointsHandler)
mux.HandleFunc("/api/inspector/checkpoint/", network.local.inspectorCheckpointDetailHandler)
mux.HandleFunc("/api/inspector/projections", network.local.inspectorProjectionsHandler)
mux.HandleFunc("/api/inspector/projection/", network.local.inspectorProjectionDetailHandler)
mux.HandleFunc("/api/inspector/event/", network.local.inspectorEventDetailHandler)
mux.HandleFunc("/api/inspector/uptime/", network.local.inspectorUptimeHandler)
// Catch-all for unknown API endpoints - return 404 instead of SPA
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
})
// SPA handler - serves inspector.html for all app routes
// This is the main page at / with all tabs (Home, World, Timeline, etc.)
spaHandler := func(w http.ResponseWriter, r *http.Request) {
if data, err := fs.ReadFile(staticContent, "nara-web/public/inspector.html"); err == nil {
http.ServeContent(w, r, "inspector.html", time.Now(), bytes.NewReader(data))
return
}
http.NotFound(w, r)
}
// SPA routes - all these serve inspector.html, React router handles the rest
mux.HandleFunc("/world", spaHandler)
mux.HandleFunc("/world/", spaHandler)
mux.HandleFunc("/timeline", spaHandler)
mux.HandleFunc("/timeline/", spaHandler)
mux.HandleFunc("/checkpoints", spaHandler)
mux.HandleFunc("/checkpoints/", spaHandler)
mux.HandleFunc("/projections", spaHandler)
mux.HandleFunc("/projections/", spaHandler)
mux.HandleFunc("/events/", spaHandler)
// Legacy inspector route redirects to root
mux.HandleFunc("/inspector", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusMovedPermanently)
})
mux.HandleFunc("/inspector/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusMovedPermanently)
})
// Static file server with SPA fallback for root
// First try to serve static files, then fall back to SPA
staticHandler := http.FileServer(http.FS(publicFS))
if docsFS, err := fs.Sub(publicFS, "docs"); err == nil {
docsHandler := http.StripPrefix("/docs/", http.FileServer(http.FS(docsFS)))
mux.Handle("/docs/", docsHandler)
mux.HandleFunc("/docs", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/docs/", http.StatusMovedPermanently)
})
}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// If path is exactly "/" or doesn't match a static file, serve SPA
if r.URL.Path == "/" || r.URL.Path == "/home" {
spaHandler(w, r)
return
}
// Check if it's a static file (CSS, JS, etc.)
// Try to open the file - if it exists, serve it
filePath := strings.TrimPrefix(r.URL.Path, "/")
if _, err := fs.Stat(publicFS, filePath); err == nil {
staticHandler.ServeHTTP(w, r)
return
}
// Not a static file, serve SPA (for client-side routing)
spaHandler(w, r)
})
}
return mux
}
// startMeshHttpServer starts an HTTP server on the tsnet interface for mesh communication
func (network *Network) startMeshHttpServer(tsnetServer interface {
Listen(string, string) (net.Listener, error)
}) error {
listener, err := tsnetServer.Listen("tcp", fmt.Sprintf(":%d", DefaultMeshPort))
if err != nil {
return fmt.Errorf("failed to listen on tsnet: %w", err)
}
logrus.Printf("🕸️ Mesh HTTP server listening on port %d (Tailscale interface)", DefaultMeshPort)
// Create a mux with mesh-only endpoints (no UI)
mux := network.createHTTPMux(false)
network.meshHttpServer = &http.Server{
Handler: mux,
}
go func() {
if err := network.meshHttpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
logrus.WithError(err).Error("Mesh HTTP server error")
}
}()
return nil
}