3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-25 13:29:27 +01:00

Merge pull request #1355 from slingamn/invite

security enhancements for INVITE
This commit is contained in:
Shivaram Lingamneni 2020-10-26 13:30:41 -07:00 committed by GitHub
commit 9c4b086113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 96 additions and 19 deletions

View File

@ -556,6 +556,10 @@ channels:
# than this value will get an empty response to /LIST (a time period of 0 disables) # than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time
# (0 or omit for no expiration):
invite-expiration: 24h
# operator classes # operator classes
oper-classes: oper-classes:
# local operator # local operator

View File

@ -677,6 +677,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
chname := channel.name chname := channel.name
chcfname := channel.nameCasefolded chcfname := channel.nameCasefolded
founder := channel.registeredFounder founder := channel.registeredFounder
createdAt := channel.createdTime
chkey := channel.key chkey := channel.key
limit := channel.userLimit limit := channel.userLimit
chcount := len(channel.members) chcount := len(channel.members)
@ -695,7 +696,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
// 3. people invited with INVITE can join // 3. people invited with INVITE can join
hasPrivs := isSajoin || (founder != "" && founder == details.account) || hasPrivs := isSajoin || (founder != "" && founder == details.account) ||
(persistentMode != 0 && persistentMode != modes.Voice) || (persistentMode != 0 && persistentMode != modes.Voice) ||
client.CheckInvited(chcfname) client.CheckInvited(chcfname, createdAt)
if !hasPrivs { if !hasPrivs {
if limit != 0 && chcount >= limit { if limit != 0 && chcount >= limit {
return errLimitExceeded return errLimitExceeded
@ -1475,23 +1476,33 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
// Invite invites the given client to the channel, if the inviter can do so. // Invite invites the given client to the channel, if the inviter can do so.
func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) { func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) {
chname := channel.Name() channel.stateMutex.RLock()
if channel.flags.HasMode(modes.InviteOnly) && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) { chname := channel.name
rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), chname, inviter.t("You're not a channel operator")) chcfname := channel.nameCasefolded
return createdAt := channel.createdTime
} _, inviterPresent := channel.members[inviter]
_, inviteePresent := channel.members[invitee]
channel.stateMutex.RUnlock()
if !channel.hasClient(inviter) { if !inviterPresent {
rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), chname, inviter.t("You're not on that channel")) rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), chname, inviter.t("You're not on that channel"))
return return
} }
if channel.hasClient(invitee) { inviteOnly := channel.flags.HasMode(modes.InviteOnly)
if inviteOnly && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), chname, inviter.t("You're not a channel operator"))
return
}
if inviteePresent {
rb.Add(nil, inviter.server.name, ERR_USERONCHANNEL, inviter.Nick(), invitee.Nick(), chname, inviter.t("User is already on that channel")) rb.Add(nil, inviter.server.name, ERR_USERONCHANNEL, inviter.Nick(), invitee.Nick(), chname, inviter.t("User is already on that channel"))
return return
} }
invitee.Invite(channel.NameCasefolded()) if inviteOnly {
invitee.Invite(chcfname, createdAt)
}
for _, member := range channel.Members() { for _, member := range channel.Members() {
if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) { if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {
@ -1513,6 +1524,22 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
} }
} }
// Uninvite rescinds a channel invitation, if the inviter can do so.
func (channel *Channel) Uninvite(invitee *Client, inviter *Client, rb *ResponseBuffer) {
if !channel.flags.HasMode(modes.InviteOnly) {
rb.Add(nil, channel.server.name, "FAIL", "UNINVITE", "NOT_INVITE_ONLY", channel.Name(), inviter.t("Channel is not invite-only"))
return
}
if !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
rb.Add(nil, channel.server.name, "FAIL", "UNINVITE", "NOT_PRIVED", channel.Name(), inviter.t("You're not a channel operator"))
return
}
invitee.Uninvite(channel.NameCasefolded())
rb.Add(nil, channel.server.name, "UNINVITE", invitee.Nick(), channel.Name())
}
// returns who the client can "see" in the channel, respecting the auditorium mode // returns who the client can "see" in the channel, respecting the auditorium mode
func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) { func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) {
channel.stateMutex.RLock() channel.stateMutex.RLock()

View File

@ -84,7 +84,7 @@ type Client struct {
destroyed bool destroyed bool
modes modes.ModeSet modes modes.ModeSet
hostname string hostname string
invitedTo utils.StringSet invitedTo map[string]channelInvite
isSTSOnly bool isSTSOnly bool
languages []string languages []string
lastActive time.Time // last time they sent a command that wasn't PONG or similar lastActive time.Time // last time they sent a command that wasn't PONG or similar
@ -1764,26 +1764,51 @@ func (client *Client) removeChannel(channel *Channel) {
} }
} }
type channelInvite struct {
channelCreatedAt time.Time
invitedAt time.Time
}
// Records that the client has been invited to join an invite-only channel // Records that the client has been invited to join an invite-only channel
func (client *Client) Invite(casefoldedChannel string) { func (client *Client) Invite(casefoldedChannel string, channelCreatedAt time.Time) {
now := time.Now().UTC()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
if client.invitedTo == nil { if client.invitedTo == nil {
client.invitedTo = make(utils.StringSet) client.invitedTo = make(map[string]channelInvite)
} }
client.invitedTo.Add(casefoldedChannel) client.invitedTo[casefoldedChannel] = channelInvite{
channelCreatedAt: channelCreatedAt,
invitedAt: now,
}
return
}
func (client *Client) Uninvite(casefoldedChannel string) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
delete(client.invitedTo, casefoldedChannel)
} }
// Checks that the client was invited to join a given channel // Checks that the client was invited to join a given channel
func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) { func (client *Client) CheckInvited(casefoldedChannel string, createdTime time.Time) (invited bool) {
config := client.server.Config()
expTime := time.Duration(config.Channels.InviteExpiration)
now := time.Now().UTC()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
invited = client.invitedTo.Has(casefoldedChannel) curInvite, ok := client.invitedTo[casefoldedChannel]
// joining an invited channel "uses up" your invite, so you can't rejoin on kick if ok {
delete(client.invitedTo, casefoldedChannel) // joining an invited channel "uses up" your invite, so you can't rejoin on kick
delete(client.invitedTo, casefoldedChannel)
}
invited = ok && (expTime == time.Duration(0) || now.Sub(curInvite.invitedAt) < expTime) &&
createdTime.Equal(curInvite.channelCreatedAt)
return return
} }

View File

@ -324,6 +324,10 @@ func init() {
minParams: 1, minParams: 1,
oper: true, oper: true,
}, },
"UNINVITE": {
handler: inviteHandler,
minParams: 2,
},
"UNKLINE": { "UNKLINE": {
handler: unKLineHandler, handler: unKLineHandler,
minParams: 1, minParams: 1,

View File

@ -577,7 +577,8 @@ type Config struct {
OperatorOnly bool `yaml:"operator-only"` OperatorOnly bool `yaml:"operator-only"`
MaxChannelsPerAccount int `yaml:"max-channels-per-account"` MaxChannelsPerAccount int `yaml:"max-channels-per-account"`
} }
ListDelay time.Duration `yaml:"list-delay"` ListDelay time.Duration `yaml:"list-delay"`
InviteExpiration custime.Duration `yaml:"invite-expiration"`
} }
OperClasses map[string]*OperClassConfig `yaml:"oper-classes"` OperClasses map[string]*OperClassConfig `yaml:"oper-classes"`

View File

@ -1113,7 +1113,9 @@ func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
} }
// INVITE <nickname> <channel> // INVITE <nickname> <channel>
// UNINVITE <nickname> <channel>
func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
invite := msg.Command == "INVITE"
nickname := msg.Params[0] nickname := msg.Params[0]
channelName := msg.Params[1] channelName := msg.Params[1]
@ -1129,7 +1131,12 @@ func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
return false return false
} }
channel.Invite(target, client, rb) if invite {
channel.Invite(target, client, rb)
} else {
channel.Uninvite(target, client, rb)
}
return false return false
} }

View File

@ -541,6 +541,11 @@ For example:
Used in connection registration, sets your username and realname to the given Used in connection registration, sets your username and realname to the given
values (though your username may also be looked up with Ident).`, values (though your username may also be looked up with Ident).`,
},
"uninvite": {
text: `UNINVITE <nickname> <channel>
UNINVITE rescinds a channel invitation sent for an invite-only channel.`,
}, },
"users": { "users": {
text: `USERS [parameters] text: `USERS [parameters]

View File

@ -528,6 +528,10 @@ channels:
# than this value will get an empty response to /LIST (a time period of 0 disables) # than this value will get an empty response to /LIST (a time period of 0 disables)
list-delay: 0s list-delay: 0s
# INVITE to an invite-only channel expires after this amount of time
# (0 or omit for no expiration):
invite-expiration: 24h
# operator classes # operator classes
oper-classes: oper-classes:
# local operator # local operator