diff --git a/README.md b/README.md index a754b7c..53fe709 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/internal/handlers/api_test.go b/internal/handlers/api_test.go index d859b55..e7882f2 100644 --- a/internal/handlers/api_test.go +++ b/internal/handlers/api_test.go @@ -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 diff --git a/internal/handlers/utility.go b/internal/handlers/utility.go index 30dcb02..50ee0be 100644 --- a/internal/handlers/utility.go +++ b/internal/handlers/utility.go @@ -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, diff --git a/internal/handlers/utility_test.go b/internal/handlers/utility_test.go index 7afa834..f5dea41 100644 --- a/internal/handlers/utility_test.go +++ b/internal/handlers/utility_test.go @@ -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", ""}, diff --git a/internal/ircserver/commands.go b/internal/ircserver/commands.go index fb2e8c5..f7ffa04 100644 --- a/internal/ircserver/commands.go +++ b/internal/ircserver/commands.go @@ -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", diff --git a/internal/ircserver/integration_test.go b/internal/ircserver/integration_test.go index 3646832..2dde41f 100644 --- a/internal/ircserver/integration_test.go +++ b/internal/ircserver/integration_test.go @@ -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}, diff --git a/internal/ircserver/relay.go b/internal/ircserver/relay.go index 844e705..0dee85b 100644 --- a/internal/ircserver/relay.go +++ b/internal/ircserver/relay.go @@ -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 } diff --git a/internal/ircserver/relay_test.go b/internal/ircserver/relay_test.go index 579d422..caf0cc8 100644 --- a/internal/ircserver/relay_test.go +++ b/internal/ircserver/relay_test.go @@ -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) + } + } +}