refactor: clean up admin request handling and improve metadata parsing

Kim committed Dec 11, 2025 at 22:35 UTC e6113a3542854f9aa1ca067d55a2465202f7467c
3 files changed +368 -368
cmd/relay-server/admin.go new
+320
@@ -0,0 +1,320 @@
1 +package main
2 +
3 +import (
4 + "encoding/base64"
5 + "encoding/json"
6 + "fmt"
7 + "net/http"
8 + "os"
9 + "path/filepath"
10 + "strings"
11 + "sync"
12 + "time"
13 +
14 + "github.com/rs/zerolog/log"
15 +
16 + "gosuda.org/portal/portal"
17 + "gosuda.org/portal/utils"
18 +)
19 +
20 +func handleAdminRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
21 + if !utils.IsLocalhost(r) {
22 + http.Error(w, "Forbidden", http.StatusForbidden)
23 + return
24 + }
25 +
26 + route := strings.Trim(strings.TrimPrefix(r.URL.Path, "/admin"), "/")
27 +
28 + switch {
29 + case route == "":
30 + serveAppStatic(w, r, "", serv)
31 + case route == "leases" && r.Method == http.MethodGet:
32 + writeJSON(w, convertLeaseEntriesToAdminRows(serv))
33 + case route == "leases/banned" && r.Method == http.MethodGet:
34 + writeJSON(w, serv.GetLeaseManager().GetBannedLeases())
35 + case route == "stats" && r.Method == http.MethodGet:
36 + writeJSON(w, map[string]interface{}{
37 + "leases_count": len(serv.GetAllLeaseEntries()),
38 + "uptime": "TODO",
39 + })
40 + case strings.HasPrefix(route, "leases/") && strings.HasSuffix(route, "/ban"):
41 + handleLeaseBanRequest(w, r, serv, route)
42 + case strings.HasPrefix(route, "leases/") && strings.HasSuffix(route, "/bps"):
43 + handleLeaseBPSRequest(w, r, serv, route)
44 + default:
45 + http.NotFound(w, r)
46 + }
47 +}
48 +
49 +func handleLeaseBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
50 + parts := strings.Split(route, "/")
51 + if len(parts) != 3 {
52 + http.NotFound(w, r)
53 + return
54 + }
55 +
56 + leaseID, ok := decodeLeaseID(parts[1])
57 + if !ok {
58 + http.Error(w, "Invalid lease ID", http.StatusBadRequest)
59 + return
60 + }
61 +
62 + switch r.Method {
63 + case http.MethodPost:
64 + serv.GetLeaseManager().BanLease(leaseID)
65 + saveAdminSettings(serv, globalBPSManager)
66 + w.WriteHeader(http.StatusOK)
67 + case http.MethodDelete:
68 + serv.GetLeaseManager().UnbanLease(leaseID)
69 + saveAdminSettings(serv, globalBPSManager)
70 + w.WriteHeader(http.StatusOK)
71 + default:
72 + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
73 + }
74 +}
75 +
76 +func handleLeaseBPSRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
77 + parts := strings.Split(route, "/")
78 + if len(parts) != 3 {
79 + http.NotFound(w, r)
80 + return
81 + }
82 +
83 + leaseID, ok := decodeLeaseID(parts[1])
84 + if !ok {
85 + http.Error(w, "Invalid lease ID", http.StatusBadRequest)
86 + return
87 + }
88 +
89 + switch r.Method {
90 + case http.MethodPost:
91 + var req struct {
92 + BPS int64 `json:"bps"`
93 + }
94 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
95 + http.Error(w, "Invalid request body", http.StatusBadRequest)
96 + return
97 + }
98 + oldBPS := globalBPSManager.GetBPSLimit(leaseID)
99 + globalBPSManager.SetBPSLimit(leaseID, req.BPS)
100 + log.Info().
101 + Str("lease_id", leaseID).
102 + Int64("old_bps", oldBPS).
103 + Int64("new_bps", req.BPS).
104 + Msg("[Admin] BPS limit updated")
105 + saveAdminSettings(serv, globalBPSManager)
106 + w.WriteHeader(http.StatusOK)
107 + case http.MethodDelete:
108 + oldBPS := globalBPSManager.GetBPSLimit(leaseID)
109 + globalBPSManager.SetBPSLimit(leaseID, 0)
110 + log.Info().
111 + Str("lease_id", leaseID).
112 + Int64("old_bps", oldBPS).
113 + Msg("[Admin] BPS limit removed (now unlimited)")
114 + saveAdminSettings(serv, globalBPSManager)
115 + w.WriteHeader(http.StatusOK)
116 + default:
117 + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
118 + }
119 +}
120 +
121 +func decodeLeaseID(encoded string) (string, bool) {
122 + idBytes, err := base64.URLEncoding.DecodeString(encoded)
123 + if err != nil {
124 + idBytes, err = base64.RawURLEncoding.DecodeString(encoded)
125 + if err != nil {
126 + return "", false
127 + }
128 + }
129 + return string(idBytes), true
130 +}
131 +
132 +// convertLeaseEntriesToAdminRows converts LeaseEntry data to leaseRow format for admin API
133 +func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
134 + leaseEntries := serv.GetAllLeaseEntries()
135 + rows := []leaseRow{}
136 + now := time.Now()
137 +
138 + for _, leaseEntry := range leaseEntries {
139 + if now.After(leaseEntry.Expires) {
140 + continue
141 + }
142 +
143 + lease := leaseEntry.Lease
144 + identityID := string(lease.Identity.Id)
145 +
146 + ttl := time.Until(leaseEntry.Expires)
147 + ttlStr := ""
148 + if ttl > 0 {
149 + if ttl > time.Hour {
150 + ttlStr = fmt.Sprintf("%.0fh", ttl.Hours())
151 + } else if ttl > time.Minute {
152 + ttlStr = fmt.Sprintf("%.0fm", ttl.Minutes())
153 + } else {
154 + ttlStr = fmt.Sprintf("%.0fs", ttl.Seconds())
155 + }
156 + }
157 +
158 + since := now.Sub(leaseEntry.LastSeen)
159 + if since < 0 {
160 + since = 0
161 + }
162 + lastSeenStr := func(d time.Duration) string {
163 + if d >= time.Hour {
164 + h := int(d / time.Hour)
165 + m := int((d % time.Hour) / time.Minute)
166 + if m > 0 {
167 + return fmt.Sprintf("%dh %dm", h, m)
168 + }
169 + return fmt.Sprintf("%dh", h)
170 + }
171 + if d >= time.Minute {
172 + m := int(d / time.Minute)
173 + s := int((d % time.Minute) / time.Second)
174 + if s > 0 {
175 + return fmt.Sprintf("%dm %ds", m, s)
176 + }
177 + return fmt.Sprintf("%dm", m)
178 + }
179 + return fmt.Sprintf("%ds", int(d/time.Second))
180 + }(since)
181 + lastSeenISO := leaseEntry.LastSeen.UTC().Format(time.RFC3339)
182 +
183 + connected := serv.IsConnectionActive(leaseEntry.ConnectionID)
184 +
185 + name := lease.Name
186 + if name == "" {
187 + name = "(unnamed)"
188 + }
189 +
190 + kind := "client"
191 + if len(lease.Alpn) > 0 {
192 + kind = lease.Alpn[0]
193 + }
194 +
195 + dnsLabel := identityID
196 + if len(dnsLabel) > 8 {
197 + dnsLabel = dnsLabel[:8] + "..."
198 + }
199 +
200 + base := flagPortalAppURL
201 + if base == "" {
202 + base = flagPortalURL
203 + }
204 + link := fmt.Sprintf("//%s.%s/", lease.Name, utils.StripWildCard(utils.StripScheme(base)))
205 +
206 + bps := globalBPSManager.GetBPSLimit(identityID)
207 +
208 + rows = append(rows, leaseRow{
209 + Peer: identityID,
210 + Name: name,
211 + Kind: kind,
212 + Connected: connected,
213 + DNS: dnsLabel,
214 + LastSeen: lastSeenStr,
215 + LastSeenISO: lastSeenISO,
216 + TTL: ttlStr,
217 + Link: link,
218 + StaleRed: !connected && since >= 15*time.Second,
219 + Metadata: lease.Metadata,
220 + BPS: bps,
221 + })
222 + }
223 +
224 + return rows
225 +}
226 +
227 +// AdminSettings stores persistent admin configuration
228 +type AdminSettings struct {
229 + BannedLeases []string `json:"banned_leases"`
230 + BPSLimits map[string]int64 `json:"bps_limits"`
231 +}
232 +
233 +var (
234 + adminSettingsPath = "admin_settings.json"
235 + adminSettingsMu sync.Mutex
236 +)
237 +
238 +// SetAdminSettingsPath sets the path for admin settings file
239 +func SetAdminSettingsPath(path string) {
240 + adminSettingsMu.Lock()
241 + defer adminSettingsMu.Unlock()
242 + adminSettingsPath = path
243 +}
244 +
245 +func saveAdminSettings(serv *portal.RelayServer, bpsManager *BPSManager) {
246 + adminSettingsMu.Lock()
247 + defer adminSettingsMu.Unlock()
248 +
249 + lm := serv.GetLeaseManager()
250 +
251 + bannedBytes := lm.GetBannedLeases()
252 + banned := make([]string, len(bannedBytes))
253 + for i, b := range bannedBytes {
254 + banned[i] = string(b)
255 + }
256 +
257 + bpsLimits := bpsManager.GetAllBPSLimits()
258 +
259 + settings := AdminSettings{
260 + BannedLeases: banned,
261 + BPSLimits: bpsLimits,
262 + }
263 +
264 + data, err := json.MarshalIndent(settings, "", " ")
265 + if err != nil {
266 + log.Error().Err(err).Msg("[Admin] Failed to marshal admin settings")
267 + return
268 + }
269 +
270 + dir := filepath.Dir(adminSettingsPath)
271 + if dir != "" && dir != "." {
272 + if err := os.MkdirAll(dir, 0755); err != nil {
273 + log.Error().Err(err).Msg("[Admin] Failed to create settings directory")
274 + return
275 + }
276 + }
277 +
278 + if err := os.WriteFile(adminSettingsPath, data, 0644); err != nil {
279 + log.Error().Err(err).Msg("[Admin] Failed to save admin settings")
280 + return
281 + }
282 +
283 + log.Debug().Str("path", adminSettingsPath).Msg("[Admin] Saved admin settings")
284 +}
285 +
286 +func loadAdminSettings(serv *portal.RelayServer, bpsManager *BPSManager) {
287 + adminSettingsMu.Lock()
288 + defer adminSettingsMu.Unlock()
289 +
290 + data, err := os.ReadFile(adminSettingsPath)
291 + if err != nil {
292 + if os.IsNotExist(err) {
293 + log.Debug().Msg("[Admin] No admin settings file found, starting fresh")
294 + return
295 + }
296 + log.Error().Err(err).Msg("[Admin] Failed to read admin settings")
297 + return
298 + }
299 +
300 + var settings AdminSettings
301 + if err := json.Unmarshal(data, &settings); err != nil {
302 + log.Error().Err(err).Msg("[Admin] Failed to parse admin settings")
303 + return
304 + }
305 +
306 + lm := serv.GetLeaseManager()
307 +
308 + for _, leaseID := range settings.BannedLeases {
309 + lm.BanLease(leaseID)
310 + }
311 +
312 + for leaseID, bps := range settings.BPSLimits {
313 + bpsManager.SetBPSLimit(leaseID, bps)
314 + }
315 +
316 + log.Info().
317 + Int("banned_count", len(settings.BannedLeases)).
318 + Int("bps_limits_count", len(settings.BPSLimits)).
319 + Msg("[Admin] Loaded admin settings")
320 +}
cmd/relay-server/view.go
+15 -368
@@ -3,20 +3,16 @@ package main
3 import (
4 "context"
5 "embed"
6 - "encoding/base64"
6 "encoding/json"
7 "fmt"
9 - "net"
8 "net/http"
11 - "os"
12 - "path/filepath"
9 "strings"
14 - "sync"
10 "time"
11
12 "github.com/rs/zerolog/log"
13
14 "gosuda.org/portal/portal"
15 + "gosuda.org/portal/sdk"
16 "gosuda.org/portal/utils"
17 )
18
@@ -201,241 +197,11 @@ type leaseRow struct {
197 TTL string
198 Link string
199 StaleRed bool
204 - Hide bool
200 Metadata string
201 BPS int64 // bytes-per-second limit (0 = unlimited)
202 }
203
204 // convertLeaseEntriesToRows converts LeaseEntry data from LeaseManager to leaseRow format for the app page
210 -func handleAdminRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
211 - if !isLocalhost(r) {
212 - http.Error(w, "Forbidden", http.StatusForbidden)
213 - return
214 - }
215 -
216 - // Simple routing for admin
217 - p := strings.TrimPrefix(r.URL.Path, "/admin/")
218 -
219 - if p == "leases" && r.Method == http.MethodGet {
220 - // List all leases in the same format as SSR data (leaseRow)
221 - rows := convertLeaseEntriesToAdminRows(serv)
222 - w.Header().Set("Content-Type", "application/json")
223 - json.NewEncoder(w).Encode(rows)
224 - return
225 - }
226 -
227 - if p == "stats" && r.Method == http.MethodGet {
228 - // Basic stats
229 - stats := map[string]interface{}{
230 - "leases_count": len(serv.GetAllLeaseEntries()),
231 - "uptime": "TODO", // We could add start time to server
232 - }
233 - w.Header().Set("Content-Type", "application/json")
234 - json.NewEncoder(w).Encode(stats)
235 - return
236 - }
237 -
238 - if strings.HasPrefix(p, "leases/") && strings.HasSuffix(p, "/ban") {
239 - parts := strings.Split(p, "/")
240 - if len(parts) == 3 {
241 - encodedID := parts[1]
242 -
243 - // Decode ID (expecting URL-safe base64 from frontend)
244 - idBytes, err := base64.URLEncoding.DecodeString(encodedID)
245 - if err != nil {
246 - // Try Raw URL encoding
247 - idBytes, err = base64.RawURLEncoding.DecodeString(encodedID)
248 - }
249 -
250 - leaseID := encodedID
251 - if err == nil {
252 - leaseID = string(idBytes)
253 - }
254 -
255 - if r.Method == http.MethodPost {
256 - serv.GetLeaseManager().BanLease(leaseID)
257 - saveAdminSettings(serv, globalBPSManager)
258 - w.WriteHeader(http.StatusOK)
259 - return
260 - }
261 - if r.Method == http.MethodDelete {
262 - serv.GetLeaseManager().UnbanLease(leaseID)
263 - saveAdminSettings(serv, globalBPSManager)
264 - w.WriteHeader(http.StatusOK)
265 - return
266 - }
267 - }
268 - }
269 -
270 - if p == "leases/banned" && r.Method == http.MethodGet {
271 - banned := serv.GetLeaseManager().GetBannedLeases()
272 - w.Header().Set("Content-Type", "application/json")
273 - json.NewEncoder(w).Encode(banned)
274 - return
275 - }
276 -
277 - // Set BPS limit for a lease: POST/DELETE /admin/leases/{id}/bps
278 - if strings.HasPrefix(p, "leases/") && strings.HasSuffix(p, "/bps") {
279 - parts := strings.Split(p, "/")
280 - if len(parts) == 3 {
281 - encodedID := parts[1]
282 -
283 - // Decode ID (expecting URL-safe base64 from frontend)
284 - idBytes, err := base64.URLEncoding.DecodeString(encodedID)
285 - if err != nil {
286 - idBytes, err = base64.RawURLEncoding.DecodeString(encodedID)
287 - }
288 -
289 - leaseID := encodedID
290 - if err == nil {
291 - leaseID = string(idBytes)
292 - }
293 -
294 - if r.Method == http.MethodPost {
295 - var req struct {
296 - BPS int64 `json:"bps"`
297 - }
298 - if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
299 - http.Error(w, "Invalid request body", http.StatusBadRequest)
300 - return
301 - }
302 - oldBPS := globalBPSManager.GetBPSLimit(leaseID)
303 - globalBPSManager.SetBPSLimit(leaseID, req.BPS)
304 - log.Info().
305 - Str("lease_id", leaseID).
306 - Int64("old_bps", oldBPS).
307 - Int64("new_bps", req.BPS).
308 - Msg("[Admin] BPS limit updated")
309 - saveAdminSettings(serv, globalBPSManager)
310 - w.WriteHeader(http.StatusOK)
311 - return
312 - }
313 - if r.Method == http.MethodDelete {
314 - oldBPS := globalBPSManager.GetBPSLimit(leaseID)
315 - globalBPSManager.SetBPSLimit(leaseID, 0)
316 - log.Info().
317 - Str("lease_id", leaseID).
318 - Int64("old_bps", oldBPS).
319 - Msg("[Admin] BPS limit removed (now unlimited)")
320 - saveAdminSettings(serv, globalBPSManager)
321 - w.WriteHeader(http.StatusOK)
322 - return
323 - }
324 - }
325 - }
326 -
327 - serveAppStatic(w, r, "", serv)
328 -}
329 -
330 -// convertLeaseEntriesToAdminRows converts LeaseEntry data to leaseRow format for admin API
331 -// Unlike convertLeaseEntriesToRows, this includes banned and hidden entries
332 -func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
333 - leaseEntries := serv.GetAllLeaseEntries()
334 - rows := []leaseRow{}
335 - now := time.Now()
336 -
337 - for _, leaseEntry := range leaseEntries {
338 - // Check if lease is still valid
339 - if now.After(leaseEntry.Expires) {
340 - continue
341 - }
342 -
343 - lease := leaseEntry.Lease
344 - identityID := string(lease.Identity.Id)
345 -
346 - // Calculate TTL
347 - ttl := time.Until(leaseEntry.Expires)
348 - ttlStr := ""
349 - if ttl > 0 {
350 - if ttl > time.Hour {
351 - ttlStr = fmt.Sprintf("%.0fh", ttl.Hours())
352 - } else if ttl > time.Minute {
353 - ttlStr = fmt.Sprintf("%.0fm", ttl.Minutes())
354 - } else {
355 - ttlStr = fmt.Sprintf("%.0fs", ttl.Seconds())
356 - }
357 - }
358 -
359 - // Format last active as relative time
360 - since := now.Sub(leaseEntry.LastSeen)
361 - if since < 0 {
362 - since = 0
363 - }
364 - lastSeenStr := func(d time.Duration) string {
365 - if d >= time.Hour {
366 - h := int(d / time.Hour)
367 - m := int((d % time.Hour) / time.Minute)
368 - if m > 0 {
369 - return fmt.Sprintf("%dh %dm", h, m)
370 - }
371 - return fmt.Sprintf("%dh", h)
372 - }
373 - if d >= time.Minute {
374 - m := int(d / time.Minute)
375 - s := int((d % time.Minute) / time.Second)
376 - if s > 0 {
377 - return fmt.Sprintf("%dm %ds", m, s)
378 - }
379 - return fmt.Sprintf("%dm", m)
380 - }
381 - s := int(d / time.Second)
382 - return fmt.Sprintf("%ds", s)
383 - }(since)
384 - lastSeenISO := leaseEntry.LastSeen.UTC().Format(time.RFC3339)
385 -
386 - // Check if connection is still active
387 - connected := serv.IsConnectionActive(leaseEntry.ConnectionID)
388 -
389 - // Use name from lease if available
390 - name := lease.Name
391 - if name == "" {
392 - name = "(unnamed)"
393 - }
394 -
395 - // Determine kind/type based on ALPN if available
396 - kind := "client"
397 - if len(lease.Alpn) > 0 {
398 - kind = lease.Alpn[0]
399 - }
400 -
401 - // Create DNS label from identity
402 - dnsLabel := identityID
403 - if len(dnsLabel) > 8 {
404 - dnsLabel = dnsLabel[:8] + "..."
405 - }
406 -
407 - // Build link using the configured subdomain base
408 - base := flagPortalAppURL
409 - if base == "" {
410 - base = flagPortalURL
411 - }
412 - link := fmt.Sprintf("//%s.%s/", lease.Name, utils.StripWildCard(utils.StripScheme(base)))
413 -
414 - // Get BPS limit for this lease from BPSManager
415 - bps := globalBPSManager.GetBPSLimit(identityID)
416 -
417 - row := leaseRow{
418 - Peer: identityID,
419 - Name: name,
420 - Kind: kind,
421 - Connected: connected,
422 - DNS: dnsLabel,
423 - LastSeen: lastSeenStr,
424 - LastSeenISO: lastSeenISO,
425 - TTL: ttlStr,
426 - Link: link,
427 - StaleRed: !connected && since >= 15*time.Second,
428 - Hide: parseHideFromMetadata(lease.Metadata),
429 - Metadata: lease.Metadata,
430 - BPS: bps,
431 - }
432 -
433 - rows = append(rows, row)
434 - }
435 -
436 - return rows
437 -}
438 -
205 func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
206 // Get all lease entries directly from the lease manager
207 leaseEntries := serv.GetAllLeaseEntries()
@@ -445,7 +211,11 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
211 now := time.Now()
212
213 // Build banned map once for O(1) lookup per lease
448 - bannedMap := buildBannedMap(serv.GetLeaseManager().GetBannedLeases())
214 + bannedList := serv.GetLeaseManager().GetBannedLeases()
215 + bannedMap := make(map[string]struct{}, len(bannedList))
216 + for _, b := range bannedList {
217 + bannedMap[string(b)] = struct{}{}
218 + }
219
220 for _, leaseEntry := range leaseEntries {
221 // Check if lease is still valid
@@ -456,13 +226,16 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
226 lease := leaseEntry.Lease
227 identityID := string(lease.Identity.Id)
228
229 + var metadata sdk.Metadata
230 + json.Unmarshal([]byte(lease.Metadata), &metadata)
231 +
232 // Skip banned leases for user-facing list
233 if _, banned := bannedMap[identityID]; banned {
234 continue
235 }
236
237 // Check hidden status
465 - if parseHideFromMetadata(lease.Metadata) {
238 + if metadata.Hide {
239 continue
240 }
241
@@ -550,12 +323,11 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
323 TTL: ttlStr,
324 Link: link,
325 StaleRed: !connected && since >= 15*time.Second,
553 - Hide: parseHideFromMetadata(lease.Metadata),
326 Metadata: lease.Metadata,
327 }
328
329 // Hidden entries are already filtered above, but keep check for safety
558 - if !row.Hide {
330 + if !metadata.Hide {
331 rows = append(rows, row)
332 }
333 }
@@ -563,134 +335,9 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
335 return rows
336 }
337
566 -func isLocalhost(r *http.Request) bool {
567 - host, _, err := net.SplitHostPort(r.RemoteAddr)
568 - if err != nil {
569 - host = r.RemoteAddr
570 - }
571 - return host == "127.0.0.1" || host == "::1"
572 -}
573 -
574 -// buildBannedMap converts banned list to O(1) lookup map
575 -func buildBannedMap(bannedList [][]byte) map[string]struct{} {
576 - m := make(map[string]struct{}, len(bannedList))
577 - for _, b := range bannedList {
578 - m[string(b)] = struct{}{}
579 - }
580 - return m
581 -}
582 -
583 -// AdminSettings stores persistent admin configuration
584 -type AdminSettings struct {
585 - BannedLeases []string `json:"banned_leases"`
586 - BPSLimits map[string]int64 `json:"bps_limits"`
587 -}
588 -
589 -var (
590 - adminSettingsPath = "admin_settings.json"
591 - adminSettingsMu sync.Mutex
592 -)
593 -
594 -// SetAdminSettingsPath sets the path for admin settings file
595 -func SetAdminSettingsPath(path string) {
596 - adminSettingsMu.Lock()
597 - defer adminSettingsMu.Unlock()
598 - adminSettingsPath = path
599 -}
600 -
601 -// saveAdminSettings persists ban and BPS settings to disk
602 -func saveAdminSettings(serv *portal.RelayServer, bpsManager *BPSManager) {
603 - adminSettingsMu.Lock()
604 - defer adminSettingsMu.Unlock()
605 -
606 - lm := serv.GetLeaseManager()
607 -
608 - // Collect banned leases
609 - bannedBytes := lm.GetBannedLeases()
610 - banned := make([]string, len(bannedBytes))
611 - for i, b := range bannedBytes {
612 - banned[i] = string(b)
613 - }
614 -
615 - // Collect BPS limits from BPSManager
616 - bpsLimits := bpsManager.GetAllBPSLimits()
617 -
618 - settings := AdminSettings{
619 - BannedLeases: banned,
620 - BPSLimits: bpsLimits,
621 - }
622 -
623 - data, err := json.MarshalIndent(settings, "", " ")
624 - if err != nil {
625 - log.Error().Err(err).Msg("[Admin] Failed to marshal admin settings")
626 - return
627 - }
628 -
629 - // Ensure directory exists
630 - dir := filepath.Dir(adminSettingsPath)
631 - if dir != "" && dir != "." {
632 - if err := os.MkdirAll(dir, 0755); err != nil {
633 - log.Error().Err(err).Msg("[Admin] Failed to create settings directory")
634 - return
635 - }
636 - }
637 -
638 - if err := os.WriteFile(adminSettingsPath, data, 0644); err != nil {
639 - log.Error().Err(err).Msg("[Admin] Failed to save admin settings")
640 - return
641 - }
642 -
643 - log.Debug().Str("path", adminSettingsPath).Msg("[Admin] Saved admin settings")
644 -}
645 -
646 -// loadAdminSettings loads ban and BPS settings from disk
647 -func loadAdminSettings(serv *portal.RelayServer, bpsManager *BPSManager) {
648 - adminSettingsMu.Lock()
649 - defer adminSettingsMu.Unlock()
650 -
651 - data, err := os.ReadFile(adminSettingsPath)
652 - if err != nil {
653 - if os.IsNotExist(err) {
654 - log.Debug().Msg("[Admin] No admin settings file found, starting fresh")
655 - return
656 - }
657 - log.Error().Err(err).Msg("[Admin] Failed to read admin settings")
658 - return
659 - }
660 -
661 - var settings AdminSettings
662 - if err := json.Unmarshal(data, &settings); err != nil {
663 - log.Error().Err(err).Msg("[Admin] Failed to parse admin settings")
664 - return
665 - }
666 -
667 - lm := serv.GetLeaseManager()
668 -
669 - // Restore banned leases
670 - for _, leaseID := range settings.BannedLeases {
671 - lm.BanLease(leaseID)
672 - }
673 -
674 - // Restore BPS limits to BPSManager
675 - for leaseID, bps := range settings.BPSLimits {
676 - bpsManager.SetBPSLimit(leaseID, bps)
677 - }
678 -
679 - log.Info().
680 - Int("banned_count", len(settings.BannedLeases)).
681 - Int("bps_limits_count", len(settings.BPSLimits)).
682 - Msg("[Admin] Loaded admin settings")
683 -}
684 -
685 -func parseHideFromMetadata(metadata string) bool {
686 - if metadata == "" {
687 - return false
688 - }
689 - var pm struct {
690 - Hide bool `json:"hide"`
691 - }
692 - if err := json.Unmarshal([]byte(metadata), &pm); err != nil {
693 - return false
338 +func writeJSON(w http.ResponseWriter, v any) {
339 + w.Header().Set("Content-Type", "application/json")
340 + if err := json.NewEncoder(w).Encode(v); err != nil {
341 + log.Error().Err(err).Msg("[HTTP] Failed to encode response")
342 }
695 - return pm.Hide
343 }
utils/utils.go
+33
@@ -5,6 +5,7 @@ import (
5 "fmt"
6 "io"
7 "mime"
8 + "net"
9 "net/http"
10 "net/url"
11 "regexp"
@@ -289,3 +290,35 @@ func DefaultBootstrapFrom(base string) string {
290 }
291 return "ws://" + host + "/relay"
292 }
293 +
294 +func IsLocalhost(r *http.Request) bool {
295 + host, _, err := net.SplitHostPort(r.RemoteAddr)
296 + if err != nil {
297 + host = r.RemoteAddr
298 + }
299 +
300 + ip := net.ParseIP(host)
301 + if ip == nil {
302 + return false
303 + }
304 + // Normal localhost
305 + if ip.IsLoopback() {
306 + return true
307 + }
308 +
309 + // Docker networks & private networks
310 + dockerRanges := []string{
311 + "172.17.0.0/16", // Linux Docker
312 + "192.168.64.0/24", // Docker Desktop macOS/Windows
313 + "192.168.65.0/24",
314 + }
315 +
316 + for _, cidr := range dockerRanges {
317 + _, subnet, _ := net.ParseCIDR(cidr)
318 + if subnet.Contains(ip) {
319 + return true
320 + }
321 + }
322 +
323 + return false
324 +}