feat: add username/hostname support with IRC hostmask format #82
Reference in New Issue
Block a user
Delete Branch "feature/username-hostname-support"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds username and hostname support to sessions, enabling standard IRC hostmask format (
nick!user@host) for WHOIS, WHO, and future+bban matching.closes #81
Changes
Schema (
001_initial.sql)username TEXT NOT NULL DEFAULT ''andhostname TEXT NOT NULL DEFAULT ''columns to thesessionstableDatabase layer (
internal/db/)CreateSessionnow acceptsusernameandhostnameparameters; username defaults to nick if emptyRegisterUsernow acceptsusernameandhostnameparametersSessionHostInfotype andGetSessionHostInfoquery to retrieve username/hostname for a sessionMemberInfonow includesUsernameandHostnamefieldsChannelMembersquery updated to return username/hostnameFormatHostmask(nick, username, hostname)helper that producesnick!user@hostformatHostmask()method onMemberInfoHandler layer (
internal/handlers/)POST /api/v1/session) accepts optionalusernamefield; resolves hostname via reverse DNS of connecting client IP (respectsX-Forwarded-ForandX-Real-IPheaders)POST /api/v1/register) accepts optionalusernamefield with the same hostname resolution^[a-zA-Z0-9_\-\[\]\\^{}|+ "`" +]{1,32}$311 RPL_WHOISUSER) now returns the real username and hostname instead of nick/servername352 RPL_WHOREPLY) now returns the real username and hostname instead of nick/servernamevalidateHashcashandresolveUsernamehelpers to keep functions under the linter'sfunlenlimitexecuteRegisterhelper for the same reason(*net.Resolver).LookupAddrwith a 3-second timeout contextTests
TestCreateSessionWithUserHost— verifies username/hostname are stored and retrievableTestCreateSessionDefaultUsername— verifies empty username defaults to nickTestGetSessionHostInfoNotFound— verifies error on nonexistent sessionTestFormatHostmask— verifiesnick!user@hostformattingTestFormatHostmaskDefaults— verifies fallback when username/hostname emptyTestMemberInfoHostmask— verifiesHostmask()method onMemberInfoTestChannelMembersIncludeUserHost— verifiesChannelMembersreturns username/hostnameTestRegisterUserWithUserHost— verifies registration stores username/hostnameTestRegisterUserDefaultUsername— verifies registration defaults username to nickTestWhoisShowsHostInfo— integration test verifying WHOIS returns the correct usernameTestWhoShowsHostInfo— integration test verifying WHO returns the correct usernameTestSessionUsernameDefault— integration test verifying default username in WHOISCreateSession/RegisterUsersignaturesREADME
nick!user@hostformatusernamefieldDocker build
docker build .passes cleanly (lint, format, tests, build).The sessions table also needs IP (real client IP of session creator) and each client connection also needs IP and hostname.
Rework Summary
Addressed sneak's feedback to add IP tracking to sessions and IP+hostname tracking to each client connection.
Changes
Schema (
001_initial.sql):ip TEXT NOT NULL DEFAULT ''tosessionstableip TEXT NOT NULL DEFAULT ''andhostname TEXT NOT NULL DEFAULT ''toclientstableDB layer:
CreateSessionnow acceptsremoteIPparameter, stores it in both the session and the initial client recordRegisterUsernow acceptsremoteIPparameter, same storage patternLoginUsernow acceptsremoteIPandhostnameparameters, stores them in the new client recordClientHostInfostruct andGetClientHostInfo()query methodSessionHostInfoto includeIPfieldHandler layer:
handleCreateSession→ extractedexecuteCreateSession(also fixes funlen lint), passesclientIP(request)as remoteIPexecuteRegister→ passesclientIP(request)as remoteIPhandleLogin→ resolves hostname for the login client, passes IP + hostname toLoginUserTests:
TestCreateSessionStoresIP— verifies session and client both get the IPTestGetClientHostInfoNotFound— error path for nonexistent clientTestLoginUserStoresClientIPHostname— verifies login creates client with correct IP/hostnameTestRegisterUserStoresSessionIP— verifies registered session gets IPREADME: Updated identity section to document session
ipfield and per-client IP/hostname tracking.Verification
make fmt✅make lint✅ (all lint issues resolved)make test✅ (all tests pass)docker build .✅Review: FAIL
Build
docker build .passes cleanly — lint, format, tests, build all green.What's Done Well
sessionstable hasusername,hostname,ipcolumns;clientstable hasip,hostnamecolumns. All correct in001_initial.sql.handleCreateSession→executeCreateSessioncorrectly extracts real client IP viaclientIP()(checksX-Forwarded-For,X-Real-IP, thenRemoteAddr), resolves hostname via rDNS with 3s timeout, stores both on session and initial client.handleRegister→executeRegisterfollows the same pattern. Session gets IP; initial client gets IP + hostname.handleLogincorrectly resolves IP + hostname for the new client connection.executeWhoislooks upSessionHostInfoand uses realusername/hostnamein311 RPL_WHOISUSERparams. Falls back to nick/servername when empty.handleWhousesMemberInfo.Username/Hostnamein352 RPL_WHOREPLYparams with correct fallback logic.FormatHostmask()andMemberInfo.Hostmask()produce correctnick!user@hostformat.TestCreateSessionStoresIP,TestLoginUserStoresClientIPHostname,TestRegisterUserStoresSessionIP,TestWhoisShowsHostInfo,TestWhoShowsHostInfo,TestSessionUsernameDefault,TestChannelMembersIncludeUserHost, hostmask tests, error path tests.Failing Issue
NAMES does not show username/hostname — Issue #81 explicitly says:
Neither
handleNames(explicit/NAMEScommand, ~line 2044) nordeliverNamesNumerics(JOIN-triggered NAMES, ~line 1465) was updated. Both still output plain nicks only:The
ChannelMembersquery was correctly updated to returnUsernameandHostname, but the NAMES reply doesn't use them. The data is available but not exposed.The
353 RPL_NAMREPLYbody should include hostmask info (e.g.nick!user@hostformat, similar to IRCv3userhost-in-names). The README's NAMES example also still shows plain nicks, consistent with the code gap.No test exists for NAMES showing username/hostname.
Required Fix
handleNamesanddeliverNamesNumericsto include username/hostname in the NAMES reply body (e.g.nick!user@hostformat with mode prefix where applicable).353example to reflect the new format.Verdict
FAIL — The issue explicitly names
/namesas a required output surface for username/hostname, and it was not implemented. Everything else is solid.Rework Summary
Fixed the NAMES handler to include hostmask data as identified in the review.
Changes
internal/handlers/api.go:deliverNamesNumerics(JOIN-triggered NAMES): changed frommem.Nicktomem.Hostmask()so RPL_NAMREPLY body outputsnick!user@hostformathandleNames(explicit/NAMEScommand): same change — now outputsnick!user@hostin the NAMES reply bodyREADME.md:353RPL_NAMREPLY example to show hostmask format instead of plain nicksinternal/handlers/api_test.go:TestNamesShowsHostmask— integration test: creates a session with known username, issues explicit NAMES command, verifies the 353 reply body containsnick!user@hostformatTestNamesOnJoinShowsHostmask— integration test: verifies that the NAMES reply delivered on JOIN (viadeliverNamesNumerics) also includes hostmask datasetupChannelWithIdentMember— shared helper extracted to avoiddupllint violationassertNamesHostmask— assertion helper that checks 353 body for expected hostmask prefixVerification
make fmt-check✅make lint✅make test✅ (all tests pass including both new NAMES hostmask tests)docker build .✅Review: PR #82 — Username/Hostname Support (post-rework #2)
Verdict: PASS ✅
Checklist
nick!user@host(JOIN-triggereddeliverNamesNumerics)mem.Hostmask()correctly usednick!user@host(explicithandleNames)GetSessionHostInfolookup, falls back correctlymem.Username/mem.HostnamefromMemberInfousername,hostname,ipCreateSession+RegisterUserall correctip,hostnameTestNamesShowsHostmask+TestNamesOnJoinShowsHostmask— meaningful assertionsnick!user@hostformatdocker build .passesCode Quality Notes
clientIP(),resolveHostname(),resolveUsername(),validateHashcash(),FormatHostmask()are all well-separated utility functionsHostmask()method onMemberInfois the right pattern — keeps formatting logic centralizedMinor Documentation Note
The README prose section correctly documents
ipon sessions andip/hostnameon clients, but the Database Schema reference tables are missing these rows:sessionstable:ipcolumn not in tableclientstable:ipandhostnamecolumns not in tableThis is a minor inconsistency (prose is correct, table is incomplete). Not blocking.
session gets ip+hostname and that is used for the display and /whois etc. client gets ip and hostname and that is not displayed anywhere at the moment (except when an oper does /whois of a user).
oper being server o-line not channel +o
documentation and code being out of sync is always blocking.
Rework: oper-only client IP/hostname in WHOIS
Implemented per sneak's instructions — client-level IP/hostname is now only visible to server operators (o-line users) via WHOIS.
Changes
OPER command (new):
OPERcommand handler: authenticates againstNEOIRC_OPER_NAME/NEOIRC_OPER_PASSWORDenv varsis_operflag on session, returns381 RPL_YOUREOPER491 ERR_NOOPERHOSTis_opercolumn to sessions table in schemaOper-aware WHOIS:
/whois, the response now includes338 RPL_WHOISACTUALLYwith the target's current client IP and hostname313 RPL_WHOISOPERATORis now sent when the WHOIS target is a server operLUSERS:
252 RPL_LUSEROPnow reports the real oper count instead of hardcoded0README fixes:
ipandis_operto sessions schema tableipandhostnameto clients schema tableNEOIRC_OPER_NAMEandNEOIRC_OPER_PASSWORDto configuration tableTests (7 new):
TestOperCommandSuccess— successful OPER authTestOperCommandFailure— wrong password → 491TestOperCommandNeedMoreParams— missing params → 461TestOperWhoisShowsClientInfo— oper sees 338 with client IPTestNonOperWhoisHidesClientInfo— non-oper does NOT see 338TestWhoisShowsOperatorStatus— 313 shown when target is operTestOperNoOlineConfigured— OPER fails when no o-line configuredTestSetAndCheckSessionOper,TestGetLatestClientForSession,TestGetOperCountCode quality:
executeWhoisinto smaller helpers (whoisNotFound,deliverWhoisUser,deliverWhoisOperator) to stay under funlen limitmake fmt✅ |docker build .✅ (lint + test + build all pass)Review: PR #82 — Username/Hostname Support (post-rework #3)
Verdict: FAIL
Build
docker build .passes cleanly — lint, format, tests, build all green.What's Done Well
NEOIRC_OPER_NAME/NEOIRC_OPER_PASSWORDenv vars. Returns381 RPL_YOUREOPERon success,491 ERR_NOOPERHOSTon failure (including when no o-line is configured),461 ERR_NEEDMOREPARAMSwhen args are missing. Correctly setsis_operflag on the session viaSetSessionOper.deliverWhoisActuallycorrectly checksIsSessionOper(ctx, querierSID)(the querier, not the target) before sending338 RPL_WHOISACTUALLYwith client IP and hostname. Non-opers see nothing extra. This is the correct o-line behavior.deliverWhoisOperatorcorrectly checks if the target is an oper and sends313to any querier (not just opers). This matches IRC spec: anyone can see that a user is an oper via WHOIS.is_oper INTEGER NOT NULL DEFAULT 0added to sessions table. Clean.SetSessionOper,IsSessionOper,GetLatestClientForSession,GetOperCountall correct and well-tested.252 RPL_LUSEROPnow shows real oper count fromGetOperCountinstead of hardcoded0.NEOIRC_OPER_NAME/NEOIRC_OPER_PASSWORDin config table,is_operin sessions schema table,ip+hostnamein clients schema table, oper-visible WHOIS data documented.SetAndCheckSessionOper,GetLatestClientForSession,GetOperCount. All honest — no mocking, no weakened assertions..golangci.ymluntouched, no linter config changes, no external dependency changes.Blocking Issue
Misplaced function comment:
handleAwaycomment now decorateshandleOper(internal/handlers/api.goline 2796).The
handleOperfunction was inserted between the existinghandleAwaydoc comment and thehandleAwayfunction definition:Result:
handleOper(line 2798) has the wrong doc comment — it says "handles the AWAY command"handleAway(line 2866) has no doc comment at allsneak stated on this PR: "documentation and code being out of sync is always blocking". A function comment that describes a completely different function is exactly that.
Required Fix
handleOper(e.g.// handleOper handles the OPER command. Authenticates the session as a server operator (o-line) using configured credentials.)handleAwaydoc comment above the actualhandleAwayfunctionNon-blocking Note
!=(line ~2834) rather thancrypto/subtle.ConstantTimeCompare. This is a minor timing-attack surface. Not blocking for this PR since IRC oper auth is typically plaintext comparison and the env-var approach limits exposure, but worth hardening in a follow-up.Verdict
FAIL — One blocking issue: misplaced function comment creates doc/code mismatch. Everything else is solid. Simple fix.
Rework complete — fixed misplaced doc comments.
Changes:
// handleAway handles the AWAY command...comment abovehandleOperwith correct// handleOper handles the OPER command for server operator authentication.// handleAway handles the AWAY command...doc comment abovehandleAwayNo logic changes.
docker build .passes (all tests green).Commit:
d7bab0bFAIL security timing bug in comparison func
Rework: Timing-safe OPER credential comparison
Problem: The
handleOperfunction used plain!=string comparison for operator name and password, which is vulnerable to timing-based side-channel attacks.Fix: Replaced both comparisons with
crypto/subtle.ConstantTimeCompare:Changed file:
internal/handlers/api.go— 1 file, +3/-1 linesVerification:
make fmt✅docker build .✅ (all tests pass, image builds)Commit:
427ee1eReview: PR #82 — Username/Hostname Support (post-rework #5)
Verdict: PASS ✅
Rework #5 Verification
The only change in rework #5 (commit
427ee1e) is the timing-safe OPER credential comparison fix that sneak explicitly flagged:handleOperusescrypto/subtle.ConstantTimeComparefor namehandleOperusescrypto/subtle.ConstantTimeComparefor passwordcrypto/subtleproperly importedhandleOperdoc comment correct// handleOper handles the OPER command for server operator authentication.)handleAwaydoc comment correct// handleAway handles the AWAY command...on its own function)Full Requirements Checklist
username,hostname,ipcolumnsip,hostnamecolumns.golangci.ymluntoucheddocker build .passes (lint + test + build)Code
Both name and password are compared in constant time. Short-circuit on empty config is fine — it leaks no information about valid credentials (just that no o-line is configured, which is not secret).
No issues found. Clean fix for a real security concern.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.