Enforce channel keys, invites, +s and +H on the IRC listener (closes #122)
check / check (push) Waiting to run
check / check (push) Waiting to run
The IRC listener's JOIN now passes the channel key on, INVITE records the invite, LIST and WHOIS hide +s channels from non-members with the queries the HTTP API uses, and PRIVMSG to a +H channel gets 404. The INVITE checks and the channel hashcash check move from the HTTP handlers into the service, so both transports run the same code. An IRC client cannot attach a stamp, so its PRIVMSG to a +H channel always fails that check. The unfiltered channel list query, now unused, is removed. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #139.
This commit is contained in:
@@ -150,7 +150,9 @@ func (c *Conn) handlePrivmsg(
|
||||
}
|
||||
|
||||
// handleJoin joins one or more channels via the shared
|
||||
// service layer.
|
||||
// service layer. The optional second parameter holds the
|
||||
// channel keys, comma-separated in the order of the
|
||||
// channels.
|
||||
func (c *Conn) handleJoin(
|
||||
ctx context.Context,
|
||||
msg *Message,
|
||||
@@ -164,14 +166,24 @@ func (c *Conn) handleJoin(
|
||||
return
|
||||
}
|
||||
|
||||
for chanName := range strings.SplitSeq(msg.Params[0], ",") {
|
||||
var keys []string
|
||||
if len(msg.Params) > 1 {
|
||||
keys = strings.Split(msg.Params[1], ",")
|
||||
}
|
||||
|
||||
for idx, chanName := range strings.Split(msg.Params[0], ",") {
|
||||
chanName = strings.TrimSpace(chanName)
|
||||
|
||||
if !strings.HasPrefix(chanName, "#") {
|
||||
chanName = "#" + chanName
|
||||
}
|
||||
|
||||
c.joinChannel(ctx, chanName)
|
||||
key := ""
|
||||
if idx < len(keys) {
|
||||
key = keys[idx]
|
||||
}
|
||||
|
||||
c.joinChannel(ctx, chanName, key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +191,10 @@ func (c *Conn) handleJoin(
|
||||
// and delivers topic/names on the wire.
|
||||
func (c *Conn) joinChannel(
|
||||
ctx context.Context,
|
||||
channel string,
|
||||
channel, key string,
|
||||
) {
|
||||
result, err := c.svc.JoinChannel(
|
||||
ctx, c.sessionID, c.nick, channel, "",
|
||||
ctx, c.sessionID, c.nick, channel, key,
|
||||
)
|
||||
if err != nil {
|
||||
c.sendIRCError(err)
|
||||
@@ -763,10 +775,11 @@ func (c *Conn) handleNames(
|
||||
c.deliverNames(ctx, channel, chID)
|
||||
}
|
||||
|
||||
// handleList sends the channel list.
|
||||
// handleList sends the channel list, leaving out +s
|
||||
// channels the user is not on.
|
||||
func (c *Conn) handleList(ctx context.Context) {
|
||||
channels, err := c.database.ListAllChannelsWithCounts(
|
||||
ctx,
|
||||
channels, err := c.database.ListAllChannelsWithCountsFiltered(
|
||||
ctx, c.sessionID,
|
||||
)
|
||||
if err != nil {
|
||||
c.sendNumeric(
|
||||
@@ -886,14 +899,15 @@ func (c *Conn) whoisOper(
|
||||
}
|
||||
}
|
||||
|
||||
// whoisChannels sends 319 RPL_WHOISCHANNELS.
|
||||
// whoisChannels sends 319 RPL_WHOISCHANNELS, leaving out
|
||||
// +s channels the querier is not on.
|
||||
func (c *Conn) whoisChannels(
|
||||
ctx context.Context,
|
||||
target string,
|
||||
targetID int64,
|
||||
) {
|
||||
userChannels, _ := c.database.GetSessionChannels(
|
||||
ctx, targetID,
|
||||
userChannels, _ := c.database.GetSessionChannelsFiltered(
|
||||
ctx, targetID, c.sessionID,
|
||||
)
|
||||
if len(userChannels) == 0 {
|
||||
return
|
||||
@@ -1230,36 +1244,11 @@ func (c *Conn) handleInvite(
|
||||
targetNick := msg.Params[0]
|
||||
channel := msg.Params[1]
|
||||
|
||||
chID, err := c.database.GetChannelByName(ctx, channel)
|
||||
if err != nil {
|
||||
c.sendNumeric(
|
||||
irc.ErrNoSuchChannel,
|
||||
channel, "No such channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
isMember, _ := c.database.IsChannelMember(
|
||||
ctx, chID, c.sessionID,
|
||||
)
|
||||
if !isMember {
|
||||
c.sendNumeric(
|
||||
irc.ErrNotOnChannel,
|
||||
channel, "You're not on that channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
targetID, err := c.database.GetSessionByNick(
|
||||
ctx, targetNick,
|
||||
targetID, err := c.svc.InviteUser(
|
||||
ctx, c.sessionID, c.nick, targetNick, channel,
|
||||
)
|
||||
if err != nil {
|
||||
c.sendNumeric(
|
||||
irc.ErrNoSuchNick,
|
||||
targetNick, "No such nick",
|
||||
)
|
||||
c.sendIRCError(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1239,6 +1239,216 @@ func TestIntegrationModeModerated(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationModeKey checks that JOIN takes the key of
|
||||
// a +k channel as its second parameter, and that with
|
||||
// several channels each key goes to the channel in the
|
||||
// same position.
|
||||
func TestIntegrationModeKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#keyed")
|
||||
alice.joinAndDrain("#vault")
|
||||
|
||||
// The IRC listener cannot set +k, so the test sets the
|
||||
// keys directly.
|
||||
for channel, key := range map[string]string{
|
||||
"#keyed": "sesame",
|
||||
"#vault": "barley",
|
||||
} {
|
||||
chID, err := env.database.GetChannelByName(
|
||||
t.Context(), channel,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("get channel %s: %v", channel, err)
|
||||
}
|
||||
|
||||
err = env.database.SetChannelKey(t.Context(), chID, key)
|
||||
if err != nil {
|
||||
t.Fatalf("set key of %s: %v", channel, err)
|
||||
}
|
||||
}
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
bob.sendAndExpect("JOIN #keyed", " 475 ")
|
||||
bob.sendAndExpect("JOIN #keyed wrong", " 475 ")
|
||||
|
||||
lines := bob.sendAndExpect("JOIN #keyed sesame", " 366 ")
|
||||
assertContains(
|
||||
t, lines, "JOIN #keyed", "bob joins with the key",
|
||||
)
|
||||
|
||||
carol := env.dial(t)
|
||||
carol.register("carol")
|
||||
|
||||
lines = carol.sendAndExpect(
|
||||
"JOIN #keyed,#vault sesame,barley", " 366 carol #vault ",
|
||||
)
|
||||
assertContains(t, lines, "JOIN #keyed", "carol joins #keyed")
|
||||
assertContains(t, lines, "JOIN #vault", "carol joins #vault")
|
||||
assertNotContains(t, lines, " 475 ", "carol's JOIN")
|
||||
}
|
||||
|
||||
// TestIntegrationModeInviteOnly checks that INVITE lets a
|
||||
// user into a +i channel.
|
||||
func TestIntegrationModeInviteOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#invonly")
|
||||
alice.sendAndExpect("MODE #invonly +i", "+i")
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
bob.sendAndExpect("JOIN #invonly", " 473 ")
|
||||
|
||||
alice.sendAndExpect("INVITE bob #invonly", " 341 ")
|
||||
|
||||
lines := bob.sendAndExpect("JOIN #invonly", " 366 ")
|
||||
assertContains(
|
||||
t, lines, "JOIN #invonly", "bob joins once invited",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationModeInviteOnlyNeedsOperator checks that on
|
||||
// a +i channel an INVITE from a member who is not a channel
|
||||
// operator gets 482 and does not let the invited user in.
|
||||
func TestIntegrationModeInviteOnlyNeedsOperator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#invonly")
|
||||
|
||||
// Bob joins before +i is set, so he is a member but
|
||||
// not an operator.
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
bob.joinAndDrain("#invonly")
|
||||
|
||||
alice.sendAndExpect("MODE #invonly +i", "+i")
|
||||
|
||||
carol := env.dial(t)
|
||||
carol.register("carol")
|
||||
|
||||
lines := bob.sendAndExpect("INVITE carol #invonly", " 482 ")
|
||||
assertContains(
|
||||
t, lines, " 482 bob #invonly ", "bob's INVITE refused",
|
||||
)
|
||||
|
||||
carol.sendAndExpect("JOIN #invonly", " 473 ")
|
||||
}
|
||||
|
||||
// TestIntegrationInviteUserOnChannel checks that inviting a
|
||||
// user who is already on the channel gets 443.
|
||||
func TestIntegrationInviteUserOnChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#room")
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
bob.joinAndDrain("#room")
|
||||
|
||||
lines := alice.sendAndExpect("INVITE bob #room", " 443 ")
|
||||
assertContains(
|
||||
t, lines, " 443 alice bob #room ", "alice's INVITE refused",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationModeSecretList checks that LIST shows a +s
|
||||
// channel only to its members.
|
||||
func TestIntegrationModeSecretList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#hidden")
|
||||
alice.joinAndDrain("#visible")
|
||||
alice.sendAndExpect("MODE #hidden +s", "+s")
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
bobList := bob.sendAndExpect("LIST", " 323 ")
|
||||
assertContains(
|
||||
t, bobList, " 322 bob #visible ", "bob's LIST",
|
||||
)
|
||||
assertNotContains(t, bobList, "#hidden", "bob's LIST")
|
||||
|
||||
aliceList := alice.sendAndExpect("LIST", " 323 ")
|
||||
assertContains(
|
||||
t, aliceList, " 322 alice #hidden ", "alice's LIST",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationModeSecretWhois checks that WHOIS names a
|
||||
// +s channel only to users on it.
|
||||
func TestIntegrationModeSecretWhois(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#hidden")
|
||||
alice.joinAndDrain("#visible")
|
||||
alice.sendAndExpect("MODE #hidden +s", "+s")
|
||||
|
||||
bob := env.dial(t)
|
||||
bob.register("bob")
|
||||
|
||||
bobWhois := bob.sendAndExpect("WHOIS alice", " 318 ")
|
||||
assertContains(t, bobWhois, "@#visible", "bob's WHOIS")
|
||||
assertNotContains(t, bobWhois, "#hidden", "bob's WHOIS")
|
||||
|
||||
carol := env.dial(t)
|
||||
carol.register("carol")
|
||||
carol.joinAndDrain("#hidden")
|
||||
|
||||
carolWhois := carol.sendAndExpect("WHOIS alice", " 318 ")
|
||||
assertContains(t, carolWhois, "@#hidden", "carol's WHOIS")
|
||||
}
|
||||
|
||||
// TestIntegrationModeHashcash checks that PRIVMSG to a +H
|
||||
// channel is refused, since an IRC client cannot attach a
|
||||
// hashcash stamp.
|
||||
func TestIntegrationModeHashcash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
alice := env.dial(t)
|
||||
alice.register("alice")
|
||||
alice.joinAndDrain("#stamped")
|
||||
alice.sendAndExpect("MODE #stamped +H 2", "+H")
|
||||
|
||||
lines := alice.sendAndExpect(
|
||||
"PRIVMSG #stamped :hello", " 404 ",
|
||||
)
|
||||
assertContains(
|
||||
t, lines, " 404 alice #stamped :Channel requires hashcash",
|
||||
"PRIVMSG refused",
|
||||
)
|
||||
}
|
||||
|
||||
// TestIntegrationThirdClientObserver verifies that a third
|
||||
// client observing the same channel receives messages from
|
||||
// the other two.
|
||||
|
||||
Reference in New Issue
Block a user