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
+}