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