2026-01-03 04:26:00 +00:00
|
|
|
package controller
|
|
|
|
|
|
|
|
|
|
import (
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
"net"
|
2026-01-03 04:26:00 +00:00
|
|
|
"net/http"
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
"net/url"
|
2026-01-03 04:26:00 +00:00
|
|
|
"strings"
|
|
|
|
|
|
2026-05-10 00:13:42 +00:00
|
|
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/web/service"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/web/session"
|
2026-01-03 04:26:00 +00:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
ws "github.com/gorilla/websocket"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var upgrader = ws.Upgrader{
|
2026-04-19 19:01:00 +00:00
|
|
|
ReadBufferSize: 32768,
|
|
|
|
|
WriteBufferSize: 32768,
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
EnableCompression: true,
|
|
|
|
|
CheckOrigin: checkSameOrigin,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkSameOrigin allows requests with no Origin header (same-origin or non-browser
|
|
|
|
|
// clients) and otherwise requires the Origin hostname to match the request hostname.
|
|
|
|
|
// Comparison is case-insensitive (RFC 7230 §2.7.3) and ignores port differences
|
|
|
|
|
// (the panel often sits behind a reverse proxy on a different port).
|
|
|
|
|
func checkSameOrigin(r *http.Request) bool {
|
|
|
|
|
origin := r.Header.Get("Origin")
|
|
|
|
|
if origin == "" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
u, err := url.Parse(origin)
|
|
|
|
|
if err != nil || u.Hostname() == "" {
|
2026-01-03 04:26:00 +00:00
|
|
|
return false
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
}
|
|
|
|
|
host, _, err := net.SplitHostPort(r.Host)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// IPv6 literals without a port arrive as "[::1]"; net.SplitHostPort
|
|
|
|
|
// fails in that case while url.Hostname() returns the address without
|
|
|
|
|
// brackets. Strip them so same-origin checks pass for bare IPv6 hosts.
|
|
|
|
|
host = r.Host
|
|
|
|
|
if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
|
|
|
|
|
host = host[1 : len(host)-1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return strings.EqualFold(u.Hostname(), host)
|
2026-01-03 04:26:00 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(websocket): split controller into service + thin controller
Move per-connection lifecycle out of the controller and into a new
service.WebSocketService. The controller is now HTTP-layer only:
authenticate, validate origin, upgrade, and hand the connection off.
- web/service/websocket.go (new): owns the read/write pumps, hub
registration, and connection lifetime. Pump constants are prefixed
(wsWriteWait, wsPongWait, wsPingPeriod, wsClientReadLimit) to avoid
collisions in the larger service package namespace.
- web/controller/websocket.go: trimmed to the upgrader, same-origin
check, auth gate, and hand-off to the service.
- web/web.go: wires controller.NewWebSocketController(service.NewWebSocketService(hub)).
The hub package (web/websocket) stays as low-level fan-out
infrastructure. Behavior is unchanged — this is a structural cleanup
to align with the rest of the codebase's controller/service split.
Also includes a small range-int modernization in login_limiter_test.go
that gopls flagged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:00:44 +00:00
|
|
|
// WebSocketController handles the HTTP→WebSocket upgrade for real-time updates.
|
|
|
|
|
// All per-connection lifecycle (pumps, hub registration) lives in
|
|
|
|
|
// service.WebSocketService — this controller is HTTP-layer only.
|
2026-01-03 04:26:00 +00:00
|
|
|
type WebSocketController struct {
|
|
|
|
|
BaseController
|
refactor(websocket): split controller into service + thin controller
Move per-connection lifecycle out of the controller and into a new
service.WebSocketService. The controller is now HTTP-layer only:
authenticate, validate origin, upgrade, and hand the connection off.
- web/service/websocket.go (new): owns the read/write pumps, hub
registration, and connection lifetime. Pump constants are prefixed
(wsWriteWait, wsPongWait, wsPingPeriod, wsClientReadLimit) to avoid
collisions in the larger service package namespace.
- web/controller/websocket.go: trimmed to the upgrader, same-origin
check, auth gate, and hand-off to the service.
- web/web.go: wires controller.NewWebSocketController(service.NewWebSocketService(hub)).
The hub package (web/websocket) stays as low-level fan-out
infrastructure. Behavior is unchanged — this is a structural cleanup
to align with the rest of the codebase's controller/service split.
Also includes a small range-int modernization in login_limiter_test.go
that gopls flagged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:00:44 +00:00
|
|
|
service *service.WebSocketService
|
2026-01-03 04:26:00 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(websocket): split controller into service + thin controller
Move per-connection lifecycle out of the controller and into a new
service.WebSocketService. The controller is now HTTP-layer only:
authenticate, validate origin, upgrade, and hand the connection off.
- web/service/websocket.go (new): owns the read/write pumps, hub
registration, and connection lifetime. Pump constants are prefixed
(wsWriteWait, wsPongWait, wsPingPeriod, wsClientReadLimit) to avoid
collisions in the larger service package namespace.
- web/controller/websocket.go: trimmed to the upgrader, same-origin
check, auth gate, and hand-off to the service.
- web/web.go: wires controller.NewWebSocketController(service.NewWebSocketService(hub)).
The hub package (web/websocket) stays as low-level fan-out
infrastructure. Behavior is unchanged — this is a structural cleanup
to align with the rest of the codebase's controller/service split.
Also includes a small range-int modernization in login_limiter_test.go
that gopls flagged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:00:44 +00:00
|
|
|
// NewWebSocketController creates a controller wired to the given service.
|
|
|
|
|
func NewWebSocketController(svc *service.WebSocketService) *WebSocketController {
|
|
|
|
|
return &WebSocketController{service: svc}
|
2026-01-03 04:26:00 +00:00
|
|
|
}
|
|
|
|
|
|
refactor(websocket): split controller into service + thin controller
Move per-connection lifecycle out of the controller and into a new
service.WebSocketService. The controller is now HTTP-layer only:
authenticate, validate origin, upgrade, and hand the connection off.
- web/service/websocket.go (new): owns the read/write pumps, hub
registration, and connection lifetime. Pump constants are prefixed
(wsWriteWait, wsPongWait, wsPingPeriod, wsClientReadLimit) to avoid
collisions in the larger service package namespace.
- web/controller/websocket.go: trimmed to the upgrader, same-origin
check, auth gate, and hand-off to the service.
- web/web.go: wires controller.NewWebSocketController(service.NewWebSocketService(hub)).
The hub package (web/websocket) stays as low-level fan-out
infrastructure. Behavior is unchanged — this is a structural cleanup
to align with the rest of the codebase's controller/service split.
Also includes a small range-int modernization in login_limiter_test.go
that gopls flagged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:00:44 +00:00
|
|
|
// HandleWebSocket authenticates the request, upgrades the HTTP connection, and
|
|
|
|
|
// hands ownership of the connection off to the service.
|
2026-01-03 04:26:00 +00:00
|
|
|
func (w *WebSocketController) HandleWebSocket(c *gin.Context) {
|
|
|
|
|
if !session.IsLogin(c) {
|
|
|
|
|
logger.Warningf("Unauthorized WebSocket connection attempt from %s", getRemoteIp(c))
|
|
|
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("Failed to upgrade WebSocket connection:", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
refactor(websocket): split controller into service + thin controller
Move per-connection lifecycle out of the controller and into a new
service.WebSocketService. The controller is now HTTP-layer only:
authenticate, validate origin, upgrade, and hand the connection off.
- web/service/websocket.go (new): owns the read/write pumps, hub
registration, and connection lifetime. Pump constants are prefixed
(wsWriteWait, wsPongWait, wsPingPeriod, wsClientReadLimit) to avoid
collisions in the larger service package namespace.
- web/controller/websocket.go: trimmed to the upgrader, same-origin
check, auth gate, and hand-off to the service.
- web/web.go: wires controller.NewWebSocketController(service.NewWebSocketService(hub)).
The hub package (web/websocket) stays as low-level fan-out
infrastructure. Behavior is unchanged — this is a structural cleanup
to align with the rest of the codebase's controller/service split.
Also includes a small range-int modernization in login_limiter_test.go
that gopls flagged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 22:00:44 +00:00
|
|
|
w.service.HandleConnection(conn, getRemoteIp(c))
|
2026-01-03 04:26:00 +00:00
|
|
|
}
|