aboutsummaryrefslogtreecommitdiff
path: root/internal/net
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 13:56:10 -0400
committerhistoria <[not public]>2026-09-15 13:56:48 -0400
commitb3568f03e4103dd46db2ccc680fbab0a6454d604 (patch)
treedb3f6bb4f3d7c1deefbcd50de0f5022756d6b9ae /internal/net
parentf4ea40c4b3589104dfaced3481ba19c5735889ae (diff)
downloadthehouseoficarus-b3568f03e4103dd46db2ccc680fbab0a6454d604.tar.gz
fix: telnet buffer reuse, bank stack handling,
transport casino gate, and admin cache invalidation
Diffstat (limited to 'internal/net')
-rw-r--r--internal/net/conn.go37
-rw-r--r--internal/net/conn_bound_test.go42
-rw-r--r--internal/net/server.go2
-rw-r--r--internal/net/wsconn.go3
4 files changed, 77 insertions, 7 deletions
diff --git a/internal/net/conn.go b/internal/net/conn.go
index e46031b..2fff0bc 100644
--- a/internal/net/conn.go
+++ b/internal/net/conn.go
@@ -24,21 +24,46 @@ type tcpConn struct {
func newTCPConn(conn net.Conn) *tcpConn {
return &tcpConn{
conn: conn,
- reader: bufio.NewReader(conn),
+ reader: bufio.NewReaderSize(conn, maxLineLength*4),
}
}
+// maxLineLength bounds how much of an incoming line is accepted. Longer
+// lines are drained from the socket and truncated so a client cannot grow
+// the read buffer without limit.
+const maxLineLength = 1024
+
func (t *tcpConn) ReadMessage() (string, error) {
- line, err := t.reader.ReadString('\n')
- if err != nil {
+ line, err := t.reader.ReadSlice('\n')
+ if err == bufio.ErrBufferFull {
+ // Copy the bounded prefix out of the reusable read buffer BEFORE
+ // draining the rest of the oversized line; further reads overwrite the
+ // buffer the slice points into.
+ prefixLen := len(line)
+ if prefixLen > maxLineLength {
+ prefixLen = maxLineLength
+ }
+ prefix := make([]byte, prefixLen)
+ copy(prefix, line[:prefixLen])
+ for {
+ _, err = t.reader.ReadSlice('\n')
+ if err == nil {
+ break
+ }
+ if err != bufio.ErrBufferFull {
+ return "", err
+ }
+ }
+ line = prefix
+ } else if err != nil {
return "", err
}
- line = strings.TrimRight(line, "\r\n")
- line = stripIAC(line)
+ text := strings.TrimRight(string(line), "\r\n")
+ text = stripIAC(text)
if t.echoOff {
t.conn.Write([]byte("\r\n"))
}
- return line, nil
+ return text, nil
}
func (t *tcpConn) Write(b []byte) (int, error) {
diff --git a/internal/net/conn_bound_test.go b/internal/net/conn_bound_test.go
new file mode 100644
index 0000000..45b0f90
--- /dev/null
+++ b/internal/net/conn_bound_test.go
@@ -0,0 +1,42 @@
+package net
+
+import (
+ "bufio"
+ "strings"
+ "testing"
+)
+
+// Regression: the oversized-line handler drains the rest of the line before
+// returning. ReadSlice returns a slice into a reusable buffer, so the
+// truncated prefix must be copied out before draining or it gets overwritten
+// by the remainder of the client's input.
+func TestOversizedLineKeepsTruncatedPrefix(t *testing.T) {
+ oversized := strings.Repeat("A", maxLineLength*2) + strings.Repeat("Z", maxLineLength*3) + "\n"
+ c := &tcpConn{reader: bufio.NewReaderSize(strings.NewReader(oversized+"look\n"), maxLineLength*4)}
+
+ got, err := c.ReadMessage()
+ if err != nil {
+ t.Fatalf("oversized line produced an error: %v", err)
+ }
+ if got != strings.Repeat("A", maxLineLength) {
+ t.Fatalf("truncated prefix corrupted by buffer reuse: got %q...", got[:20])
+ }
+
+ next, err := c.ReadMessage()
+ if err != nil {
+ t.Fatalf("reader did not recover after an oversized line: %v", err)
+ }
+ if next != "look" {
+ t.Fatalf("subsequent line lost: got %q", next)
+ }
+}
+
+func TestNormalLinesUnaffected(t *testing.T) {
+ c := &tcpConn{reader: bufio.NewReaderSize(strings.NewReader("look\nsay hi\r\n"), maxLineLength*4)}
+ for _, want := range []string{"look", "say hi"} {
+ got, err := c.ReadMessage()
+ if err != nil || got != want {
+ t.Fatalf("ReadMessage()=(%q,%v), want (%q,nil)", got, err, want)
+ }
+ }
+}
diff --git a/internal/net/server.go b/internal/net/server.go
index da1c01b..9b5be18 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -135,7 +135,7 @@ func (h *Hub) Add(s *Session) {
func (h *Hub) Remove(s *Session) {
h.mu.Lock()
- if s.Player != nil && s.State == StateGame && !s.Disconnecting {
+ if s.Player != nil && !s.Disconnecting {
s.Disconnecting = true
s.DisconnectTicks = 10
onDisconnect := h.onDisconnect
diff --git a/internal/net/wsconn.go b/internal/net/wsconn.go
index d6f45dc..4990c0b 100644
--- a/internal/net/wsconn.go
+++ b/internal/net/wsconn.go
@@ -9,6 +9,9 @@ type wsConn struct {
}
func newWSConn(conn *websocket.Conn) *wsConn {
+ // Bound incoming message size so a client cannot push arbitrarily large
+ // frames into memory before the session's truncation logic runs.
+ conn.SetReadLimit(maxLineLength * 4)
return &wsConn{conn: conn}
}