Browse Source

Merge pull request #1355 from slingamn/invite

security enhancements for INVITE
tags/v2.4.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
9c4b086113
No account linked to committer's email address
8 changed files with 95 additions and 18 deletions
  1. 4
    0
      default.yaml
  2. 35
    8
      irc/channel.go
  3. 33
    8
      irc/client.go
  4. 4
    0
      irc/commands.go
  5. 2
    1
      irc/config.go
  6. 8
    1
      irc/handlers.go
  7. 5
    0
      irc/help.go
  8. 4
    0
      traditional.yaml

+ 4
- 0
default.yaml View File

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

+ 35
- 8
irc/channel.go View File

@@ -677,6 +677,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
677 677
 	chname := channel.name
678 678
 	chcfname := channel.nameCasefolded
679 679
 	founder := channel.registeredFounder
680
+	createdAt := channel.createdTime
680 681
 	chkey := channel.key
681 682
 	limit := channel.userLimit
682 683
 	chcount := len(channel.members)
@@ -695,7 +696,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
695 696
 	// 3. people invited with INVITE can join
696 697
 	hasPrivs := isSajoin || (founder != "" && founder == details.account) ||
697 698
 		(persistentMode != 0 && persistentMode != modes.Voice) ||
698
-		client.CheckInvited(chcfname)
699
+		client.CheckInvited(chcfname, createdAt)
699 700
 	if !hasPrivs {
700 701
 		if limit != 0 && chcount >= limit {
701 702
 			return errLimitExceeded
@@ -1475,23 +1476,33 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
1475 1476
 
1476 1477
 // Invite invites the given client to the channel, if the inviter can do so.
1477 1478
 func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) {
1478
-	chname := channel.Name()
1479
-	if channel.flags.HasMode(modes.InviteOnly) && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
1480
-		rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), chname, inviter.t("You're not a channel operator"))
1479
+	channel.stateMutex.RLock()
1480
+	chname := channel.name
1481
+	chcfname := channel.nameCasefolded
1482
+	createdAt := channel.createdTime
1483
+	_, inviterPresent := channel.members[inviter]
1484
+	_, inviteePresent := channel.members[invitee]
1485
+	channel.stateMutex.RUnlock()
1486
+
1487
+	if !inviterPresent {
1488
+		rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), chname, inviter.t("You're not on that channel"))
1481 1489
 		return
1482 1490
 	}
1483 1491
 
1484
-	if !channel.hasClient(inviter) {
1485
-		rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), chname, inviter.t("You're not on that channel"))
1492
+	inviteOnly := channel.flags.HasMode(modes.InviteOnly)
1493
+	if inviteOnly && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
1494
+		rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), chname, inviter.t("You're not a channel operator"))
1486 1495
 		return
1487 1496
 	}
1488 1497
 
1489
-	if channel.hasClient(invitee) {
1498
+	if inviteePresent {
1490 1499
 		rb.Add(nil, inviter.server.name, ERR_USERONCHANNEL, inviter.Nick(), invitee.Nick(), chname, inviter.t("User is already on that channel"))
1491 1500
 		return
1492 1501
 	}
1493 1502
 
1494
-	invitee.Invite(channel.NameCasefolded())
1503
+	if inviteOnly {
1504
+		invitee.Invite(chcfname, createdAt)
1505
+	}
1495 1506
 
1496 1507
 	for _, member := range channel.Members() {
1497 1508
 		if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {
@@ -1513,6 +1524,22 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
1513 1524
 	}
1514 1525
 }
1515 1526
 
1527
+// Uninvite rescinds a channel invitation, if the inviter can do so.
1528
+func (channel *Channel) Uninvite(invitee *Client, inviter *Client, rb *ResponseBuffer) {
1529
+	if !channel.flags.HasMode(modes.InviteOnly) {
1530
+		rb.Add(nil, channel.server.name, "FAIL", "UNINVITE", "NOT_INVITE_ONLY", channel.Name(), inviter.t("Channel is not invite-only"))
1531
+		return
1532
+	}
1533
+
1534
+	if !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
1535
+		rb.Add(nil, channel.server.name, "FAIL", "UNINVITE", "NOT_PRIVED", channel.Name(), inviter.t("You're not a channel operator"))
1536
+		return
1537
+	}
1538
+
1539
+	invitee.Uninvite(channel.NameCasefolded())
1540
+	rb.Add(nil, channel.server.name, "UNINVITE", invitee.Nick(), channel.Name())
1541
+}
1542
+
1516 1543
 // returns who the client can "see" in the channel, respecting the auditorium mode
1517 1544
 func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) {
1518 1545
 	channel.stateMutex.RLock()

+ 33
- 8
irc/client.go View File

@@ -84,7 +84,7 @@ type Client struct {
84 84
 	destroyed          bool
85 85
 	modes              modes.ModeSet
86 86
 	hostname           string
87
-	invitedTo          utils.StringSet
87
+	invitedTo          map[string]channelInvite
88 88
 	isSTSOnly          bool
89 89
 	languages          []string
90 90
 	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) {
1764 1764
 	}
1765 1765
 }
1766 1766
 
1767
+type channelInvite struct {
1768
+	channelCreatedAt time.Time
1769
+	invitedAt        time.Time
1770
+}
1771
+
1767 1772
 // Records that the client has been invited to join an invite-only channel
1768
-func (client *Client) Invite(casefoldedChannel string) {
1773
+func (client *Client) Invite(casefoldedChannel string, channelCreatedAt time.Time) {
1774
+	now := time.Now().UTC()
1769 1775
 	client.stateMutex.Lock()
1770 1776
 	defer client.stateMutex.Unlock()
1771 1777
 
1772 1778
 	if client.invitedTo == nil {
1773
-		client.invitedTo = make(utils.StringSet)
1779
+		client.invitedTo = make(map[string]channelInvite)
1780
+	}
1781
+
1782
+	client.invitedTo[casefoldedChannel] = channelInvite{
1783
+		channelCreatedAt: channelCreatedAt,
1784
+		invitedAt:        now,
1774 1785
 	}
1775 1786
 
1776
-	client.invitedTo.Add(casefoldedChannel)
1787
+	return
1788
+}
1789
+
1790
+func (client *Client) Uninvite(casefoldedChannel string) {
1791
+	client.stateMutex.Lock()
1792
+	defer client.stateMutex.Unlock()
1793
+	delete(client.invitedTo, casefoldedChannel)
1777 1794
 }
1778 1795
 
1779 1796
 // Checks that the client was invited to join a given channel
1780
-func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) {
1797
+func (client *Client) CheckInvited(casefoldedChannel string, createdTime time.Time) (invited bool) {
1798
+	config := client.server.Config()
1799
+	expTime := time.Duration(config.Channels.InviteExpiration)
1800
+	now := time.Now().UTC()
1801
+
1781 1802
 	client.stateMutex.Lock()
1782 1803
 	defer client.stateMutex.Unlock()
1783 1804
 
1784
-	invited = client.invitedTo.Has(casefoldedChannel)
1785
-	// joining an invited channel "uses up" your invite, so you can't rejoin on kick
1786
-	delete(client.invitedTo, casefoldedChannel)
1805
+	curInvite, ok := client.invitedTo[casefoldedChannel]
1806
+	if ok {
1807
+		// joining an invited channel "uses up" your invite, so you can't rejoin on kick
1808
+		delete(client.invitedTo, casefoldedChannel)
1809
+	}
1810
+	invited = ok && (expTime == time.Duration(0) || now.Sub(curInvite.invitedAt) < expTime) &&
1811
+		createdTime.Equal(curInvite.channelCreatedAt)
1787 1812
 	return
1788 1813
 }
1789 1814
 

+ 4
- 0
irc/commands.go View File

@@ -324,6 +324,10 @@ func init() {
324 324
 			minParams: 1,
325 325
 			oper:      true,
326 326
 		},
327
+		"UNINVITE": {
328
+			handler:   inviteHandler,
329
+			minParams: 2,
330
+		},
327 331
 		"UNKLINE": {
328 332
 			handler:   unKLineHandler,
329 333
 			minParams: 1,

+ 2
- 1
irc/config.go View File

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

+ 8
- 1
irc/handlers.go View File

@@ -1113,7 +1113,9 @@ func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1113 1113
 }
1114 1114
 
1115 1115
 // INVITE <nickname> <channel>
1116
+// UNINVITE <nickname> <channel>
1116 1117
 func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1118
+	invite := msg.Command == "INVITE"
1117 1119
 	nickname := msg.Params[0]
1118 1120
 	channelName := msg.Params[1]
1119 1121
 
@@ -1129,7 +1131,12 @@ func inviteHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
1129 1131
 		return false
1130 1132
 	}
1131 1133
 
1132
-	channel.Invite(target, client, rb)
1134
+	if invite {
1135
+		channel.Invite(target, client, rb)
1136
+	} else {
1137
+		channel.Uninvite(target, client, rb)
1138
+	}
1139
+
1133 1140
 	return false
1134 1141
 }
1135 1142
 

+ 5
- 0
irc/help.go View File

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

+ 4
- 0
traditional.yaml View File

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

Loading…
Cancel
Save