mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 21:39:25 +01:00
Merge pull request #1355 from slingamn/invite
security enhancements for INVITE
This commit is contained in:
commit
9c4b086113
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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]
|
||||||
|
if ok {
|
||||||
// joining an invited channel "uses up" your invite, so you can't rejoin on kick
|
// joining an invited channel "uses up" your invite, so you can't rejoin on kick
|
||||||
delete(client.invitedTo, casefoldedChannel)
|
delete(client.invitedTo, casefoldedChannel)
|
||||||
|
}
|
||||||
|
invited = ok && (expTime == time.Duration(0) || now.Sub(curInvite.invitedAt) < expTime) &&
|
||||||
|
createdTime.Equal(curInvite.channelCreatedAt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -578,6 +578,7 @@ type Config struct {
|
|||||||
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"`
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if invite {
|
||||||
channel.Invite(target, client, rb)
|
channel.Invite(target, client, rb)
|
||||||
|
} else {
|
||||||
|
channel.Uninvite(target, client, rb)
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user