Compare nicks by exact case everywhere (closes #128) #141
@@ -1031,14 +1031,14 @@ e.g. `+o-v alice bob`.
|
|||||||
| `441` | ERR_USERNOTINCHANNEL | The `+o`/`+v` nick is not in the channel |
|
| `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
|
**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
|
own gets ERR_USERSDONTMATCH (502). With no mode string the server sends
|
||||||
no mode string the server sends RPL_UMODEIS (221) with your modes: `+`, `+o`,
|
RPL_UMODEIS (221) with your modes: `+`, `+o`, `+w` or `+ow`. A change is a mode
|
||||||
`+w` or `+ow`. A change is a mode string, `body[0]` over the HTTP API or the
|
string, `body[0]` over the HTTP API or the parameter after the nick on the IRC
|
||||||
parameter after the nick on the IRC listener, that starts with `+` or `-`: `w`
|
listener, that starts with `+` or `-`: `w` (receive `WALLOPS`) can be set or
|
||||||
(receive `WALLOPS`) can be set or unset, and `o` (server operator) only unset,
|
unset, and `o` (server operator) only unset, since only `OPER` sets it. Any
|
||||||
since only `OPER` sets it. Any other letter, or a string that names no mode,
|
other letter, or a string that names no mode, gets ERR_UMODEUNKNOWNFLAG (501)
|
||||||
gets ERR_UMODEUNKNOWNFLAG (501) and changes nothing. A change that is accepted
|
and changes nothing. A change that is accepted is answered with 221 and the new
|
||||||
is answered with 221 and the new modes; it is not sent to anyone else.
|
modes; it is not sent to anyone else.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "command": "RPL_UMODEIS", "code": 221, "to": "alice", "body": ["+w"] }
|
{ "command": "RPL_UMODEIS", "code": 221, "to": "alice", "body": ["+w"] }
|
||||||
@@ -2679,8 +2679,7 @@ IRC_LISTEN_ADDR: ""
|
|||||||
set only over the HTTP API.
|
set only over the HTTP API.
|
||||||
- **User modes**: `+o` (operator, set only via `OPER`), `+w` (receives
|
- **User modes**: `+o` (operator, set only via `OPER`), `+w` (receives
|
||||||
`WALLOPS`). `MODE` for any nick other than your own is rejected with
|
`WALLOPS`). `MODE` for any nick other than your own is rejected with
|
||||||
`ERR_USERSDONTMATCH` (502), for both queries and changes. Nick comparison is
|
`ERR_USERSDONTMATCH` (502), for both queries and changes.
|
||||||
case-insensitive.
|
|
||||||
- **KILL**: an operator's `KILL` broadcasts the victim's `QUIT` to its channel
|
- **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
|
peers, deletes its session, and then sends the victim a `KILL` and
|
||||||
`ERROR :Closing Link` before closing its socket. This applies to victims on
|
`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
|
separate parameter, e.g. `MODE #general +k secretpass` or
|
||||||
`MODE #general +o bob`.
|
`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
|
### Docker Usage
|
||||||
|
|
||||||
To expose the IRC port in Docker (the listener is enabled by default on
|
To expose the IRC port in Docker (the listener is enabled by default on
|
||||||
|
|||||||
@@ -669,6 +669,9 @@ func TestCreateSessionDuplicate(t *testing.T) {
|
|||||||
if resp.StatusCode != http.StatusConflict {
|
if resp.StatusCode != http.StatusConflict {
|
||||||
t.Fatalf("expected 409, got %d", resp.StatusCode)
|
t.Fatalf("expected 409, got %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nicks are case-sensitive: Alice is not alice.
|
||||||
|
tserver.createSession("Alice")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEachTestServerHasItsOwnDatabase checks that starting a
|
// TestEachTestServerHasItsOwnDatabase checks that starting a
|
||||||
|
|||||||
@@ -254,8 +254,7 @@ func (hdlr *Handlers) handleWallops(
|
|||||||
|
|
||||||
// handleUserMode handles MODE for a nick: a query when the
|
// handleUserMode handles MODE for a nick: a query when the
|
||||||
// body is empty, otherwise a change by the mode string in
|
// body is empty, otherwise a change by the mode string in
|
||||||
// the body. Only the caller's own nick, in any letter case,
|
// the body. Only the caller's own nick is allowed.
|
||||||
// is allowed.
|
|
||||||
func (hdlr *Handlers) handleUserMode(
|
func (hdlr *Handlers) handleUserMode(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
request *http.Request,
|
request *http.Request,
|
||||||
@@ -263,7 +262,7 @@ func (hdlr *Handlers) handleUserMode(
|
|||||||
nick, target string,
|
nick, target string,
|
||||||
bodyLines func() []string,
|
bodyLines func() []string,
|
||||||
) {
|
) {
|
||||||
if !strings.EqualFold(target, nick) {
|
if target != nick {
|
||||||
hdlr.respondIRCError(
|
hdlr.respondIRCError(
|
||||||
writer, request, clientID, sessionID,
|
writer, request, clientID, sessionID,
|
||||||
irc.ErrUsersDoNotMatch, nick, nil,
|
irc.ErrUsersDoNotMatch, nick, nil,
|
||||||
|
|||||||
@@ -290,8 +290,10 @@ func TestUserMode(t *testing.T) {
|
|||||||
target, modeStr, numeric, modes string
|
target, modeStr, numeric, modes string
|
||||||
}{
|
}{
|
||||||
{nick, "+w", "221", "+w"},
|
{nick, "+w", "221", "+w"},
|
||||||
|
{"Alice", "", "502", ""},
|
||||||
|
{"Alice", "-w", "502", ""},
|
||||||
{nick, "", "221", "+w"},
|
{nick, "", "221", "+w"},
|
||||||
{strings.ToUpper(nick), "-w", "221", "+"},
|
{nick, "-w", "221", "+"},
|
||||||
{nick, "+z", "501", ""},
|
{nick, "+z", "501", ""},
|
||||||
{nick, "+o", "501", ""},
|
{nick, "+o", "501", ""},
|
||||||
{other, "", "502", ""},
|
{other, "", "502", ""},
|
||||||
|
|||||||
@@ -710,12 +710,12 @@ func (c *Conn) applyChannelModes(
|
|||||||
|
|
||||||
// handleUserMode handles MODE for a nick: a query without a
|
// handleUserMode handles MODE for a nick: a query without a
|
||||||
// mode string, otherwise a change. Only the client's own
|
// mode string, otherwise a change. Only the client's own
|
||||||
// nick, in any letter case, is allowed.
|
// nick is allowed.
|
||||||
func (c *Conn) handleUserMode(
|
func (c *Conn) handleUserMode(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
msg *Message,
|
msg *Message,
|
||||||
) {
|
) {
|
||||||
if !strings.EqualFold(msg.Params[0], c.currentNick()) {
|
if msg.Params[0] != c.currentNick() {
|
||||||
c.sendNumeric(
|
c.sendNumeric(
|
||||||
irc.ErrUsersDoNotMatch,
|
irc.ErrUsersDoNotMatch,
|
||||||
"Can't change mode for other users",
|
"Can't change mode for other users",
|
||||||
|
|||||||
@@ -1084,9 +1084,9 @@ func TestIntegrationWallops(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestIntegrationUserMode covers MODE for a nick: changes
|
// TestIntegrationUserMode covers MODE for a nick: changes
|
||||||
// and queries of your own modes in any letter case, refusal
|
// and queries of your own modes, refusal for another nick,
|
||||||
// for another nick, and a rejected mode string changing
|
// even one that differs from yours only in letter case, and
|
||||||
// nothing.
|
// a rejected mode string changing nothing.
|
||||||
func TestIntegrationUserMode(t *testing.T) {
|
func TestIntegrationUserMode(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1106,7 +1106,9 @@ func TestIntegrationUserMode(t *testing.T) {
|
|||||||
for _, step := range []struct {
|
for _, step := range []struct {
|
||||||
line, want string
|
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", notYours},
|
||||||
{"MODE bob -w", notYours},
|
{"MODE bob -w", notYours},
|
||||||
{"MODE alice xw", unknown},
|
{"MODE alice xw", unknown},
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func (c *Conn) deliverTextMessage(
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Don't echo our own messages back.
|
// Don't echo our own messages back.
|
||||||
if strings.EqualFold(from, nick) {
|
if from == nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ func (c *Conn) deliverJoin(msg *db.IRCMessage) {
|
|||||||
|
|
||||||
// Don't echo our own JOINs (we already sent them
|
// Don't echo our own JOINs (we already sent them
|
||||||
// during joinChannel).
|
// during joinChannel).
|
||||||
if strings.EqualFold(msg.From, nick) {
|
if msg.From == nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ func (c *Conn) deliverPart(msg *db.IRCMessage, text string) {
|
|||||||
nick := c.nick
|
nick := c.nick
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if strings.EqualFold(msg.From, nick) {
|
if msg.From == nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ func (c *Conn) deliverNickChange(
|
|||||||
nick := c.nick
|
nick := c.nick
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if strings.EqualFold(msg.From, nick) {
|
if msg.From == nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ func (c *Conn) deliverQuitMsg(
|
|||||||
nick := c.nick
|
nick := c.nick
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
if strings.EqualFold(msg.From, nick) {
|
if msg.From == nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,3 +185,39 @@ func TestRelayLineBreaks(t *testing.T) {
|
|||||||
t.Errorf("got %q, want %q", got, want)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user