refactor(cmd): split utils.go into http_helpers.go and lease_rows.go
C8: Pure file reorganization, no behavior change. - http_helpers.go: API envelope writers, CORS, content-type mapping, secure request detection, WebSocket detection, base64 lease decode - lease_rows.go: leaseRow struct, formatting helpers, lease entry conversion for admin UI and frontend display
cognitive committed
Mar 5, 2026 at 05:24 UTC
b9ab5ef445d04492f4d358b300b1cb17e8fb2b4b
2 files changed
+134
-124
cmd/relay-server/http_helpers.go
new
+134
@@ -0,0 +1,134 @@
1
+package main
2
+
3
+import (
4
+ "encoding/base64"
5
+ "encoding/json"
6
+ "net/http"
7
+ "strings"
8
+
9
+ "github.com/rs/zerolog/log"
10
+
11
+ "gosuda.org/portal/portal/keyless"
12
+ "gosuda.org/portal/portal/policy"
13
+ "gosuda.org/portal/types"
14
+)
15
+
16
+func isSecureRequestWithPolicy(r *http.Request, trustProxyHeaders bool) bool {
17
+ if r == nil {
18
+ return false
19
+ }
20
+ if r.TLS != nil {
21
+ return true
22
+ }
23
+ if !trustProxyHeaders || !policy.IsTrustedProxyRemoteAddr(r.RemoteAddr) {
24
+ return false
25
+ }
26
+ if hasForwardedToken(r.Header.Get("X-Forwarded-Proto"), "https") {
27
+ return true
28
+ }
29
+ return hasForwardedToken(r.Header.Get("X-Forwarded-Ssl"), "on")
30
+}
31
+
32
+func hasForwardedToken(raw, target string) bool {
33
+ for token := range strings.SplitSeq(raw, ",") {
34
+ if strings.EqualFold(strings.TrimSpace(token), target) {
35
+ return true
36
+ }
37
+ }
38
+ return false
39
+}
40
+
41
+func isWebSocketUpgrade(req *http.Request) bool {
42
+ if req == nil {
43
+ return false
44
+ }
45
+ return hasForwardedToken(req.Header.Get("Upgrade"), "websocket")
46
+}
47
+
48
+// getContentType returns the MIME type for a file extension.
49
+func getContentType(ext string) string {
50
+ switch ext {
51
+ case ".html":
52
+ return "text/html; charset=utf-8"
53
+ case ".js":
54
+ return "application/javascript"
55
+ case ".json":
56
+ return "application/json"
57
+ case ".wasm":
58
+ return "application/wasm"
59
+ case ".css":
60
+ return "text/css"
61
+ case ".mp4":
62
+ return "video/mp4"
63
+ case ".svg":
64
+ return "image/svg+xml"
65
+ case ".png":
66
+ return "image/png"
67
+ case ".ico":
68
+ return "image/x-icon"
69
+ default:
70
+ return ""
71
+ }
72
+}
73
+
74
+// setCORSHeaders sets permissive CORS headers for GET/OPTIONS and common headers.
75
+func setCORSHeaders(w http.ResponseWriter) {
76
+ w.Header().Set("Access-Control-Allow-Origin", "*")
77
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
78
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
79
+}
80
+
81
+func writeAPIData(w http.ResponseWriter, status int, data any) {
82
+ w.Header().Set("Content-Type", "application/json")
83
+ w.WriteHeader(status)
84
+ if err := json.NewEncoder(w).Encode(types.APIEnvelope{
85
+ OK: true,
86
+ Data: data,
87
+ }); err != nil {
88
+ log.Error().Err(err).Msg("[HTTP] Failed to encode API success response")
89
+ }
90
+}
91
+
92
+func writeAPIOK(w http.ResponseWriter, status int) {
93
+ w.Header().Set("Content-Type", "application/json")
94
+ w.WriteHeader(status)
95
+ if err := json.NewEncoder(w).Encode(types.APIEnvelope{OK: true}); err != nil {
96
+ log.Error().Err(err).Msg("[HTTP] Failed to encode API success response")
97
+ }
98
+}
99
+
100
+func writeAPIError(w http.ResponseWriter, status int, code, message string) {
101
+ writeAPIErrorWithData(w, status, code, message, nil)
102
+}
103
+
104
+func writeAPIErrorWithData(w http.ResponseWriter, status int, code, message string, data any) {
105
+ w.Header().Set("Content-Type", "application/json")
106
+ w.WriteHeader(status)
107
+ if err := json.NewEncoder(w).Encode(types.APIEnvelope{
108
+ OK: false,
109
+ Data: data,
110
+ Error: &types.APIError{
111
+ Code: code,
112
+ Message: message,
113
+ },
114
+ }); err != nil {
115
+ log.Error().Err(err).Msg("[HTTP] Failed to encode API error response")
116
+ }
117
+}
118
+
119
+func writeSignError(w http.ResponseWriter, status int, message string) {
120
+ w.Header().Set("Content-Type", "application/json")
121
+ w.WriteHeader(status)
122
+ _ = json.NewEncoder(w).Encode(keyless.ErrorResponse{Error: message})
123
+}
124
+
125
+func decodeLeaseID(encoded string) (string, bool) {
126
+ idBytes, err := base64.URLEncoding.DecodeString(encoded)
127
+ if err != nil {
128
+ idBytes, err = base64.RawURLEncoding.DecodeString(encoded)
129
+ if err != nil {
130
+ return "", false
131
+ }
132
+ }
133
+ return string(idBytes), true
134
+}
cmd/relay-server/lease_rows.go
renamed
-124
@@ -1,17 +1,13 @@
1
package main
2
3
import (
4
- "encoding/base64"
4
"encoding/json"
5
"fmt"
7
- "net/http"
8
- "strings"
6
"time"
7
8
"github.com/rs/zerolog/log"
9
10
"gosuda.org/portal/portal"
14
- "gosuda.org/portal/portal/keyless"
11
"gosuda.org/portal/portal/policy"
12
"gosuda.org/portal/types"
13
)
@@ -21,71 +17,6 @@ const (
17
staleLeaseHideWindow = 3 * time.Minute
18
)
19
24
-func isSecureRequestWithPolicy(r *http.Request, trustProxyHeaders bool) bool {
25
- if r == nil {
26
- return false
27
- }
28
- if r.TLS != nil {
29
- return true
30
- }
31
- if !trustProxyHeaders || !policy.IsTrustedProxyRemoteAddr(r.RemoteAddr) {
32
- return false
33
- }
34
- if hasForwardedToken(r.Header.Get("X-Forwarded-Proto"), "https") {
35
- return true
36
- }
37
- return hasForwardedToken(r.Header.Get("X-Forwarded-Ssl"), "on")
38
-}
39
-
40
-func hasForwardedToken(raw, target string) bool {
41
- for token := range strings.SplitSeq(raw, ",") {
42
- if strings.EqualFold(strings.TrimSpace(token), target) {
43
- return true
44
- }
45
- }
46
- return false
47
-}
48
-
49
-func isWebSocketUpgrade(req *http.Request) bool {
50
- if req == nil {
51
- return false
52
- }
53
- return hasForwardedToken(req.Header.Get("Upgrade"), "websocket")
54
-}
55
-
56
-// getContentType returns the MIME type for a file extension.
57
-func getContentType(ext string) string {
58
- switch ext {
59
- case ".html":
60
- return "text/html; charset=utf-8"
61
- case ".js":
62
- return "application/javascript"
63
- case ".json":
64
- return "application/json"
65
- case ".wasm":
66
- return "application/wasm"
67
- case ".css":
68
- return "text/css"
69
- case ".mp4":
70
- return "video/mp4"
71
- case ".svg":
72
- return "image/svg+xml"
73
- case ".png":
74
- return "image/png"
75
- case ".ico":
76
- return "image/x-icon"
77
- default:
78
- return ""
79
- }
80
-}
81
-
82
-// setCORSHeaders sets permissive CORS headers for GET/OPTIONS and common headers.
83
-func setCORSHeaders(w http.ResponseWriter) {
84
- w.Header().Set("Access-Control-Allow-Origin", "*")
85
- w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
86
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
87
-}
88
-
20
// leaseRow represents a lease entry for display in admin UI and frontend.
21
type leaseRow struct {
22
TTL string
@@ -278,58 +209,3 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer, admin *Admin, forAdmin
209
210
return rows
211
}
281
-
282
-func writeAPIData(w http.ResponseWriter, status int, data any) {
283
- w.Header().Set("Content-Type", "application/json")
284
- w.WriteHeader(status)
285
- if err := json.NewEncoder(w).Encode(types.APIEnvelope{
286
- OK: true,
287
- Data: data,
288
- }); err != nil {
289
- log.Error().Err(err).Msg("[HTTP] Failed to encode API success response")
290
- }
291
-}
292
-
293
-func writeAPIOK(w http.ResponseWriter, status int) {
294
- w.Header().Set("Content-Type", "application/json")
295
- w.WriteHeader(status)
296
- if err := json.NewEncoder(w).Encode(types.APIEnvelope{OK: true}); err != nil {
297
- log.Error().Err(err).Msg("[HTTP] Failed to encode API success response")
298
- }
299
-}
300
-
301
-func writeAPIError(w http.ResponseWriter, status int, code, message string) {
302
- writeAPIErrorWithData(w, status, code, message, nil)
303
-}
304
-
305
-func writeAPIErrorWithData(w http.ResponseWriter, status int, code, message string, data any) {
306
- w.Header().Set("Content-Type", "application/json")
307
- w.WriteHeader(status)
308
- if err := json.NewEncoder(w).Encode(types.APIEnvelope{
309
- OK: false,
310
- Data: data,
311
- Error: &types.APIError{
312
- Code: code,
313
- Message: message,
314
- },
315
- }); err != nil {
316
- log.Error().Err(err).Msg("[HTTP] Failed to encode API error response")
317
- }
318
-}
319
-
320
-func writeSignError(w http.ResponseWriter, status int, message string) {
321
- w.Header().Set("Content-Type", "application/json")
322
- w.WriteHeader(status)
323
- _ = json.NewEncoder(w).Encode(keyless.ErrorResponse{Error: message})
324
-}
325
-
326
-func decodeLeaseID(encoded string) (string, bool) {
327
- idBytes, err := base64.URLEncoding.DecodeString(encoded)
328
- if err != nil {
329
- idBytes, err = base64.RawURLEncoding.DecodeString(encoded)
330
- if err != nil {
331
- return "", false
332
- }
333
- }
334
- return string(idBytes), true
335
-}