Commit graph

23 commits

Author SHA1 Message Date
Stephen Dennis
52f8c08eb9 fix(hydra): Pass 10 gRPC residual — caps, encoding, PID (#1265–#1269)
#1265: Bound WorkQueue to MAX_PENDING=1024 (blocking enqueue) and wait on
futures in GameSession reader so a stream cannot flood the main loop.

#1266: Cap concurrent subscribers per session (MAX_SUBSCRIBERS=8); reject
GameSession/Subscribe/SubscribeGmcp/WS with RESOURCE_EXHAUSTED / close.

#1267: grpc-web SendInput routes through TelnetBridge::convertInput like
native gRPC and WS GameSession.

#1268: Reject gRPC/WS/grpc-web input lines above MAX_INPUT_LINE_LENGTH
(8192, shared with front-door telnet assembly); drop oversized GMCP.

#1269: GetGameStatus returns host PIDs only for admin accounts; any
authenticated session still sees running/up.

#1270: proxy_regression covers subscriber cap, line-limit constants,
work-queue cap constant, and convertInput non-UTF8 target.
2026-07-25 19:29:20 -06:00
Stephen Dennis
cb4c101125 fix: resolve Pass 4 Hydra Medium audit defects #1095–#1102
- #1095: WS RSV + control FIN/len≤125 before ext-len (no large PING→PONG)
- #1096: cap writeBuffers_ (256 KiB, close slow FD); subscriber queues drop oldest at 256
- #1097: gRPC/grpc-web login lockout by peer IP; /passwd requires old password
- #1098: RAND_bytes hard-fail; password/scrollback salts via CSPRNG
- #1099: ProcessManager::stopAndWait with SIGKILL escalate; restart + dtor wait/reap
- #1100: scrollback_lines applied on session create/restore; max_sessions_per_account enforced
- #1101: telnet IAC SB reassembly capped at 64 KiB
- #1102: /metrics loopback-only; ListGames/GetGameStatus require session

proxy_regression: ok (RSV, large PING, SB cap + prior WS cases)
hydra: builds with GRPC=1
2026-07-24 18:22:17 -06:00
Stephen Dennis
1f73fbb8bf Propagate Hydra end-of-record boundaries 2026-03-29 02:12:03 -06:00
Stephen Dennis
1baa843572 Fix Hydra stream translation boundaries 2026-03-29 01:43:55 -06:00
Stephen Dennis
fdeaf228d4 Consolidate UTF-8 helpers into utf8_utils.h
Move issueTypeName() and sanitizeProtoTextForLog() from duplicated
static functions in session_manager.cpp and grpc_server.cpp into
utf8_utils.h. Remove unused parameter from truncated lambda.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:28:02 +00:00
Stephen Dennis
51df4b3322 Fix Hydra UTF-8 output handling and diagnostics 2026-03-28 18:19:56 -06:00
Stephen Dennis
01830e3082 Fix LBUF_SIZE discrepancy, rate limit pruning, insecure gRPC binding
H-8: LBUF_SIZE discrepancy (8000 vs 32768)
- Override color_ops.h fallback to match engine's LBUF_SIZE=32768
- TelnetBridge: all three methods (ingestGameOutput, renderForClient,
  charsetEncodeFromUtf8) now use heap-allocated buffers scaled to input
  size, with LBUF_SIZE as minimum capacity

M-10: IP-based rate limit bypass via pruning
- Pruning now removes expired accountCreateTimes entries first, then
  only erases the IP tracker if accountCreateTimes is also empty.
  Previously, pruning cleared the entire entry, resetting the 1-hour
  account creation window.

L-6: gRPC insecure listener restricted to loopback
- Without TLS cert/key, GrpcServer::start() now rejects non-loopback
  bind addresses (must be 127.0.0.1, [::1], or localhost).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:26:49 -06:00
Stephen Dennis
eae87e864f Fix token rotation cascade, gRPC rate limit, TLS fail-fast
Fixes from code review of the previous two commits:

1. High: Token rotation no longer destroys scrollback/link state.
   Added dbPersistId to track the SQLite row's primary key separately
   from the in-memory API token. Rotation only changes the in-memory
   persistId; the database row (and its CASCADE-linked scrollback) is
   untouched. flushSession and deleteSession use dbPersistId for all
   SQLite operations.

2. Medium: Account creation rate limit now covers gRPC native
   (extracts IP from ServerContext::peer()) and grpc-web (uses
   FrontDoorState::clientIp). createAccountAndGetSession takes a
   clientIp parameter; checkAccountCreateRate/recordAccountCreate
   moved to public interface.

3. Medium: Partial gRPC TLS config (one of cert/key set, other
   missing) now fails at config parse time. If TLS is configured
   and startup fails, Hydra exits rather than silently falling back
   to insecure credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:03:11 -06:00
Stephen Dennis
6a449179c5 Hydra proxy: session tokens, gRPC TLS, remaining review fixes
High:
- H-4: Session token TTL (session_token_ttl, default 24h) and rotation
  on re-authentication. Expired tokens rejected in findByPersistId.
- H-5: gRPC TLS support via grpc_tls_cert/grpc_tls_key config options.
  Warning logged when using insecure credentials.

Medium:
- M-1: Replace deprecated getpass() with termios echo-disable on POSIX
- M-3: O(1) findByPersistId via unordered_map persistIdIndex_
- M-4: Account creation rate-limited to 2/hour per IP
- M-6: strerror_r portability guard for GNU vs XSI semantics
- M-7: GMCP cache capped at 64 packages per session
- M-8: OutputItem::render uses heap buffer (4x input + 256) instead of
  fixed 8000-byte stack buffer

Low:
- L-3: SIGHUP log rotation via logReopen() (for logrotate integration)
- L-4: Remove dead stub files front_door.cpp/h, back_door.cpp/h
- L-5: Adaptive event loop poll: 10ms when active, ramps to 100ms idle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:51:37 -06:00
Stephen Dennis
b42aae8fb5 Harden Hydra proxy for internet-facing deployment
Security fixes from code review (ISSUES.md):
- TLS policy: allow_plaintext (default no), credential commands always
  require TLS, tls_required per game block (default yes)
- Admin authorization on /start /stop /restart and gRPC process RPCs
- CORS wildcard replaced with configurable cors_origin list
- Unbounded buffer DoS: cap lineBuf (8KB), httpBuf (1MB), WS fragBuf
- Constant-time password comparison via CRYPTO_memcmp
- PBKDF2 key derivation uniformly (replaces lossy crypt_r on POSIX)
- Session idle/detached timeout reaping in runTimers
- IP tracker map periodic pruning
- Content-Length stoul wrapped in try/catch
- ListenConfig bools initialized

Bug fixes:
- handleGrpcWebRequest used undeclared 'handle' (should be fd.handle)
- setFrontDoorTls called before front-door entry existed in map

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:41:51 -06:00
Stephen Dennis
4d18e7bc7f Add GMCP state cache with replay on client attach
Hydra now caches the last GMCP payload per package (e.g. Char.Vitals,
Room.Info) in HydraSession::gmcpCache. When a new front-door or gRPC
subscriber attaches to an existing session, the cached GMCP state is
replayed so the client's vitals bar, room info, etc. aren't blank
after reconnect.

- session_manager.h: gmcpCache field (map<string, string>) on HydraSession,
  replayGmcpCache() method declaration
- session_manager.cpp: cache updated on every incoming GMCP from game,
  replayed to telnet front-doors in showGameMenu(), replayed to gRPC
  subscribers via replayGmcpCache()
- grpc_server.cpp: GameSession handler calls replayGmcpCache() after
  subscriber registration via work queue

Core.KeepAlive synthesis was already implemented by the Ubuntu agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:15:37 -06:00
Stephen Dennis
d2c762523f Fix SetPreferences overwrite, GetScrollBack color, terminal_type
Three remaining review findings:

1. ColorFormat enum: added COLOR_UNSPECIFIED = 0 as proto3 default.
   Existing values shifted to 1-5.  SetPreferences handler skips
   color update when COLOR_UNSPECIFIED (resize-only updates no longer
   reset color to TrueColor).  Subscribe/GetScrollBack treat 0 as
   "server default (TrueColor)".

2. GetScrollBack now renders PUA text at the requested color_format
   using OutputItem::render() during replay.

3. terminal_type from SetPreferences is stored in HydraSession for
   future TTYPE forwarding to back-door games.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:01:14 -06:00
Stephen Dennis
ca97aa48e8 Fix five review findings: stream race, IAC escape, O(N) memory, strerror
1. GameSession concurrent Write race: pong replies are now queued
   through the subscriber's output queue instead of written directly
   from the reader thread.  renderFormat updates are protected by
   the output-queue mutex.

2. IAC escaping in telnet_utils.h: buildGmcpFrame() and buildNawsFrame()
   now escape 0xFF payload bytes as IAC IAC per RFC 854.

3. O(N) scrollback memory check replaced with atomic global counter.
   SessionManager::globalScrollbackBytes_ is updated incrementally on
   each append (O(1)) instead of scanning all sessions.

4. strerror() replaced with strerror_r() in safeWrite for thread safety.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:51:27 -06:00
Stephen Dennis
9033f3dac0 Add terminal size to Subscribe for legacy/grpc-web clients
SessionRequest now carries terminal_width and terminal_height fields
(proto fields 3-4).  When a Subscribe RPC provides non-zero values,
the server forwards NAWS to the active game back-door link so the
game renders at the correct width.

Implemented in both native gRPC (grpc_server.cpp) and grpc-web
(session_manager.cpp handleGrpcWebRequest) Subscribe handlers.
HTML5 client sends approximate terminal dimensions based on window
size in _startSubscribe().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:15:48 -06:00
Stephen Dennis
bc01d39b9c Centralize GMCP and NAWS frame builders in telnet_utils.h
New shared header proxy/telnet_utils.h provides:
- telnet:: namespace with IAC, SB, SE, WILL, DO, GMCP, NAWS constants
- buildGmcpFrame(payload) — IAC SB GMCP payload IAC SE
- buildNawsFrame(width, height) — IAC SB NAWS w h IAC SE

Eliminates duplicate manual frame construction in grpc_server.cpp
(GMCP 6 lines → 1, NAWS 10 lines → 1) and replaces the static
helper in session_manager.cpp.  Local T_* constants now alias the
shared telnet:: namespace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 18:04:42 -06:00
Stephen Dennis
3dd350fafd Replace all raw send() with GANL postWrite via safeWrite helper
Eliminate 19 raw send() calls across session_manager.cpp and
grpc_server.cpp.  All writes now go through engine_.postWrite()
via a safeWrite() helper method, which handles:
- Non-blocking I/O (GANL buffers if socket not ready)
- Partial write completion
- Thread-safe write ordering through GANL's write queue

ReplayContext extended with engine pointer so scroll-back replay
callbacks can also use the safe write path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:59:47 -06:00
Stephen Dennis
6853a2e7e2 Add ColorFormat negotiation via SetPreferences on GameSession stream
Proto changes:
- Add SetPreferences message to ClientMessage oneof with color_format,
  terminal_width, terminal_height, terminal_type fields
- Add color_format to SessionRequest for legacy Subscribe RPC
- Remove dead GameSessionRequest message (replaced by SetPreferences)

Server changes:
- GameSession reader handles SetPreferences: updates subscriber's
  RenderFormat and forwards NAWS to the game back-door link
- Subscribe RPC passes color_format from SessionRequest to subscriber

Client changes (TitanFugue):
- Sends SetPreferences as first message on GameSession stream
- Overrides send_naws() to forward terminal resize as SetPreferences
  update on the bidi stream

This resolves the "ColorFormat is dead API surface" issue — clients
can now negotiate color depth and report terminal size over gRPC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:39:16 -06:00
Stephen Dennis
634ce31e94 Defer color rendering to per-subscriber read time
OutputQueue now stores PUA-encoded UTF-8 instead of pre-rendered
TrueColor ANSI.  Each SubscriberQueue has a RenderFormat preference
(TrueColor/256/16/PUA/Plain) and OutputItem::render() converts at
read time via the appropriate co_render_* function.

This enables different gRPC clients on the same Hydra session to
receive different color formats — e.g., a terminal client getting
TrueColor while a bot gets PLAIN text.  Previously all subscribers
received the same pre-rendered TrueColor regardless of capability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:33:10 -06:00
Stephen Dennis
d0d5d05e4a Fix gRPC subscriber queue stealing across Hydra sessions
Replace the single shared OutputQueue per session with per-subscriber
queues. Each Subscribe, SubscribeGmcp, or GameSession call registers
a SubscriberQueue via addSubscriber(wantsOutput, wantsGmcp). Producers
replicate items to all active subscriber queues via pushOutput() and
pushGmcp(). Each consumer drains only its own queue.

Previously, all consumers popped from the same std::queue, causing
the first consumer to wake to steal items from other subscribers.
Two gRPC clients on the same Hydra session would randomly miss
game output and GMCP messages.

- session_manager.h: SubscriberQueue struct, addSubscriber/
  removeSubscriber/pushOutput/pushGmcp/hasSubscribers methods
- session_manager.cpp: producers call pushOutput/pushGmcp under lock
- grpc_server.cpp: GameSession, Subscribe, SubscribeGmcp each register
  their own SubscriberQueue and drain from it exclusively

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:22:12 -06:00
Stephen Dennis
5770aa39d3 Replace string state fields with proto enums in gRPC API
Convert LinkInfo.state, SessionInfo.state, LinkEvent states,
GameInfo.type, and SystemNotice.severity from string fields to
strongly-typed protobuf enums (LinkState, SessionState, GameType,
Severity).  This prevents typos, enables exhaustive matching in
client code, and eliminates string comparison for semantic data.
GMCP json field documented as intentionally string (game-defined).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:52:01 -06:00
Stephen Dennis
ae39dfdd0a Address gRPC feedback: bidi streaming, GMCP, ColorFormat, auth, more
Six improvements based on review:
1. Bidi GameSession RPC replaces separate SendInput+Subscribe — no
   per-line round-trips, maps to persistent connection model
2. SubscribeGmcp RPC streams structured GMCP data with package filters
3. ColorFormat enum (TrueColor/256/16/PUA/Plain) — clients choose
4. Ping RPC + bidi ping/pong with timestamps for RTT
5. Metadata-based auth (authorization header) with per-message fallback
6. CreateAccount RPC for gRPC-only clients

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:28:03 -06:00
Stephen Dennis
6aa9a1c539 Flesh out gRPC with 18 RPCs and work queue (Phase 3 Step 3b)
Complete gRPC service implementation with thread-safe work queue
bridging gRPC threads to the main event loop.  18 strongly-typed RPCs
covering auth, session lifecycle, link management, I/O streaming
(TrueColor ANSI, all links tagged), scroll-back, credential CRUD,
and process management.  Subscribe uses shared_ptr<OutputQueue> with
condvar for low-latency server-streaming output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:15:17 -06:00
Stephen Dennis
54c48c567b Add optional gRPC front-door with --enable-grpc (Phase 3 Step 3)
gRPC support is an optional configure dependency: ./configure
--enable-grpc detects grpc++ and protobuf via pkg-config and enables
compilation of the gRPC server and protobuf service definition.
Without --enable-grpc (the default), no gRPC code is compiled or
linked.  The HydraService provides Authenticate, ListGames, Connect,
SendInput, Subscribe (server-streaming output), and ListLinks RPCs.

This completes Phase 3 (protocol expansion).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:02:39 -06:00