refactor: split utils.go into ws.go, http.go, and url.go for SRP

sinwoojin committed Dec 16, 2025 at 10:22 UTC 151333c480ce5fd0e21a6ee06c7f4fbc58c1d5c4
3 files changed +123 -113
utils/http.go new
+81
@@ -0,0 +1,81 @@
1 +package utils
2 +
3 +import (
4 + "mime"
5 + "net"
6 + "net/http"
7 + "strings"
8 +)
9 +
10 +// IsHTMLContentType checks if the Content-Type header indicates HTML content
11 +// It properly handles media type parsing with parameters like charset
12 +func IsHTMLContentType(contentType string) bool {
13 + if contentType == "" {
14 + return false
15 + }
16 + mediaType, _, err := mime.ParseMediaType(contentType)
17 + if err != nil {
18 + return strings.HasPrefix(strings.ToLower(contentType), "text/html")
19 + }
20 + return mediaType == "text/html"
21 +}
22 +
23 +// GetContentType returns the MIME type for a file extension
24 +func GetContentType(ext string) string {
25 + switch ext {
26 + case ".html":
27 + return "text/html; charset=utf-8"
28 + case ".js":
29 + return "application/javascript"
30 + case ".json":
31 + return "application/json"
32 + case ".wasm":
33 + return "application/wasm"
34 + case ".css":
35 + return "text/css"
36 + case ".mp4":
37 + return "video/mp4"
38 + case ".svg":
39 + return "image/svg+xml"
40 + case ".png":
41 + return "image/png"
42 + case ".ico":
43 + return "image/x-icon"
44 + default:
45 + return ""
46 + }
47 +}
48 +
49 +// SetCORSHeaders sets permissive CORS headers for GET/OPTIONS and common headers
50 +func SetCORSHeaders(w http.ResponseWriter) {
51 + w.Header().Set("Access-Control-Allow-Origin", "*")
52 + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
53 + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
54 +}
55 +
56 +func IsLocalhost(r *http.Request) bool {
57 + host := r.RemoteAddr
58 + if h, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
59 + host = h
60 + }
61 +
62 + // If a proxy/adapter reports a hostname, allow Docker Desktop host alias.
63 + if strings.EqualFold(host, "host.docker.internal") {
64 + return true
65 + }
66 +
67 + ip := net.ParseIP(host)
68 + if ip == nil {
69 + // Try resolving hostnames to IPs (best-effort).
70 + if addrs, err := net.LookupIP(host); err == nil {
71 + for _, a := range addrs {
72 + if a.IsLoopback() || a.IsPrivate() {
73 + return true
74 + }
75 + }
76 + }
77 + return false
78 + }
79 +
80 + return ip.IsLoopback() || ip.IsPrivate()
81 +}
utils/url.go renamed
-113
@@ -1,52 +1,12 @@
1 package utils
2
3 import (
4 - "context"
4 "fmt"
6 - "io"
7 - "mime"
8 - "net"
9 - "net/http"
5 "net/url"
6 "regexp"
7 "strings"
13 -
14 - "github.com/gorilla/websocket"
15 -
16 - "gosuda.org/portal/portal/utils/wsstream"
8 )
9
19 -// NewWebSocketDialer returns a dialer that establishes WebSocket connections
20 -// and wraps them as io.ReadWriteCloser.
21 -func NewWebSocketDialer() func(context.Context, string) (io.ReadWriteCloser, error) {
22 - return func(ctx context.Context, url string) (io.ReadWriteCloser, error) {
23 - wsConn, _, err := websocket.DefaultDialer.Dial(url, nil)
24 - if err != nil {
25 - return nil, err
26 - }
27 - return &wsstream.WsStream{Conn: wsConn}, nil
28 - }
29 -}
30 -
31 -// defaultWebSocketUpgrader provides a permissive upgrader used across cmd binaries
32 -var defaultWebSocketUpgrader = websocket.Upgrader{
33 - CheckOrigin: func(r *http.Request) bool { return true },
34 -}
35 -
36 -// UpgradeWebSocket upgrades the request/response to a WebSocket connection using DefaultWebSocketUpgrader
37 -func UpgradeWebSocket(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, error) {
38 - return defaultWebSocketUpgrader.Upgrade(w, r, responseHeader)
39 -}
40 -
41 -// UpgradeToWSStream upgrades HTTP to WebSocket and wraps it as io.ReadWriteCloser
42 -func UpgradeToWSStream(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (io.ReadWriteCloser, *websocket.Conn, error) {
43 - wsConn, err := UpgradeWebSocket(w, r, responseHeader)
44 - if err != nil {
45 - return nil, nil, err
46 - }
47 - return &wsstream.WsStream{Conn: wsConn}, wsConn, nil
48 -}
49 -
10 // URL-safe name validation regex
11 var urlSafeNameRegex = regexp.MustCompile(`^[\p{L}\p{N}_-]+$`)
12
@@ -129,45 +89,6 @@ func ParseURLs(raw string) []string {
89 return out
90 }
91
132 -// IsHTMLContentType checks if the Content-Type header indicates HTML content
133 -// It properly handles media type parsing with parameters like charset
134 -func IsHTMLContentType(contentType string) bool {
135 - if contentType == "" {
136 - return false
137 - }
138 - mediaType, _, err := mime.ParseMediaType(contentType)
139 - if err != nil {
140 - return strings.HasPrefix(strings.ToLower(contentType), "text/html")
141 - }
142 - return mediaType == "text/html"
143 -}
144 -
145 -// GetContentType returns the MIME type for a file extension
146 -func GetContentType(ext string) string {
147 - switch ext {
148 - case ".html":
149 - return "text/html; charset=utf-8"
150 - case ".js":
151 - return "application/javascript"
152 - case ".json":
153 - return "application/json"
154 - case ".wasm":
155 - return "application/wasm"
156 - case ".css":
157 - return "text/css"
158 - case ".mp4":
159 - return "video/mp4"
160 - case ".svg":
161 - return "image/svg+xml"
162 - case ".png":
163 - return "image/png"
164 - case ".ico":
165 - return "image/x-icon"
166 - default:
167 - return ""
168 - }
169 -}
170 -
92 // IsHexString reports whether s contains only hexadecimal characters
93 func IsHexString(s string) bool {
94 for _, c := range s {
@@ -178,13 +99,6 @@ func IsHexString(s string) bool {
99 return true
100 }
101
181 -// SetCORSHeaders sets permissive CORS headers for GET/OPTIONS and common headers
182 -func SetCORSHeaders(w http.ResponseWriter) {
183 - w.Header().Set("Access-Control-Allow-Origin", "*")
184 - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
185 - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
186 -}
187 -
102 // IsSubdomain reports whether host matches the given domain pattern.
103 // Supports patterns like:
104 // - "*.example.com" (wildcard for any subdomain of example.com)
@@ -290,30 +204,3 @@ func DefaultBootstrapFrom(base string) string {
204 }
205 return "ws://" + host + "/relay"
206 }
293 -
294 -func IsLocalhost(r *http.Request) bool {
295 - host := r.RemoteAddr
296 - if h, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
297 - host = h
298 - }
299 -
300 - // If a proxy/adapter reports a hostname, allow Docker Desktop host alias.
301 - if strings.EqualFold(host, "host.docker.internal") {
302 - return true
303 - }
304 -
305 - ip := net.ParseIP(host)
306 - if ip == nil {
307 - // Try resolving hostnames to IPs (best-effort).
308 - if addrs, err := net.LookupIP(host); err == nil {
309 - for _, a := range addrs {
310 - if a.IsLoopback() || a.IsPrivate() {
311 - return true
312 - }
313 - }
314 - }
315 - return false
316 - }
317 -
318 - return ip.IsLoopback() || ip.IsPrivate()
319 -}
utils/ws.go new
+42
@@ -0,0 +1,42 @@
1 +package utils
2 +
3 +import (
4 + "context"
5 + "io"
6 + "net/http"
7 +
8 + "github.com/gorilla/websocket"
9 +
10 + "gosuda.org/portal/portal/utils/wsstream"
11 +)
12 +
13 +// NewWebSocketDialer returns a dialer that establishes WebSocket connections
14 +// and wraps them as io.ReadWriteCloser.
15 +func NewWebSocketDialer() func(context.Context, string) (io.ReadWriteCloser, error) {
16 + return func(ctx context.Context, url string) (io.ReadWriteCloser, error) {
17 + wsConn, _, err := websocket.DefaultDialer.Dial(url, nil)
18 + if err != nil {
19 + return nil, err
20 + }
21 + return &wsstream.WsStream{Conn: wsConn}, nil
22 + }
23 +}
24 +
25 +// defaultWebSocketUpgrader provides a permissive upgrader used across cmd binaries
26 +var defaultWebSocketUpgrader = websocket.Upgrader{
27 + CheckOrigin: func(r *http.Request) bool { return true },
28 +}
29 +
30 +// UpgradeWebSocket upgrades the request/response to a WebSocket connection using DefaultWebSocketUpgrader
31 +func UpgradeWebSocket(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, error) {
32 + return defaultWebSocketUpgrader.Upgrade(w, r, responseHeader)
33 +}
34 +
35 +// UpgradeToWSStream upgrades HTTP to WebSocket and wraps it as io.ReadWriteCloser
36 +func UpgradeToWSStream(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (io.ReadWriteCloser, *websocket.Conn, error) {
37 + wsConn, err := UpgradeWebSocket(w, r, responseHeader)
38 + if err != nil {
39 + return nil, nil, err
40 + }
41 + return &wsstream.WsStream{Conn: wsConn}, wsConn, nil
42 +}