Fix IRC listener relay of bodies and modes, and JOIN names (closes #127)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The IRC listener relayed only the first line of a PRIVMSG or NOTICE body; it now sends one line per body line. A MODE change reached IRC listener members without its key, limit or mask, and "+o bob" went out as one trailing parameter; each value is now its own parameter, in the relay and in the echo to the IRC listener user who made the change. Every line written to an IRC listener connection now has CR, LF and NUL removed, so a stored body line, key or mask cannot start a line of its own. The channel name check moved from the HTTP handler into service.JoinChannel, so the IRC listener's JOIN refuses the same names with 403. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package ircserver_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// queueMessage queues a message for the user with nick
|
||||
// recipient the way the HTTP API sends one: stored once, then
|
||||
// queued to the recipient's session.
|
||||
func (env *testEnv) queueMessage(
|
||||
t *testing.T,
|
||||
recipient, command, from, to string,
|
||||
body []string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
bodyJSON, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal body: %v", err)
|
||||
}
|
||||
|
||||
msgID, _, err := env.database.InsertMessage(
|
||||
ctx, command, from, to, nil, bodyJSON, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("insert message: %v", err)
|
||||
}
|
||||
|
||||
sessionID, err := env.database.GetSessionByNick(ctx, recipient)
|
||||
if err != nil {
|
||||
t.Fatalf("get session of %s: %v", recipient, err)
|
||||
}
|
||||
|
||||
err = env.database.EnqueueToSession(ctx, sessionID, msgID)
|
||||
if err != nil {
|
||||
t.Fatalf("enqueue: %v", err)
|
||||
}
|
||||
|
||||
env.brk.Notify(sessionID)
|
||||
}
|
||||
|
||||
// TestRelayMultiLineBody checks that each line of a PRIVMSG
|
||||
// or NOTICE body sent over the HTTP API reaches an IRC
|
||||
// listener user as its own line.
|
||||
func TestRelayMultiLineBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
|
||||
for _, command := range []string{"PRIVMSG", "NOTICE"} {
|
||||
env.queueMessage(
|
||||
t, "alice", command, "carol", "alice",
|
||||
[]string{"first line", "second line"},
|
||||
)
|
||||
|
||||
lines := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, "second line")
|
||||
})
|
||||
|
||||
want := []string{
|
||||
":carol!carol@* " + command + " alice :first line",
|
||||
":carol!carol@* " + command + " alice :second line",
|
||||
}
|
||||
|
||||
got := lines[max(0, len(lines)-len(want)):]
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("%s: got %q, want %q", command, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRelayModeParameters checks that a MODE change reaches
|
||||
// IRC listener members with its values as separate
|
||||
// parameters, both for the bodies the HTTP API stores and
|
||||
// for a change made on the IRC listener.
|
||||
func TestRelayModeParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
|
||||
for _, step := range []struct {
|
||||
body []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"+k", "secret"}, "+k secret"},
|
||||
{[]string{"-k", "*"}, "-k *"},
|
||||
{[]string{"+l", "50"}, "+l 50"},
|
||||
{[]string{"-l"}, "-l"},
|
||||
{[]string{"+b", "*!*@*.example.com"}, "+b *!*@*.example.com"},
|
||||
{[]string{"-b", "*!*@*.example.com"}, "-b *!*@*.example.com"},
|
||||
{[]string{"+o bob"}, "+o bob"},
|
||||
{[]string{"+m"}, "+m"},
|
||||
} {
|
||||
env.queueMessage(t, "alice", "MODE", "op", "#modes", step.body)
|
||||
|
||||
lines := alice.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " MODE ")
|
||||
})
|
||||
|
||||
got := lines[len(lines)-1]
|
||||
want := ":op!op@* MODE #modes " + step.want
|
||||
|
||||
if got != want {
|
||||
t.Errorf("body %q: got %q, want %q", step.body, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
alice.joinAndDrain("#modes")
|
||||
bob.joinAndDrain("#modes")
|
||||
|
||||
alice.send("MODE #modes +v bob")
|
||||
|
||||
const change = " MODE #modes +v bob"
|
||||
|
||||
for name, client := range map[string]*testClient{
|
||||
"alice": alice, "bob": bob,
|
||||
} {
|
||||
lines := client.readUntil(func(l string) bool {
|
||||
return strings.Contains(l, " MODE ")
|
||||
})
|
||||
|
||||
got := lines[len(lines)-1]
|
||||
if !strings.HasSuffix(got, change) {
|
||||
t.Errorf("%s: got %q, want it to end in %q", name, got, change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRelayLineBreaks checks that a CR, LF or NUL in a stored
|
||||
// body line, key or mask never reaches an IRC listener user
|
||||
// as a line break, so no extra line arrives.
|
||||
func TestRelayLineBreaks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
|
||||
env.queueMessage(t, "alice", "PRIVMSG", "carol", "alice", []string{
|
||||
"one\r\n:admin!a@b PRIVMSG alice :hi",
|
||||
"two\n:admin!a@b NOTICE alice :hi\x00",
|
||||
})
|
||||
env.queueMessage(t, "alice", "MODE", "op", "#breaks",
|
||||
[]string{"+k", "key\r\nERROR :Closing link"})
|
||||
env.queueMessage(t, "alice", "MODE", "op", "#breaks",
|
||||
[]string{"+b", "*!*@*\r\n:admin!a@b KICK #breaks alice"})
|
||||
env.queueMessage(t, "alice", "PRIVMSG", "carol", "alice",
|
||||
[]string{"the end"})
|
||||
|
||||
lines := alice.readUntil(func(l string) bool {
|
||||
return strings.HasSuffix(l, " :the end")
|
||||
})
|
||||
|
||||
want := []string{
|
||||
":carol!carol@* PRIVMSG alice :one:admin!a@b PRIVMSG alice :hi",
|
||||
":carol!carol@* PRIVMSG alice :two:admin!a@b NOTICE alice :hi",
|
||||
":op!op@* MODE #breaks +k :keyERROR :Closing link",
|
||||
":op!op@* MODE #breaks +b :*!*@*:admin!a@b KICK #breaks alice",
|
||||
":carol!carol@* PRIVMSG alice :the end",
|
||||
}
|
||||
|
||||
got := lines[max(0, len(lines)-len(want)):]
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user