Compare nicks by exact case everywhere (closes #128) #141

Merged
clawbot merged 1 commits from issue-128-exact-nicks into next 2026-10-20 16:31:29 +02:00
8 changed files with 66 additions and 31 deletions
+9 -16
View File
@@ -1031,14 +1031,14 @@ e.g. `+o-v alice bob`.
| `441` | ERR_USERNOTINCHANNEL | The `+o`/`+v` nick is not in the channel |
**User modes:** you can query and change only your own; a nick other than your
own, compared without regard to letter case, gets ERR_USERSDONTMATCH (502). With
no mode string the server sends RPL_UMODEIS (221) with your modes: `+`, `+o`,
`+w` or `+ow`. A change is a mode string, `body[0]` over the HTTP API or the
parameter after the nick on the IRC listener, that starts with `+` or `-`: `w`
(receive `WALLOPS`) can be set or unset, and `o` (server operator) only unset,
since only `OPER` sets it. Any other letter, or a string that names no mode,
gets ERR_UMODEUNKNOWNFLAG (501) and changes nothing. A change that is accepted
is answered with 221 and the new modes; it is not sent to anyone else.
own gets ERR_USERSDONTMATCH (502). With no mode string the server sends
RPL_UMODEIS (221) with your modes: `+`, `+o`, `+w` or `+ow`. A change is a mode
string, `body[0]` over the HTTP API or the parameter after the nick on the IRC
listener, that starts with `+` or `-`: `w` (receive `WALLOPS`) can be set or
unset, and `o` (server operator) only unset, since only `OPER` sets it. Any
other letter, or a string that names no mode, gets ERR_UMODEUNKNOWNFLAG (501)
and changes nothing. A change that is accepted is answered with 221 and the new
modes; it is not sent to anyone else.
```json
{ "command": "RPL_UMODEIS", "code": 221, "to": "alice", "body": ["+w"] }
@@ -2679,8 +2679,7 @@ IRC_LISTEN_ADDR: ""
set only over the HTTP API.
- **User modes**: `+o` (operator, set only via `OPER`), `+w` (receives
`WALLOPS`). `MODE` for any nick other than your own is rejected with
`ERR_USERSDONTMATCH` (502), for both queries and changes. Nick comparison is
case-insensitive.
`ERR_USERSDONTMATCH` (502), for both queries and changes.
- **KILL**: an operator's `KILL` broadcasts the victim's `QUIT` to its channel
peers, deletes its session, and then sends the victim a `KILL` and
`ERROR :Closing Link` before closing its socket. This applies to victims on
@@ -2701,12 +2700,6 @@ API reaches IRC listener members as an IRC `MODE` line with its value as a
separate parameter, e.g. `MODE #general +k secretpass` or
`MODE #general +o bob`.
One gap remains: the IRC listener does not pass on a `PRIVMSG`, `NOTICE`,
`JOIN`, `PART`, `NICK` or `QUIT` whose sender's nick matches the user's own nick
ignoring case, so an IRC listener user `alice` never sees these from `Alice`,
although nicks are case-sensitive. https://git.eeqj.de/sneak/neoirc/issues/128
tracks it.
### Docker Usage
To expose the IRC port in Docker (the listener is enabled by default on
+3
View File
@@ -669,6 +669,9 @@ func TestCreateSessionDuplicate(t *testing.T) {
if resp.StatusCode != http.StatusConflict {
t.Fatalf("expected 409, got %d", resp.StatusCode)
}
// Nicks are case-sensitive: Alice is not alice.
tserver.createSession("Alice")
}
// TestEachTestServerHasItsOwnDatabase checks that starting a
+2 -3
View File
@@ -254,8 +254,7 @@ func (hdlr *Handlers) handleWallops(
// handleUserMode handles MODE for a nick: a query when the
// body is empty, otherwise a change by the mode string in
// the body. Only the caller's own nick, in any letter case,
// is allowed.
// the body. Only the caller's own nick is allowed.
func (hdlr *Handlers) handleUserMode(
writer http.ResponseWriter,
request *http.Request,
@@ -263,7 +262,7 @@ func (hdlr *Handlers) handleUserMode(
nick, target string,
bodyLines func() []string,
) {
if !strings.EqualFold(target, nick) {
if target != nick {
hdlr.respondIRCError(
writer, request, clientID, sessionID,
irc.ErrUsersDoNotMatch, nick, nil,
+3 -1
View File
@@ -290,8 +290,10 @@ func TestUserMode(t *testing.T) {
target, modeStr, numeric, modes string
}{
{nick, "+w", "221", "+w"},
{"Alice", "", "502", ""},
{"Alice", "-w", "502", ""},
{nick, "", "221", "+w"},
{strings.ToUpper(nick), "-w", "221", "+"},
{nick, "-w", "221", "+"},
{nick, "+z", "501", ""},
{nick, "+o", "501", ""},
{other, "", "502", ""},
+2 -2
View File
@@ -710,12 +710,12 @@ func (c *Conn) applyChannelModes(
// handleUserMode handles MODE for a nick: a query without a
// mode string, otherwise a change. Only the client's own
// nick, in any letter case, is allowed.
// nick is allowed.
func (c *Conn) handleUserMode(
ctx context.Context,
msg *Message,
) {
if !strings.EqualFold(msg.Params[0], c.currentNick()) {
if msg.Params[0] != c.currentNick() {
c.sendNumeric(
irc.ErrUsersDoNotMatch,
"Can't change mode for other users",
+6 -4
View File
@@ -1084,9 +1084,9 @@ func TestIntegrationWallops(t *testing.T) {
}
// TestIntegrationUserMode covers MODE for a nick: changes
// and queries of your own modes in any letter case, refusal
// for another nick, and a rejected mode string changing
// nothing.
// and queries of your own modes, refusal for another nick,
// even one that differs from yours only in letter case, and
// a rejected mode string changing nothing.
func TestIntegrationUserMode(t *testing.T) {
t.Parallel()
@@ -1106,7 +1106,9 @@ func TestIntegrationUserMode(t *testing.T) {
for _, step := range []struct {
line, want string
}{
{"MODE ALICE +w", " 221 alice +w"},
{"MODE alice +w", " 221 alice +w"},
{"MODE Alice", notYours},
{"MODE Alice -w", notYours},
{"MODE bob", notYours},
{"MODE bob -w", notYours},
{"MODE alice xw", unknown},
+5 -5
View File
@@ -195,7 +195,7 @@ func (c *Conn) deliverTextMessage(
c.mu.Unlock()
// Don't echo our own messages back.
if strings.EqualFold(from, nick) {
if from == nick {
return
}
@@ -217,7 +217,7 @@ func (c *Conn) deliverJoin(msg *db.IRCMessage) {
// Don't echo our own JOINs (we already sent them
// during joinChannel).
if strings.EqualFold(msg.From, nick) {
if msg.From == nick {
return
}
@@ -233,7 +233,7 @@ func (c *Conn) deliverPart(msg *db.IRCMessage, text string) {
nick := c.nick
c.mu.Unlock()
if strings.EqualFold(msg.From, nick) {
if msg.From == nick {
return
}
@@ -258,7 +258,7 @@ func (c *Conn) deliverNickChange(
nick := c.nick
c.mu.Unlock()
if strings.EqualFold(msg.From, nick) {
if msg.From == nick {
return
}
@@ -276,7 +276,7 @@ func (c *Conn) deliverQuitMsg(
nick := c.nick
c.mu.Unlock()
if strings.EqualFold(msg.From, nick) {
if msg.From == nick {
return
}
+36
View File
@@ -185,3 +185,39 @@ func TestRelayLineBreaks(t *testing.T) {
t.Errorf("got %q, want %q", got, want)
}
}
// TestRelayNickDiffersOnlyInCase checks that an IRC listener
// user alice receives a JOIN, PRIVMSG, PART, NICK and QUIT
// from Alice in a channel they share: nicks are
// case-sensitive, so Alice is someone else.
func TestRelayNickDiffersOnlyInCase(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
alice := env.dial(t)
alice.register("alice")
alice.joinAndDrain("#case")
capitalAlice := env.dial(t)
capitalAlice.register("Alice")
for _, step := range []struct {
line, want string
}{
{"JOIN #case", ":Alice!Alice@* JOIN #case"},
{"PRIVMSG #case :hi there", ":Alice!Alice@* PRIVMSG #case :hi there"},
{"PART #case :see you", ":Alice!Alice@* PART #case :see you"},
{"JOIN #case", ":Alice!Alice@* JOIN #case"},
{"NICK Alicia", ":Alice!Alice@* NICK Alicia"},
{"NICK Alice", ":Alicia!Alicia@* NICK Alice"}, // back for QUIT
{"QUIT", ":Alice!Alice@* QUIT :Quit: Connection closed"},
} {
capitalAlice.send(step.line)
got := alice.readLine()
if got != step.want {
t.Errorf("%s: got %q, want %q", step.line, got, step.want)
}
}
}