ops_ws_handler.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. package admin
  2. import (
  3. "context"
  4. "encoding/json"
  5. "math"
  6. "net"
  7. "net/http"
  8. "net/netip"
  9. "net/url"
  10. "os"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "sync/atomic"
  15. "time"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  17. servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  18. "github.com/Wei-Shaw/sub2api/internal/service"
  19. "github.com/gin-gonic/gin"
  20. "github.com/gorilla/websocket"
  21. )
  22. type OpsWSProxyConfig struct {
  23. TrustProxy bool
  24. TrustedProxies []netip.Prefix
  25. OriginPolicy string
  26. }
  27. const (
  28. envOpsWSTrustProxy = "OPS_WS_TRUST_PROXY"
  29. envOpsWSTrustedProxies = "OPS_WS_TRUSTED_PROXIES"
  30. envOpsWSOriginPolicy = "OPS_WS_ORIGIN_POLICY"
  31. envOpsWSMaxConns = "OPS_WS_MAX_CONNS"
  32. envOpsWSMaxConnsPerIP = "OPS_WS_MAX_CONNS_PER_IP"
  33. )
  34. const (
  35. OriginPolicyStrict = "strict"
  36. OriginPolicyPermissive = "permissive"
  37. )
  38. var opsWSProxyConfig = loadOpsWSProxyConfigFromEnv()
  39. var upgrader = websocket.Upgrader{
  40. CheckOrigin: func(r *http.Request) bool {
  41. return isAllowedOpsWSOrigin(r)
  42. },
  43. // Subprotocol negotiation:
  44. // - The frontend passes ["sub2api-admin", "jwt.<token>"].
  45. // - We always select "sub2api-admin" so the token is never echoed back in the handshake response.
  46. Subprotocols: []string{"sub2api-admin"},
  47. }
  48. const (
  49. qpsWSPushInterval = 2 * time.Second
  50. qpsWSRefreshInterval = 5 * time.Second
  51. qpsWSRequestCountWindow = 1 * time.Minute
  52. defaultMaxWSConns = 100
  53. defaultMaxWSConnsPerIP = 20
  54. )
  55. var wsConnCount atomic.Int32
  56. var wsConnCountByIPMu sync.Mutex
  57. var wsConnCountByIP = make(map[string]int32)
  58. const qpsWSIdleStopDelay = 30 * time.Second
  59. const (
  60. opsWSCloseRealtimeDisabled = 4001
  61. )
  62. var qpsWSIdleStopMu sync.Mutex
  63. var qpsWSIdleStopTimer *time.Timer
  64. func cancelQPSWSIdleStop() {
  65. qpsWSIdleStopMu.Lock()
  66. if qpsWSIdleStopTimer != nil {
  67. qpsWSIdleStopTimer.Stop()
  68. qpsWSIdleStopTimer = nil
  69. }
  70. qpsWSIdleStopMu.Unlock()
  71. }
  72. func scheduleQPSWSIdleStop() {
  73. qpsWSIdleStopMu.Lock()
  74. if qpsWSIdleStopTimer != nil {
  75. qpsWSIdleStopMu.Unlock()
  76. return
  77. }
  78. qpsWSIdleStopTimer = time.AfterFunc(qpsWSIdleStopDelay, func() {
  79. // Only stop if truly idle at fire time.
  80. if wsConnCount.Load() == 0 {
  81. qpsWSCache.Stop()
  82. }
  83. qpsWSIdleStopMu.Lock()
  84. qpsWSIdleStopTimer = nil
  85. qpsWSIdleStopMu.Unlock()
  86. })
  87. qpsWSIdleStopMu.Unlock()
  88. }
  89. type opsWSRuntimeLimits struct {
  90. MaxConns int32
  91. MaxConnsPerIP int32
  92. }
  93. var opsWSLimits = loadOpsWSRuntimeLimitsFromEnv()
  94. const (
  95. qpsWSWriteTimeout = 10 * time.Second
  96. qpsWSPongWait = 60 * time.Second
  97. qpsWSPingInterval = 30 * time.Second
  98. // We don't expect clients to send application messages; we only read to process control frames (Pong/Close).
  99. qpsWSMaxReadBytes = 1024
  100. )
  101. type opsWSQPSCache struct {
  102. refreshInterval time.Duration
  103. requestCountWindow time.Duration
  104. lastUpdatedUnixNano atomic.Int64
  105. payload atomic.Value // []byte
  106. opsService *service.OpsService
  107. cancel context.CancelFunc
  108. done chan struct{}
  109. mu sync.Mutex
  110. running bool
  111. }
  112. var qpsWSCache = &opsWSQPSCache{
  113. refreshInterval: qpsWSRefreshInterval,
  114. requestCountWindow: qpsWSRequestCountWindow,
  115. }
  116. func (c *opsWSQPSCache) start(opsService *service.OpsService) {
  117. if c == nil || opsService == nil {
  118. return
  119. }
  120. for {
  121. c.mu.Lock()
  122. if c.running {
  123. c.mu.Unlock()
  124. return
  125. }
  126. // If a previous refresh loop is currently stopping, wait for it to fully exit.
  127. done := c.done
  128. if done != nil {
  129. c.mu.Unlock()
  130. <-done
  131. c.mu.Lock()
  132. if c.done == done && !c.running {
  133. c.done = nil
  134. }
  135. c.mu.Unlock()
  136. continue
  137. }
  138. c.opsService = opsService
  139. ctx, cancel := context.WithCancel(context.Background())
  140. c.cancel = cancel
  141. c.done = make(chan struct{})
  142. done = c.done
  143. c.running = true
  144. c.mu.Unlock()
  145. go func() {
  146. defer close(done)
  147. c.refreshLoop(ctx)
  148. }()
  149. return
  150. }
  151. }
  152. // Stop stops the background refresh loop.
  153. // It is safe to call multiple times.
  154. func (c *opsWSQPSCache) Stop() {
  155. if c == nil {
  156. return
  157. }
  158. c.mu.Lock()
  159. if !c.running {
  160. done := c.done
  161. c.mu.Unlock()
  162. if done != nil {
  163. <-done
  164. }
  165. return
  166. }
  167. cancel := c.cancel
  168. c.cancel = nil
  169. c.running = false
  170. c.opsService = nil
  171. done := c.done
  172. c.mu.Unlock()
  173. if cancel != nil {
  174. cancel()
  175. }
  176. if done != nil {
  177. <-done
  178. }
  179. c.mu.Lock()
  180. if c.done == done && !c.running {
  181. c.done = nil
  182. }
  183. c.mu.Unlock()
  184. }
  185. func (c *opsWSQPSCache) refreshLoop(ctx context.Context) {
  186. ticker := time.NewTicker(c.refreshInterval)
  187. defer ticker.Stop()
  188. c.refresh(ctx)
  189. for {
  190. select {
  191. case <-ticker.C:
  192. c.refresh(ctx)
  193. case <-ctx.Done():
  194. return
  195. }
  196. }
  197. }
  198. func (c *opsWSQPSCache) refresh(parentCtx context.Context) {
  199. if c == nil {
  200. return
  201. }
  202. c.mu.Lock()
  203. opsService := c.opsService
  204. c.mu.Unlock()
  205. if opsService == nil {
  206. return
  207. }
  208. if parentCtx == nil {
  209. parentCtx = context.Background()
  210. }
  211. ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
  212. defer cancel()
  213. now := time.Now().UTC()
  214. stats, err := opsService.GetWindowStats(ctx, now.Add(-c.requestCountWindow), now)
  215. if err != nil || stats == nil {
  216. if err != nil {
  217. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] refresh: get window stats failed: %v", err)
  218. }
  219. return
  220. }
  221. requestCount := stats.SuccessCount + stats.ErrorCountTotal
  222. qps := 0.0
  223. tps := 0.0
  224. if c.requestCountWindow > 0 {
  225. seconds := c.requestCountWindow.Seconds()
  226. qps = roundTo1DP(float64(requestCount) / seconds)
  227. tps = roundTo1DP(float64(stats.TokenConsumed) / seconds)
  228. }
  229. payload := gin.H{
  230. "type": "qps_update",
  231. "timestamp": now.Format(time.RFC3339),
  232. "data": gin.H{
  233. "qps": qps,
  234. "tps": tps,
  235. "request_count": requestCount,
  236. },
  237. }
  238. msg, err := json.Marshal(payload)
  239. if err != nil {
  240. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] refresh: marshal payload failed: %v", err)
  241. return
  242. }
  243. c.payload.Store(msg)
  244. c.lastUpdatedUnixNano.Store(now.UnixNano())
  245. }
  246. func roundTo1DP(v float64) float64 {
  247. return math.Round(v*10) / 10
  248. }
  249. func (c *opsWSQPSCache) getPayload() []byte {
  250. if c == nil {
  251. return nil
  252. }
  253. if cached, ok := c.payload.Load().([]byte); ok && cached != nil {
  254. return cached
  255. }
  256. return nil
  257. }
  258. func closeWS(conn *websocket.Conn, code int, reason string) {
  259. if conn == nil {
  260. return
  261. }
  262. msg := websocket.FormatCloseMessage(code, reason)
  263. _ = conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(qpsWSWriteTimeout))
  264. _ = conn.Close()
  265. }
  266. // QPSWSHandler handles realtime QPS push via WebSocket.
  267. // GET /api/v1/admin/ops/ws/qps
  268. func (h *OpsHandler) QPSWSHandler(c *gin.Context) {
  269. clientIP := requestClientIP(c.Request)
  270. if h == nil || h.opsService == nil {
  271. c.JSON(http.StatusServiceUnavailable, gin.H{"error": "ops service not initialized"})
  272. return
  273. }
  274. // If realtime monitoring is disabled, prefer a successful WS upgrade followed by a clean close
  275. // with a deterministic close code. This prevents clients from spinning on 404/1006 reconnect loops.
  276. if !h.opsService.IsRealtimeMonitoringEnabled(c.Request.Context()) {
  277. conn, err := upgrader.Upgrade(c.Writer, c.Request, servermiddleware.ServerTimingResponseHeader(c))
  278. if err != nil {
  279. c.JSON(http.StatusNotFound, gin.H{"error": "ops realtime monitoring is disabled"})
  280. return
  281. }
  282. closeWS(conn, opsWSCloseRealtimeDisabled, "realtime_disabled")
  283. return
  284. }
  285. cancelQPSWSIdleStop()
  286. // Lazily start the background refresh loop so unit tests that never hit the
  287. // websocket route don't spawn goroutines that depend on DB/Redis stubs.
  288. qpsWSCache.start(h.opsService)
  289. // Reserve a global slot before upgrading the connection to keep the limit strict.
  290. if !tryAcquireOpsWSTotalSlot(opsWSLimits.MaxConns) {
  291. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] connection limit reached: %d/%d", wsConnCount.Load(), opsWSLimits.MaxConns)
  292. c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
  293. return
  294. }
  295. defer func() {
  296. if wsConnCount.Add(-1) == 0 {
  297. scheduleQPSWSIdleStop()
  298. }
  299. }()
  300. if opsWSLimits.MaxConnsPerIP > 0 && clientIP != "" {
  301. if !tryAcquireOpsWSIPSlot(clientIP, opsWSLimits.MaxConnsPerIP) {
  302. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] per-ip connection limit reached: ip=%s limit=%d", clientIP, opsWSLimits.MaxConnsPerIP)
  303. c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
  304. return
  305. }
  306. defer releaseOpsWSIPSlot(clientIP)
  307. }
  308. conn, err := upgrader.Upgrade(c.Writer, c.Request, servermiddleware.ServerTimingResponseHeader(c))
  309. if err != nil {
  310. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] upgrade failed: %v", err)
  311. return
  312. }
  313. defer func() {
  314. _ = conn.Close()
  315. }()
  316. handleQPSWebSocket(c.Request.Context(), conn)
  317. }
  318. func tryAcquireOpsWSTotalSlot(limit int32) bool {
  319. if limit <= 0 {
  320. return true
  321. }
  322. for {
  323. current := wsConnCount.Load()
  324. if current >= limit {
  325. return false
  326. }
  327. if wsConnCount.CompareAndSwap(current, current+1) {
  328. return true
  329. }
  330. }
  331. }
  332. func tryAcquireOpsWSIPSlot(clientIP string, limit int32) bool {
  333. if strings.TrimSpace(clientIP) == "" || limit <= 0 {
  334. return true
  335. }
  336. wsConnCountByIPMu.Lock()
  337. defer wsConnCountByIPMu.Unlock()
  338. current := wsConnCountByIP[clientIP]
  339. if current >= limit {
  340. return false
  341. }
  342. wsConnCountByIP[clientIP] = current + 1
  343. return true
  344. }
  345. func releaseOpsWSIPSlot(clientIP string) {
  346. if strings.TrimSpace(clientIP) == "" {
  347. return
  348. }
  349. wsConnCountByIPMu.Lock()
  350. defer wsConnCountByIPMu.Unlock()
  351. current, ok := wsConnCountByIP[clientIP]
  352. if !ok {
  353. return
  354. }
  355. if current <= 1 {
  356. delete(wsConnCountByIP, clientIP)
  357. return
  358. }
  359. wsConnCountByIP[clientIP] = current - 1
  360. }
  361. func handleQPSWebSocket(parentCtx context.Context, conn *websocket.Conn) {
  362. if conn == nil {
  363. return
  364. }
  365. ctx, cancel := context.WithCancel(parentCtx)
  366. defer cancel()
  367. var closeOnce sync.Once
  368. closeConn := func() {
  369. closeOnce.Do(func() {
  370. _ = conn.Close()
  371. })
  372. }
  373. closeFrameCh := make(chan []byte, 1)
  374. var wg sync.WaitGroup
  375. wg.Add(1)
  376. go func() {
  377. defer wg.Done()
  378. defer cancel()
  379. conn.SetReadLimit(qpsWSMaxReadBytes)
  380. if err := conn.SetReadDeadline(time.Now().Add(qpsWSPongWait)); err != nil {
  381. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] set read deadline failed: %v", err)
  382. return
  383. }
  384. conn.SetPongHandler(func(string) error {
  385. return conn.SetReadDeadline(time.Now().Add(qpsWSPongWait))
  386. })
  387. conn.SetCloseHandler(func(code int, text string) error {
  388. select {
  389. case closeFrameCh <- websocket.FormatCloseMessage(code, text):
  390. default:
  391. }
  392. cancel()
  393. return nil
  394. })
  395. for {
  396. _, _, err := conn.ReadMessage()
  397. if err != nil {
  398. if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
  399. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] read failed: %v", err)
  400. }
  401. return
  402. }
  403. }
  404. }()
  405. // Push QPS data every 2 seconds (values are globally cached and refreshed at most once per qpsWSRefreshInterval).
  406. pushTicker := time.NewTicker(qpsWSPushInterval)
  407. defer pushTicker.Stop()
  408. // Heartbeat ping every 30 seconds.
  409. pingTicker := time.NewTicker(qpsWSPingInterval)
  410. defer pingTicker.Stop()
  411. writeWithTimeout := func(messageType int, data []byte) error {
  412. if err := conn.SetWriteDeadline(time.Now().Add(qpsWSWriteTimeout)); err != nil {
  413. return err
  414. }
  415. return conn.WriteMessage(messageType, data)
  416. }
  417. sendClose := func(closeFrame []byte) {
  418. if closeFrame == nil {
  419. closeFrame = websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
  420. }
  421. _ = writeWithTimeout(websocket.CloseMessage, closeFrame)
  422. }
  423. for {
  424. select {
  425. case <-pushTicker.C:
  426. msg := qpsWSCache.getPayload()
  427. if msg == nil {
  428. continue
  429. }
  430. if err := writeWithTimeout(websocket.TextMessage, msg); err != nil {
  431. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] write failed: %v", err)
  432. cancel()
  433. closeConn()
  434. wg.Wait()
  435. return
  436. }
  437. case <-pingTicker.C:
  438. if err := writeWithTimeout(websocket.PingMessage, nil); err != nil {
  439. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] ping failed: %v", err)
  440. cancel()
  441. closeConn()
  442. wg.Wait()
  443. return
  444. }
  445. case closeFrame := <-closeFrameCh:
  446. sendClose(closeFrame)
  447. closeConn()
  448. wg.Wait()
  449. return
  450. case <-ctx.Done():
  451. var closeFrame []byte
  452. select {
  453. case closeFrame = <-closeFrameCh:
  454. default:
  455. }
  456. sendClose(closeFrame)
  457. closeConn()
  458. wg.Wait()
  459. return
  460. }
  461. }
  462. }
  463. func isAllowedOpsWSOrigin(r *http.Request) bool {
  464. if r == nil {
  465. return false
  466. }
  467. origin := strings.TrimSpace(r.Header.Get("Origin"))
  468. if origin == "" {
  469. switch strings.ToLower(strings.TrimSpace(opsWSProxyConfig.OriginPolicy)) {
  470. case OriginPolicyStrict:
  471. return false
  472. case OriginPolicyPermissive, "":
  473. return true
  474. default:
  475. return true
  476. }
  477. }
  478. parsed, err := url.Parse(origin)
  479. if err != nil || parsed.Hostname() == "" {
  480. return false
  481. }
  482. originHost := strings.ToLower(parsed.Hostname())
  483. trustProxyHeaders := shouldTrustOpsWSProxyHeaders(r)
  484. reqHost := hostWithoutPort(r.Host)
  485. if trustProxyHeaders {
  486. xfHost := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
  487. if xfHost != "" {
  488. xfHost = strings.TrimSpace(strings.Split(xfHost, ",")[0])
  489. if xfHost != "" {
  490. reqHost = hostWithoutPort(xfHost)
  491. }
  492. }
  493. }
  494. reqHost = strings.ToLower(reqHost)
  495. if reqHost == "" {
  496. return false
  497. }
  498. return originHost == reqHost
  499. }
  500. func shouldTrustOpsWSProxyHeaders(r *http.Request) bool {
  501. if r == nil {
  502. return false
  503. }
  504. if !opsWSProxyConfig.TrustProxy {
  505. return false
  506. }
  507. peerIP, ok := requestPeerIP(r)
  508. if !ok {
  509. return false
  510. }
  511. return isAddrInTrustedProxies(peerIP, opsWSProxyConfig.TrustedProxies)
  512. }
  513. func requestPeerIP(r *http.Request) (netip.Addr, bool) {
  514. if r == nil {
  515. return netip.Addr{}, false
  516. }
  517. host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
  518. if err != nil {
  519. host = strings.TrimSpace(r.RemoteAddr)
  520. }
  521. host = strings.TrimPrefix(host, "[")
  522. host = strings.TrimSuffix(host, "]")
  523. if host == "" {
  524. return netip.Addr{}, false
  525. }
  526. addr, err := netip.ParseAddr(host)
  527. if err != nil {
  528. return netip.Addr{}, false
  529. }
  530. return addr.Unmap(), true
  531. }
  532. func requestClientIP(r *http.Request) string {
  533. if r == nil {
  534. return ""
  535. }
  536. trustProxyHeaders := shouldTrustOpsWSProxyHeaders(r)
  537. if trustProxyHeaders {
  538. xff := strings.TrimSpace(r.Header.Get("X-Forwarded-For"))
  539. if xff != "" {
  540. // Use the left-most entry (original client). If multiple proxies add values, they are comma-separated.
  541. xff = strings.TrimSpace(strings.Split(xff, ",")[0])
  542. xff = strings.TrimPrefix(xff, "[")
  543. xff = strings.TrimSuffix(xff, "]")
  544. if addr, err := netip.ParseAddr(xff); err == nil && addr.IsValid() {
  545. return addr.Unmap().String()
  546. }
  547. }
  548. }
  549. if peer, ok := requestPeerIP(r); ok && peer.IsValid() {
  550. return peer.String()
  551. }
  552. return ""
  553. }
  554. func isAddrInTrustedProxies(addr netip.Addr, trusted []netip.Prefix) bool {
  555. if !addr.IsValid() {
  556. return false
  557. }
  558. for _, p := range trusted {
  559. if p.Contains(addr) {
  560. return true
  561. }
  562. }
  563. return false
  564. }
  565. func loadOpsWSProxyConfigFromEnv() OpsWSProxyConfig {
  566. cfg := OpsWSProxyConfig{
  567. TrustProxy: true,
  568. TrustedProxies: defaultTrustedProxies(),
  569. OriginPolicy: OriginPolicyPermissive,
  570. }
  571. if v := strings.TrimSpace(os.Getenv(envOpsWSTrustProxy)); v != "" {
  572. if parsed, err := strconv.ParseBool(v); err == nil {
  573. cfg.TrustProxy = parsed
  574. } else {
  575. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected bool); using default=%v", envOpsWSTrustProxy, v, cfg.TrustProxy)
  576. }
  577. }
  578. if raw := strings.TrimSpace(os.Getenv(envOpsWSTrustedProxies)); raw != "" {
  579. prefixes, invalid := parseTrustedProxyList(raw)
  580. if len(invalid) > 0 {
  581. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s entries ignored: %s", envOpsWSTrustedProxies, strings.Join(invalid, ", "))
  582. }
  583. cfg.TrustedProxies = prefixes
  584. }
  585. if v := strings.TrimSpace(os.Getenv(envOpsWSOriginPolicy)); v != "" {
  586. normalized := strings.ToLower(v)
  587. switch normalized {
  588. case OriginPolicyStrict, OriginPolicyPermissive:
  589. cfg.OriginPolicy = normalized
  590. default:
  591. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected %q or %q); using default=%q", envOpsWSOriginPolicy, v, OriginPolicyStrict, OriginPolicyPermissive, cfg.OriginPolicy)
  592. }
  593. }
  594. return cfg
  595. }
  596. func loadOpsWSRuntimeLimitsFromEnv() opsWSRuntimeLimits {
  597. cfg := opsWSRuntimeLimits{
  598. MaxConns: defaultMaxWSConns,
  599. MaxConnsPerIP: defaultMaxWSConnsPerIP,
  600. }
  601. if v := strings.TrimSpace(os.Getenv(envOpsWSMaxConns)); v != "" {
  602. if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {
  603. cfg.MaxConns = int32(parsed)
  604. } else {
  605. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected int>0); using default=%d", envOpsWSMaxConns, v, cfg.MaxConns)
  606. }
  607. }
  608. if v := strings.TrimSpace(os.Getenv(envOpsWSMaxConnsPerIP)); v != "" {
  609. if parsed, err := strconv.Atoi(v); err == nil && parsed >= 0 {
  610. cfg.MaxConnsPerIP = int32(parsed)
  611. } else {
  612. logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected int>=0); using default=%d", envOpsWSMaxConnsPerIP, v, cfg.MaxConnsPerIP)
  613. }
  614. }
  615. return cfg
  616. }
  617. func defaultTrustedProxies() []netip.Prefix {
  618. prefixes, _ := parseTrustedProxyList("127.0.0.0/8,::1/128")
  619. return prefixes
  620. }
  621. func parseTrustedProxyList(raw string) (prefixes []netip.Prefix, invalid []string) {
  622. for _, token := range strings.Split(raw, ",") {
  623. item := strings.TrimSpace(token)
  624. if item == "" {
  625. continue
  626. }
  627. var (
  628. p netip.Prefix
  629. err error
  630. )
  631. if strings.Contains(item, "/") {
  632. p, err = netip.ParsePrefix(item)
  633. } else {
  634. var addr netip.Addr
  635. addr, err = netip.ParseAddr(item)
  636. if err == nil {
  637. addr = addr.Unmap()
  638. bits := 128
  639. if addr.Is4() {
  640. bits = 32
  641. }
  642. p = netip.PrefixFrom(addr, bits)
  643. }
  644. }
  645. if err != nil || !p.IsValid() {
  646. invalid = append(invalid, item)
  647. continue
  648. }
  649. prefixes = append(prefixes, p.Masked())
  650. }
  651. return prefixes, invalid
  652. }
  653. func hostWithoutPort(hostport string) string {
  654. hostport = strings.TrimSpace(hostport)
  655. if hostport == "" {
  656. return ""
  657. }
  658. if host, _, err := net.SplitHostPort(hostport); err == nil {
  659. return host
  660. }
  661. if strings.HasPrefix(hostport, "[") && strings.HasSuffix(hostport, "]") {
  662. return strings.Trim(hostport, "[]")
  663. }
  664. parts := strings.Split(hostport, ":")
  665. return parts[0]
  666. }