aboutsummaryrefslogtreecommitdiff
path: root/internal/net/server.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/net/server.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/net/server.go')
-rw-r--r--internal/net/server.go78
1 files changed, 78 insertions, 0 deletions
diff --git a/internal/net/server.go b/internal/net/server.go
index 9d99acc..05a98d1 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -168,6 +168,14 @@ type Server struct {
// stopping flips when Shutdown begins so listener goroutines treat
// their subsequent Accept/serve errors as expected, not fatal.
stopping atomic.Bool
+ // conns counts live sessions across every transport. handleSession
+ // admits against maxConnections so a pre-auth connection flood cannot
+ // hold an unbounded number of reader goroutines, buffers, and outboxes.
+ conns atomic.Int64
+ // idleTimeout returns the read timeout armed before each message. It is a
+ // field so tests can drive the idle-drop path without waiting out the
+ // production windows; nil means sessionIdleTimeout.
+ idleTimeout func(*Session) time.Duration
}
type Hub struct {
@@ -187,6 +195,64 @@ const (
closeFlushTimeout = 2 * time.Second
)
+// Connection admission and idle-read limits. The listener admits at most
+// maxConnections live sessions; each read arms an idle deadline (short before
+// authentication, generous once the session has an account) that every
+// message resets, so a silent client cannot pin a reader goroutine forever
+// while legitimate long sessions keep working.
+const (
+ maxConnections = 1000
+ loginReadTimeout = 2 * time.Minute
+ idleReadTimeout = 30 * time.Minute
+)
+
+// admitConnection reserves a connection slot, returning false when the server
+// is at capacity. Every admitted connection must call releaseConnection.
+func (s *Server) admitConnection() bool {
+ if s.conns.Add(1) > maxConnections {
+ s.conns.Add(-1)
+ return false
+ }
+ return true
+}
+
+func (s *Server) releaseConnection() {
+ s.conns.Add(-1)
+}
+
+// sessionIdleTimeout returns the read timeout to arm for this session.
+func (s *Server) sessionIdleTimeout(sess *Session) time.Duration {
+ if s.idleTimeout != nil {
+ return s.idleTimeout(sess)
+ }
+ return sessionIdleTimeout(sess)
+}
+
+// sessionIdleTimeout returns the read timeout for a session: the account-name
+// and password prompts get a short window (pre-auth sockets are the ones an
+// anonymous flood targets), everything else gets a generous idle timeout.
+func sessionIdleTimeout(sess *Session) time.Duration {
+ switch sess.State() {
+ case StateAccountName, StatePassword:
+ return loginReadTimeout
+ default:
+ return idleReadTimeout
+ }
+}
+
+// armReadDeadline sets an idle read deadline on the session's transport when
+// it supports one. tcpConn and wsConn (both in this package) expose their
+// underlying connection, so telnet, telnet-TLS, and websocket are all covered.
+func armReadDeadline(sess *Session, d time.Duration) {
+ deadline := time.Now().Add(d)
+ switch c := sess.Conn.(type) {
+ case *tcpConn:
+ _ = c.conn.SetReadDeadline(deadline)
+ case *wsConn:
+ _ = c.conn.SetReadDeadline(deadline)
+ }
+}
+
// StartWriter puts the session into queued mode: writes are enqueued and a
// dedicated goroutine drains the queue to the network with per-message write
// deadlines. The goroutine exits after Close() — draining whatever was
@@ -705,12 +771,24 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) {
sess.Close()
}()
+ // Admit before reading anything: an over-capacity connection (including
+ // one arriving through the websocket transport, whose handler shares this
+ // path) is told so and dropped without holding a read loop.
+ if !s.admitConnection() {
+ sess.Write("Server is at full capacity. Please try again later.\r\n")
+ return
+ }
+ defer s.releaseConnection()
+
sess.Write("\033[2J\033[H")
sess.Write(WelcomeBanner())
sess.Write("What's your account name? ")
for {
+ // Re-armed every iteration so any message from the client resets the
+ // idle countdown.
+ armReadDeadline(sess, s.sessionIdleTimeout(sess))
line, err := sess.Conn.ReadMessage()
if err != nil {
log.Printf("session read error: %v", err)