mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-22 03:49:27 +01:00
initial implementation of bouncer functionality
This commit is contained in:
parent
a8f04ecc4d
commit
c2faeed4b5
@ -147,6 +147,18 @@ CAPDEFS = [
|
||||
url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
|
||||
standard="IRCv3",
|
||||
),
|
||||
CapDef(
|
||||
identifier="Bouncer",
|
||||
name="oragono.io/bnc",
|
||||
url="https://oragono.io/bnc",
|
||||
standard="Oragono-specific",
|
||||
),
|
||||
CapDef(
|
||||
identifier="ZNCSelfMessage",
|
||||
name="znc.in/self-message",
|
||||
url="https://wiki.znc.in/Query_buffers",
|
||||
standard="ZNC vendor",
|
||||
),
|
||||
]
|
||||
|
||||
def validate_defs():
|
||||
|
@ -221,8 +221,6 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
|
||||
nickMethod := finalEnforcementMethod(nickAccount)
|
||||
skelMethod := finalEnforcementMethod(skelAccount)
|
||||
switch {
|
||||
case nickMethod == NickReservationNone && skelMethod == NickReservationNone:
|
||||
return nickAccount, NickReservationNone
|
||||
case skelMethod == NickReservationNone:
|
||||
return nickAccount, nickMethod
|
||||
case nickMethod == NickReservationNone:
|
||||
@ -234,6 +232,15 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) BouncerAllowed(account string, session *Session) bool {
|
||||
// TODO stub
|
||||
config := am.server.Config()
|
||||
if !config.Accounts.Bouncer.Enabled {
|
||||
return false
|
||||
}
|
||||
return config.Accounts.Bouncer.AllowedByDefault || session.capabilities.Has(caps.Bouncer)
|
||||
}
|
||||
|
||||
// Looks up the enforcement method stored in the database for an account
|
||||
// (typically you want EnforcementStatus instead, which respects the config)
|
||||
func (am *AccountManager) getStoredEnforcementStatus(account string) string {
|
||||
@ -928,9 +935,9 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
}
|
||||
for _, client := range clients {
|
||||
if config.Accounts.RequireSasl.Enabled {
|
||||
client.Quit(client.t("You are no longer authorized to be on this server"))
|
||||
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
|
||||
// destroy acquires a semaphore so we can't call it while holding a lock
|
||||
go client.destroy(false)
|
||||
go client.destroy(false, nil)
|
||||
} else {
|
||||
am.logoutOfAccount(client)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ package caps
|
||||
|
||||
const (
|
||||
// number of recognized capabilities:
|
||||
numCapabs = 22
|
||||
numCapabs = 24
|
||||
// length of the uint64 array that represents the bitset:
|
||||
bitsetLen = 1
|
||||
)
|
||||
@ -100,6 +100,14 @@ const (
|
||||
// UserhostInNames is the IRCv3 capability named "userhost-in-names":
|
||||
// https://ircv3.net/specs/extensions/userhost-in-names-3.2.html
|
||||
UserhostInNames Capability = iota
|
||||
|
||||
// Bouncer is the Oragono-specific capability named "oragono.io/bnc":
|
||||
// https://oragono.io/bnc
|
||||
Bouncer Capability = iota
|
||||
|
||||
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
|
||||
// https://wiki.znc.in/Query_buffers
|
||||
ZNCSelfMessage Capability = iota
|
||||
)
|
||||
|
||||
// `capabilityNames[capab]` is the string name of the capability `capab`
|
||||
@ -127,5 +135,7 @@ var (
|
||||
"draft/setname",
|
||||
"sts",
|
||||
"userhost-in-names",
|
||||
"oragono.io/bnc",
|
||||
"znc.in/self-message",
|
||||
}
|
||||
)
|
||||
|
@ -20,6 +20,16 @@ func NewSet(capabs ...Capability) *Set {
|
||||
return &newSet
|
||||
}
|
||||
|
||||
// NewCompleteSet returns a new Set, with all defined capabilities enabled.
|
||||
func NewCompleteSet() *Set {
|
||||
var newSet Set
|
||||
asSlice := newSet[:]
|
||||
for i := 0; i < numCapabs; i += 1 {
|
||||
utils.BitsetSet(asSlice, uint(i), true)
|
||||
}
|
||||
return &newSet
|
||||
}
|
||||
|
||||
// Enable enables the given capabilities.
|
||||
func (s *Set) Enable(capabs ...Capability) {
|
||||
asSlice := s[:]
|
||||
@ -53,6 +63,16 @@ func (s *Set) Has(capab Capability) bool {
|
||||
return utils.BitsetGet(s[:], uint(capab))
|
||||
}
|
||||
|
||||
// HasAll returns true if the set has all the given capabilities.
|
||||
func (s *Set) HasAll(capabs ...Capability) bool {
|
||||
for _, capab := range capabs {
|
||||
if !s.Has(capab) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Union adds all the capabilities of another set to this set.
|
||||
func (s *Set) Union(other *Set) {
|
||||
utils.BitsetUnion(s[:], other[:])
|
||||
@ -94,3 +114,9 @@ func (s *Set) String(version Version, values *Values) string {
|
||||
|
||||
return strings.Join(strs, " ")
|
||||
}
|
||||
|
||||
// returns whether we should send `znc.in/self-message`-style echo messages
|
||||
// to sessions other than that which originated the message
|
||||
func (capabs *Set) SelfMessagesEnabled() bool {
|
||||
return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage)
|
||||
}
|
||||
|
195
irc/channel.go
195
irc/channel.go
@ -335,8 +335,8 @@ func (channel *Channel) regenerateMembersCache() {
|
||||
|
||||
// Names sends the list of users joined to the channel to the given client.
|
||||
func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
|
||||
isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
|
||||
isUserhostInNames := client.capabilities.Has(caps.UserhostInNames)
|
||||
isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
|
||||
isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
|
||||
|
||||
maxNamLen := 480 - len(client.server.name) - len(client.Nick())
|
||||
var namesLines []string
|
||||
@ -578,28 +578,35 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
}
|
||||
|
||||
for _, member := range channel.Members() {
|
||||
if member == client {
|
||||
continue
|
||||
}
|
||||
if member.capabilities.Has(caps.ExtendedJoin) {
|
||||
member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||
} else {
|
||||
member.Send(nil, details.nickMask, "JOIN", chname)
|
||||
}
|
||||
if givenMode != 0 {
|
||||
member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
||||
for _, session := range member.Sessions() {
|
||||
if session == rb.session {
|
||||
continue
|
||||
} else if client == session.client {
|
||||
channel.playJoinForSession(session)
|
||||
continue
|
||||
}
|
||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||
session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||
} else {
|
||||
session.Send(nil, details.nickMask, "JOIN", chname)
|
||||
}
|
||||
if givenMode != 0 {
|
||||
session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if client.capabilities.Has(caps.ExtendedJoin) {
|
||||
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
||||
rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||
} else {
|
||||
rb.Add(nil, details.nickMask, "JOIN", chname)
|
||||
}
|
||||
|
||||
channel.SendTopic(client, rb, false)
|
||||
|
||||
channel.Names(client, rb)
|
||||
if rb.session.client == client {
|
||||
// don't send topic and names for a SAJOIN of a different client
|
||||
channel.SendTopic(client, rb, false)
|
||||
channel.Names(client, rb)
|
||||
}
|
||||
|
||||
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||
rb.Flush(true)
|
||||
@ -612,6 +619,23 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
}
|
||||
}
|
||||
|
||||
// plays channel join messages (the JOIN line, topic, and names) to a session.
|
||||
// this is used when attaching a new session to an existing client that already has
|
||||
// channels, and also when one session of a client initiates a JOIN and the other
|
||||
// sessions need to receive the state change
|
||||
func (channel *Channel) playJoinForSession(session *Session) {
|
||||
client := session.client
|
||||
sessionRb := NewResponseBuffer(session)
|
||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||
sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name(), client.AccountName(), client.Realname())
|
||||
} else {
|
||||
sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name())
|
||||
}
|
||||
channel.SendTopic(client, sessionRb, false)
|
||||
channel.Names(client, sessionRb)
|
||||
sessionRb.Send(false)
|
||||
}
|
||||
|
||||
// Part parts the given client from this channel, with the given message.
|
||||
func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) {
|
||||
chname := channel.Name()
|
||||
@ -627,6 +651,11 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
||||
member.Send(nil, details.nickMask, "PART", chname, message)
|
||||
}
|
||||
rb.Add(nil, details.nickMask, "PART", chname, message)
|
||||
for _, session := range client.Sessions() {
|
||||
if session != rb.session {
|
||||
session.Send(nil, details.nickMask, "PART", chname, message)
|
||||
}
|
||||
}
|
||||
|
||||
channel.history.Add(history.Item{
|
||||
Type: history.Part,
|
||||
@ -683,24 +712,26 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
||||
accountName := newClient.AccountName()
|
||||
realName := newClient.Realname()
|
||||
for _, member := range channel.Members() {
|
||||
if member.capabilities.Has(caps.Resume) {
|
||||
continue
|
||||
}
|
||||
for _, session := range member.Sessions() {
|
||||
if session.capabilities.Has(caps.Resume) {
|
||||
continue
|
||||
}
|
||||
|
||||
if member.capabilities.Has(caps.ExtendedJoin) {
|
||||
member.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
||||
} else {
|
||||
member.Send(nil, nickMask, "JOIN", channel.name)
|
||||
}
|
||||
if session.capabilities.Has(caps.ExtendedJoin) {
|
||||
session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
||||
} else {
|
||||
session.Send(nil, nickMask, "JOIN", channel.name)
|
||||
}
|
||||
|
||||
if 0 < len(oldModes) {
|
||||
member.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick)
|
||||
if 0 < len(oldModes) {
|
||||
session.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rb := NewResponseBuffer(newClient)
|
||||
rb := NewResponseBuffer(newClient.Sessions()[0])
|
||||
// use blocking i/o to synchronize with the later history replay
|
||||
if newClient.capabilities.Has(caps.ExtendedJoin) {
|
||||
if rb.session.capabilities.Has(caps.ExtendedJoin) {
|
||||
rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
|
||||
} else {
|
||||
rb.Add(nil, nickMask, "JOIN", channel.name)
|
||||
@ -715,7 +746,7 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
|
||||
|
||||
func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
|
||||
items, complete := channel.history.Between(after, before, false, 0)
|
||||
rb := NewResponseBuffer(newClient)
|
||||
rb := NewResponseBuffer(newClient.Sessions()[0])
|
||||
channel.replayHistoryItems(rb, items)
|
||||
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
||||
// warn here if we didn't warn already
|
||||
@ -735,7 +766,7 @@ func stripMaskFromNick(nickMask string) (nick string) {
|
||||
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
|
||||
chname := channel.Name()
|
||||
client := rb.target
|
||||
serverTime := client.capabilities.Has(caps.ServerTime)
|
||||
serverTime := rb.session.capabilities.Has(caps.ServerTime)
|
||||
|
||||
for _, item := range items {
|
||||
var tags map[string]string
|
||||
@ -778,18 +809,19 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
||||
// SendTopic sends the channel topic to the given client.
|
||||
// `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset
|
||||
func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopic bool) {
|
||||
if !channel.hasClient(client) {
|
||||
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel"))
|
||||
return
|
||||
}
|
||||
|
||||
channel.stateMutex.RLock()
|
||||
name := channel.name
|
||||
topic := channel.topic
|
||||
topicSetBy := channel.topicSetBy
|
||||
topicSetTime := channel.topicSetTime
|
||||
_, hasClient := channel.members[client]
|
||||
channel.stateMutex.RUnlock()
|
||||
|
||||
if !hasClient {
|
||||
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel"))
|
||||
return
|
||||
}
|
||||
|
||||
if topic == "" {
|
||||
if sendNoTopic {
|
||||
rb.Add(nil, client.server.name, RPL_NOTOPIC, client.nick, name, client.t("No topic is set"))
|
||||
@ -824,11 +856,14 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
|
||||
channel.topicSetTime = time.Now()
|
||||
channel.stateMutex.Unlock()
|
||||
|
||||
prefix := client.NickMaskString()
|
||||
for _, member := range channel.Members() {
|
||||
if member == client {
|
||||
rb.Add(nil, client.nickMaskString, "TOPIC", channel.name, topic)
|
||||
} else {
|
||||
member.Send(nil, client.nickMaskString, "TOPIC", channel.name, topic)
|
||||
for _, session := range member.Sessions() {
|
||||
if session == rb.session {
|
||||
rb.Add(nil, prefix, "TOPIC", channel.name, topic)
|
||||
} else {
|
||||
session.Send(nil, prefix, "TOPIC", channel.name, topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -880,51 +915,68 @@ func (channel *Channel) SendSplitMessage(command string, minPrefix *modes.Mode,
|
||||
return
|
||||
}
|
||||
|
||||
nickmask := client.NickMaskString()
|
||||
account := client.AccountName()
|
||||
chname := channel.Name()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// for STATUSMSG
|
||||
var minPrefixMode modes.Mode
|
||||
if minPrefix != nil {
|
||||
minPrefixMode = *minPrefix
|
||||
}
|
||||
// send echo-message
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
// TODO this should use `now` as the time for consistency
|
||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||
var tagsToUse map[string]string
|
||||
if client.capabilities.Has(caps.MessageTags) {
|
||||
if rb.session.capabilities.Has(caps.MessageTags) {
|
||||
tagsToUse = clientOnlyTags
|
||||
}
|
||||
nickMaskString := client.NickMaskString()
|
||||
accountName := client.AccountName()
|
||||
if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) {
|
||||
rb.AddFromClient(message.Msgid, nickMaskString, accountName, tagsToUse, command, channel.name)
|
||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||
rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname)
|
||||
} else {
|
||||
rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToUse, command, channel.name, message)
|
||||
rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message)
|
||||
}
|
||||
}
|
||||
// send echo-message to other connected sessions
|
||||
for _, session := range client.Sessions() {
|
||||
if session == rb.session || !session.capabilities.SelfMessagesEnabled() {
|
||||
continue
|
||||
}
|
||||
var tagsToUse map[string]string
|
||||
if session.capabilities.Has(caps.MessageTags) {
|
||||
tagsToUse = clientOnlyTags
|
||||
}
|
||||
if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
|
||||
session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname)
|
||||
} else {
|
||||
session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message)
|
||||
}
|
||||
}
|
||||
|
||||
nickmask := client.NickMaskString()
|
||||
account := client.AccountName()
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
for _, member := range channel.Members() {
|
||||
if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
|
||||
// STATUSMSG
|
||||
continue
|
||||
}
|
||||
// echo-message is handled above, so skip sending the msg to the user themselves as well
|
||||
if member == client {
|
||||
continue
|
||||
}
|
||||
var tagsToUse map[string]string
|
||||
if member.capabilities.Has(caps.MessageTags) {
|
||||
tagsToUse = clientOnlyTags
|
||||
} else if histType == history.Tagmsg {
|
||||
if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
|
||||
// STATUSMSG
|
||||
continue
|
||||
}
|
||||
|
||||
if histType == history.Tagmsg {
|
||||
member.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, channel.name)
|
||||
} else {
|
||||
member.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, channel.name, message)
|
||||
for _, session := range member.Sessions() {
|
||||
var tagsToUse map[string]string
|
||||
if session.capabilities.Has(caps.MessageTags) {
|
||||
tagsToUse = clientOnlyTags
|
||||
} else if histType == history.Tagmsg {
|
||||
continue
|
||||
}
|
||||
|
||||
if histType == history.Tagmsg {
|
||||
session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname)
|
||||
} else {
|
||||
session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1059,9 +1111,15 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
||||
|
||||
clientMask := client.NickMaskString()
|
||||
targetNick := target.Nick()
|
||||
chname := channel.Name()
|
||||
for _, member := range channel.Members() {
|
||||
member.Send(nil, clientMask, "KICK", channel.name, targetNick, comment)
|
||||
for _, session := range member.Sessions() {
|
||||
if session != rb.session {
|
||||
session.Send(nil, clientMask, "KICK", chname, targetNick, comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
rb.Add(nil, clientMask, "KICK", chname, targetNick, comment)
|
||||
|
||||
message := utils.SplitMessage{}
|
||||
message.Message = comment
|
||||
@ -1094,8 +1152,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
|
||||
}
|
||||
|
||||
for _, member := range channel.Members() {
|
||||
if member.capabilities.Has(caps.InviteNotify) && member != inviter && member != invitee && channel.ClientIsAtLeast(member, modes.Halfop) {
|
||||
member.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname)
|
||||
if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {
|
||||
continue
|
||||
}
|
||||
for _, session := range member.Sessions() {
|
||||
if session.capabilities.Has(caps.InviteNotify) {
|
||||
session.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
418
irc/client.go
418
irc/client.go
@ -50,26 +50,16 @@ type Client struct {
|
||||
accountName string // display name of the account: uncasefolded, '*' if not logged in
|
||||
atime time.Time
|
||||
awayMessage string
|
||||
capabilities caps.Set
|
||||
capState caps.State
|
||||
capVersion caps.Version
|
||||
certfp string
|
||||
channels ChannelSet
|
||||
ctime time.Time
|
||||
exitedSnomaskSent bool
|
||||
fakelag Fakelag
|
||||
flags modes.ModeSet
|
||||
hasQuit bool
|
||||
hops int
|
||||
hostname string
|
||||
idletimer IdleTimer
|
||||
invitedTo map[string]bool
|
||||
isDestroyed bool
|
||||
isTor bool
|
||||
isQuitting bool
|
||||
languages []string
|
||||
loginThrottle connection_limits.GenericThrottle
|
||||
maxlenRest uint32
|
||||
nick string
|
||||
nickCasefolded string
|
||||
nickMaskCasefolded string
|
||||
@ -78,7 +68,6 @@ type Client struct {
|
||||
oper *Oper
|
||||
preregNick string
|
||||
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
||||
quitMessage string
|
||||
rawHostname string
|
||||
realname string
|
||||
realIP net.IP
|
||||
@ -91,13 +80,64 @@ type Client struct {
|
||||
sentPassCommand bool
|
||||
server *Server
|
||||
skeleton string
|
||||
socket *Socket
|
||||
sessions []*Session
|
||||
stateMutex sync.RWMutex // tier 1
|
||||
username string
|
||||
vhost string
|
||||
history *history.Buffer
|
||||
}
|
||||
|
||||
// Session is an individual client connection to the server (TCP connection
|
||||
// and associated per-connection data, such as capabilities). There is a
|
||||
// many-one relationship between sessions and clients.
|
||||
type Session struct {
|
||||
client *Client
|
||||
|
||||
socket *Socket
|
||||
idletimer IdleTimer
|
||||
fakelag Fakelag
|
||||
|
||||
quitMessage string
|
||||
|
||||
capabilities caps.Set
|
||||
maxlenRest uint32
|
||||
capState caps.State
|
||||
capVersion caps.Version
|
||||
|
||||
// TODO track per-connection real IP, proxied IP, and hostname here,
|
||||
// so we can list attached sessions and their details
|
||||
}
|
||||
|
||||
// sets the session quit message, if there isn't one already
|
||||
func (sd *Session) SetQuitMessage(message string) (set bool) {
|
||||
if message == "" {
|
||||
if sd.quitMessage == "" {
|
||||
sd.quitMessage = "Connection closed"
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
sd.quitMessage = message
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// set the negotiated message length based on session capabilities
|
||||
func (session *Session) SetMaxlenRest() {
|
||||
maxlenRest := 512
|
||||
if session.capabilities.Has(caps.MaxLine) {
|
||||
maxlenRest = session.client.server.Config().Limits.LineLen.Rest
|
||||
}
|
||||
atomic.StoreUint32(&session.maxlenRest, uint32(maxlenRest))
|
||||
}
|
||||
|
||||
// allow the negotiated message length limit to be read without locks; this is a convenience
|
||||
// so that Session.SendRawMessage doesn't have to acquire any Client locks
|
||||
func (session *Session) MaxlenRest() int {
|
||||
return int(atomic.LoadUint32(&session.maxlenRest))
|
||||
}
|
||||
|
||||
// WhoWas is the subset of client details needed to answer a WHOWAS query
|
||||
type WhoWas struct {
|
||||
nick string
|
||||
@ -125,32 +165,35 @@ func RunNewClient(server *Server, conn clientConn) {
|
||||
// give them 1k of grace over the limit:
|
||||
socket := NewSocket(conn.Conn, fullLineLenLimit+1024, config.Server.MaxSendQBytes)
|
||||
client := &Client{
|
||||
atime: now,
|
||||
capState: caps.NoneState,
|
||||
capVersion: caps.Cap301,
|
||||
channels: make(ChannelSet),
|
||||
ctime: now,
|
||||
isTor: conn.IsTor,
|
||||
languages: server.Languages().Default(),
|
||||
atime: now,
|
||||
channels: make(ChannelSet),
|
||||
ctime: now,
|
||||
isTor: conn.IsTor,
|
||||
languages: server.Languages().Default(),
|
||||
loginThrottle: connection_limits.GenericThrottle{
|
||||
Duration: config.Accounts.LoginThrottling.Duration,
|
||||
Limit: config.Accounts.LoginThrottling.MaxAttempts,
|
||||
},
|
||||
server: server,
|
||||
socket: socket,
|
||||
accountName: "*",
|
||||
nick: "*", // * is used until actual nick is given
|
||||
nickCasefolded: "*",
|
||||
nickMaskString: "*", // * is used until actual nick is given
|
||||
history: history.NewHistoryBuffer(config.History.ClientLength),
|
||||
}
|
||||
|
||||
client.recomputeMaxlens()
|
||||
session := &Session{
|
||||
client: client,
|
||||
socket: socket,
|
||||
capVersion: caps.Cap301,
|
||||
capState: caps.NoneState,
|
||||
}
|
||||
session.SetMaxlenRest()
|
||||
client.sessions = []*Session{session}
|
||||
|
||||
if conn.IsTLS {
|
||||
client.SetMode(modes.TLS, true)
|
||||
// error is not useful to us here anyways so we can ignore it
|
||||
client.certfp, _ = client.socket.CertFP()
|
||||
client.certfp, _ = socket.CertFP()
|
||||
}
|
||||
|
||||
if conn.IsTor {
|
||||
@ -168,7 +211,7 @@ func RunNewClient(server *Server, conn clientConn) {
|
||||
}
|
||||
}
|
||||
|
||||
client.run()
|
||||
client.run(session)
|
||||
}
|
||||
|
||||
func (client *Client) doIdentLookup(conn net.Conn) {
|
||||
@ -214,10 +257,10 @@ func (client *Client) isAuthorized(config *Config) bool {
|
||||
return !config.Accounts.RequireSasl.Enabled || saslSent || utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets)
|
||||
}
|
||||
|
||||
func (client *Client) resetFakelag() {
|
||||
var flc FakelagConfig = client.server.Config().Fakelag
|
||||
flc.Enabled = flc.Enabled && !client.HasRoleCapabs("nofakelag")
|
||||
client.fakelag.Initialize(flc)
|
||||
func (session *Session) resetFakelag() {
|
||||
var flc FakelagConfig = session.client.server.Config().Fakelag
|
||||
flc.Enabled = flc.Enabled && !session.client.HasRoleCapabs("nofakelag")
|
||||
session.fakelag.Initialize(flc)
|
||||
}
|
||||
|
||||
// IP returns the IP address of this client.
|
||||
@ -244,28 +287,7 @@ func (client *Client) IPString() string {
|
||||
// command goroutine
|
||||
//
|
||||
|
||||
func (client *Client) recomputeMaxlens() int {
|
||||
maxlenRest := 512
|
||||
if client.capabilities.Has(caps.MaxLine) {
|
||||
maxlenRest = client.server.Limits().LineLen.Rest
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&client.maxlenRest, uint32(maxlenRest))
|
||||
|
||||
return maxlenRest
|
||||
}
|
||||
|
||||
// allow these negotiated length limits to be read without locks; this is a convenience
|
||||
// so that Client.Send doesn't have to acquire any Client locks
|
||||
func (client *Client) MaxlenRest() int {
|
||||
return int(atomic.LoadUint32(&client.maxlenRest))
|
||||
}
|
||||
|
||||
func (client *Client) run() {
|
||||
var err error
|
||||
var isExiting bool
|
||||
var line string
|
||||
var msg ircmsg.IrcMessage
|
||||
func (client *Client) run(session *Session) {
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -278,27 +300,30 @@ func (client *Client) run() {
|
||||
}
|
||||
}
|
||||
// ensure client connection gets closed
|
||||
client.destroy(false)
|
||||
client.destroy(false, session)
|
||||
}()
|
||||
|
||||
client.idletimer.Initialize(client)
|
||||
session.idletimer.Initialize(session)
|
||||
session.resetFakelag()
|
||||
|
||||
client.nickTimer.Initialize(client)
|
||||
|
||||
client.resetFakelag()
|
||||
isReattach := client.Registered()
|
||||
// don't reset the nick timer during a reattach
|
||||
if !isReattach {
|
||||
client.nickTimer.Initialize(client)
|
||||
}
|
||||
|
||||
firstLine := true
|
||||
|
||||
for {
|
||||
maxlenRest := client.recomputeMaxlens()
|
||||
maxlenRest := session.MaxlenRest()
|
||||
|
||||
line, err = client.socket.Read()
|
||||
line, err := session.socket.Read()
|
||||
if err != nil {
|
||||
quitMessage := "connection closed"
|
||||
if err == errReadQ {
|
||||
quitMessage = "readQ exceeded"
|
||||
}
|
||||
client.Quit(quitMessage)
|
||||
client.Quit(quitMessage, session)
|
||||
break
|
||||
}
|
||||
|
||||
@ -307,10 +332,10 @@ func (client *Client) run() {
|
||||
}
|
||||
|
||||
// special-cased handling of PROXY protocol, see `handleProxyCommand` for details:
|
||||
if firstLine {
|
||||
if !isReattach && firstLine {
|
||||
firstLine = false
|
||||
if strings.HasPrefix(line, "PROXY") {
|
||||
err = handleProxyCommand(client.server, client, line)
|
||||
err = handleProxyCommand(client.server, client, session, line)
|
||||
if err != nil {
|
||||
break
|
||||
} else {
|
||||
@ -319,14 +344,14 @@ func (client *Client) run() {
|
||||
}
|
||||
}
|
||||
|
||||
msg, err = ircmsg.ParseLineStrict(line, true, maxlenRest)
|
||||
msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest)
|
||||
if err == ircmsg.ErrorLineIsEmpty {
|
||||
continue
|
||||
} else if err == ircmsg.ErrorLineTooLong {
|
||||
client.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line too long"))
|
||||
continue
|
||||
} else if err != nil {
|
||||
client.Quit(client.t("Received malformed line"))
|
||||
client.Quit(client.t("Received malformed line"), session)
|
||||
break
|
||||
}
|
||||
|
||||
@ -340,13 +365,24 @@ func (client *Client) run() {
|
||||
continue
|
||||
}
|
||||
|
||||
isExiting = cmd.Run(client.server, client, msg)
|
||||
if isExiting || client.isQuitting {
|
||||
isExiting := cmd.Run(client.server, client, session, msg)
|
||||
if isExiting {
|
||||
break
|
||||
} else if session.client != client {
|
||||
// bouncer reattach
|
||||
session.playReattachMessages()
|
||||
go session.client.run(session)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) playReattachMessages() {
|
||||
for _, channel := range session.client.Channels() {
|
||||
channel.playJoinForSession(session)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// idle, quit, timers and timeouts
|
||||
//
|
||||
@ -359,9 +395,8 @@ func (client *Client) Active() {
|
||||
}
|
||||
|
||||
// Ping sends the client a PING message.
|
||||
func (client *Client) Ping() {
|
||||
client.Send(nil, "", "PING", client.nick)
|
||||
|
||||
func (session *Session) Ping() {
|
||||
session.Send(nil, "", "PING", session.client.Nick())
|
||||
}
|
||||
|
||||
// tryResume tries to resume if the client asked us to.
|
||||
@ -400,6 +435,11 @@ func (client *Client) tryResume() (success bool) {
|
||||
return
|
||||
}
|
||||
|
||||
if 1 < len(oldClient.Sessions()) {
|
||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions"))
|
||||
return
|
||||
}
|
||||
|
||||
err := server.clients.Resume(client, oldClient)
|
||||
if err != nil {
|
||||
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection"))
|
||||
@ -467,17 +507,19 @@ func (client *Client) tryResume() (success bool) {
|
||||
|
||||
// send quit/resume messages to friends
|
||||
for friend := range friends {
|
||||
if friend.capabilities.Has(caps.Resume) {
|
||||
if timestamp.IsZero() {
|
||||
friend.Send(nil, oldNickmask, "RESUMED", username, hostname)
|
||||
for _, session := range friend.Sessions() {
|
||||
if session.capabilities.Has(caps.Resume) {
|
||||
if timestamp.IsZero() {
|
||||
session.Send(nil, oldNickmask, "RESUMED", username, hostname)
|
||||
} else {
|
||||
session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
|
||||
}
|
||||
} else {
|
||||
friend.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
|
||||
}
|
||||
} else {
|
||||
if client.resumeDetails.HistoryIncomplete {
|
||||
friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
|
||||
} else {
|
||||
friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
||||
if client.resumeDetails.HistoryIncomplete {
|
||||
session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
|
||||
} else {
|
||||
session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -509,17 +551,17 @@ func (client *Client) tryResumeChannels() {
|
||||
if !details.Timestamp.IsZero() {
|
||||
now := time.Now()
|
||||
items, complete := client.history.Between(details.Timestamp, now, false, 0)
|
||||
rb := NewResponseBuffer(client)
|
||||
rb := NewResponseBuffer(client.Sessions()[0])
|
||||
client.replayPrivmsgHistory(rb, items, complete)
|
||||
rb.Send(true)
|
||||
}
|
||||
|
||||
details.OldClient.destroy(true)
|
||||
details.OldClient.destroy(true, nil)
|
||||
}
|
||||
|
||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
||||
nick := client.Nick()
|
||||
serverTime := client.capabilities.Has(caps.ServerTime)
|
||||
serverTime := rb.session.capabilities.Has(caps.ServerTime)
|
||||
for _, item := range items {
|
||||
var command string
|
||||
switch item.Type {
|
||||
@ -661,37 +703,27 @@ func (client *Client) ModeString() (str string) {
|
||||
}
|
||||
|
||||
// Friends refers to clients that share a channel with this client.
|
||||
func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
|
||||
friends := make(ClientSet)
|
||||
func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]bool) {
|
||||
result = make(map[*Session]bool)
|
||||
|
||||
// make sure that I have the right caps
|
||||
hasCaps := true
|
||||
for _, capab := range capabs {
|
||||
if !client.capabilities.Has(capab) {
|
||||
hasCaps = false
|
||||
break
|
||||
// look at the client's own sessions
|
||||
for _, session := range client.Sessions() {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
result[session] = true
|
||||
}
|
||||
}
|
||||
if hasCaps {
|
||||
friends.Add(client)
|
||||
}
|
||||
|
||||
for _, channel := range client.Channels() {
|
||||
for _, member := range channel.Members() {
|
||||
// make sure they have all the required caps
|
||||
hasCaps = true
|
||||
for _, capab := range capabs {
|
||||
if !member.capabilities.Has(capab) {
|
||||
hasCaps = false
|
||||
break
|
||||
for _, session := range member.Sessions() {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
result[session] = true
|
||||
}
|
||||
}
|
||||
if hasCaps {
|
||||
friends.Add(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
return friends
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) SetOper(oper *Oper) {
|
||||
@ -816,47 +848,88 @@ func (client *Client) RplISupport(rb *ResponseBuffer) {
|
||||
// Quit sets the given quit message for the client.
|
||||
// (You must ensure separately that destroy() is called, e.g., by returning `true` from
|
||||
// the command handler or calling it yourself.)
|
||||
func (client *Client) Quit(message string) {
|
||||
func (client *Client) Quit(message string, session *Session) {
|
||||
setFinalData := func(sess *Session) {
|
||||
message := sess.quitMessage
|
||||
var finalData []byte
|
||||
// #364: don't send QUIT lines to unregistered clients
|
||||
if client.registered {
|
||||
quitMsg := ircmsg.MakeMessage(nil, client.nickMaskString, "QUIT", message)
|
||||
finalData, _ = quitMsg.LineBytesStrict(false, 512)
|
||||
}
|
||||
|
||||
errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message)
|
||||
errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512)
|
||||
finalData = append(finalData, errorMsgBytes...)
|
||||
|
||||
sess.socket.SetFinalData(finalData)
|
||||
}
|
||||
|
||||
client.stateMutex.Lock()
|
||||
alreadyQuit := client.isQuitting
|
||||
if !alreadyQuit {
|
||||
client.isQuitting = true
|
||||
client.quitMessage = message
|
||||
}
|
||||
registered := client.registered
|
||||
prefix := client.nickMaskString
|
||||
client.stateMutex.Unlock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
if alreadyQuit {
|
||||
return
|
||||
var sessions []*Session
|
||||
if session != nil {
|
||||
sessions = []*Session{session}
|
||||
} else {
|
||||
sessions = client.sessions
|
||||
}
|
||||
|
||||
var finalData []byte
|
||||
// #364: don't send QUIT lines to unregistered clients
|
||||
if registered {
|
||||
quitMsg := ircmsg.MakeMessage(nil, prefix, "QUIT", message)
|
||||
finalData, _ = quitMsg.LineBytesStrict(false, 512)
|
||||
for _, session := range sessions {
|
||||
if session.SetQuitMessage(message) {
|
||||
setFinalData(session)
|
||||
}
|
||||
}
|
||||
|
||||
errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message)
|
||||
errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512)
|
||||
finalData = append(finalData, errorMsgBytes...)
|
||||
|
||||
client.socket.SetFinalData(finalData)
|
||||
}
|
||||
|
||||
// destroy gets rid of a client, removes them from server lists etc.
|
||||
func (client *Client) destroy(beingResumed bool) {
|
||||
// if `session` is nil, destroys the client unconditionally, removing all sessions;
|
||||
// otherwise, destroys one specific session, only destroying the client if it
|
||||
// has no more sessions.
|
||||
func (client *Client) destroy(beingResumed bool, session *Session) {
|
||||
var sessionsToDestroy []*Session
|
||||
|
||||
// allow destroy() to execute at most once
|
||||
client.stateMutex.Lock()
|
||||
isDestroyed := client.isDestroyed
|
||||
client.isDestroyed = true
|
||||
quitMessage := client.quitMessage
|
||||
nickMaskString := client.nickMaskString
|
||||
accountName := client.accountName
|
||||
|
||||
alreadyDestroyed := len(client.sessions) == 0
|
||||
sessionRemoved := false
|
||||
var remainingSessions int
|
||||
if session == nil {
|
||||
sessionRemoved = !alreadyDestroyed
|
||||
sessionsToDestroy = client.sessions
|
||||
client.sessions = nil
|
||||
remainingSessions = 0
|
||||
} else {
|
||||
sessionRemoved, remainingSessions = client.removeSession(session)
|
||||
if sessionRemoved {
|
||||
sessionsToDestroy = []*Session{session}
|
||||
}
|
||||
}
|
||||
var quitMessage string
|
||||
if 0 < len(sessionsToDestroy) {
|
||||
quitMessage = sessionsToDestroy[0].quitMessage
|
||||
}
|
||||
client.stateMutex.Unlock()
|
||||
|
||||
if isDestroyed {
|
||||
if alreadyDestroyed || !sessionRemoved {
|
||||
return
|
||||
}
|
||||
|
||||
for _, session := range sessionsToDestroy {
|
||||
if session.client != client {
|
||||
// session has been attached to a new client; do not destroy it
|
||||
continue
|
||||
}
|
||||
session.idletimer.Stop()
|
||||
session.socket.Close()
|
||||
// send quit/error message to client if they haven't been sent already
|
||||
client.Quit("", session)
|
||||
}
|
||||
|
||||
if remainingSessions != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@ -871,9 +944,6 @@ func (client *Client) destroy(beingResumed bool) {
|
||||
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick))
|
||||
}
|
||||
|
||||
// send quit/error message to client if they haven't been sent already
|
||||
client.Quit("Connection closed")
|
||||
|
||||
if !beingResumed {
|
||||
client.server.whoWas.Append(client.WhoWas())
|
||||
}
|
||||
@ -916,13 +986,10 @@ func (client *Client) destroy(beingResumed bool) {
|
||||
}
|
||||
|
||||
// clean up self
|
||||
client.idletimer.Stop()
|
||||
client.nickTimer.Stop()
|
||||
|
||||
client.server.accounts.Logout(client)
|
||||
|
||||
client.socket.Close()
|
||||
|
||||
// send quit messages to friends
|
||||
if !beingResumed {
|
||||
if client.Registered() {
|
||||
@ -953,16 +1020,12 @@ func (client *Client) destroy(beingResumed bool) {
|
||||
|
||||
// SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
|
||||
// Adds account-tag to the line as well.
|
||||
func (client *Client) SendSplitMsgFromClient(from *Client, tags map[string]string, command, target string, message utils.SplitMessage) {
|
||||
client.sendSplitMsgFromClientInternal(false, time.Time{}, from.NickMaskString(), from.AccountName(), tags, command, target, message)
|
||||
}
|
||||
|
||||
func (client *Client) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
|
||||
if client.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
||||
client.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
|
||||
func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
|
||||
if session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
||||
session.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
|
||||
} else {
|
||||
for _, messagePair := range message.Wrapped {
|
||||
client.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message)
|
||||
session.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -976,22 +1039,32 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags map[string
|
||||
// this is SendFromClient, but directly exposing nickmask and accountName,
|
||||
// for things like history replay and CHGHOST where they no longer (necessarily)
|
||||
// correspond to the current state of a client
|
||||
func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) error {
|
||||
func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
|
||||
for _, session := range client.Sessions() {
|
||||
err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...)
|
||||
if err_ != nil {
|
||||
err = err_
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
|
||||
msg := ircmsg.MakeMessage(tags, nickmask, command, params...)
|
||||
// attach account-tag
|
||||
if client.capabilities.Has(caps.AccountTag) && accountName != "*" {
|
||||
if session.capabilities.Has(caps.AccountTag) && accountName != "*" {
|
||||
msg.SetTag("account", accountName)
|
||||
}
|
||||
// attach message-id
|
||||
if msgid != "" && client.capabilities.Has(caps.MessageTags) {
|
||||
if msgid != "" && session.capabilities.Has(caps.MessageTags) {
|
||||
msg.SetTag("draft/msgid", msgid)
|
||||
}
|
||||
// attach server-time
|
||||
if client.capabilities.Has(caps.ServerTime) {
|
||||
if session.capabilities.Has(caps.ServerTime) {
|
||||
msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
|
||||
}
|
||||
|
||||
return client.SendRawMessage(msg, blocking)
|
||||
return session.SendRawMessage(msg, blocking)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -1008,7 +1081,7 @@ var (
|
||||
)
|
||||
|
||||
// SendRawMessage sends a raw message to the client.
|
||||
func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error {
|
||||
func (session *Session) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error {
|
||||
// use dumb hack to force the last param to be a trailing param if required
|
||||
var usedTrailingHack bool
|
||||
if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 {
|
||||
@ -1021,19 +1094,19 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e
|
||||
}
|
||||
|
||||
// assemble message
|
||||
maxlenRest := client.MaxlenRest()
|
||||
maxlenRest := session.MaxlenRest()
|
||||
line, err := message.LineBytesStrict(false, maxlenRest)
|
||||
if err != nil {
|
||||
logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack())
|
||||
client.server.logger.Error("internal", logline)
|
||||
session.client.server.logger.Error("internal", logline)
|
||||
|
||||
message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
|
||||
message = ircmsg.MakeMessage(nil, session.client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
|
||||
line, _ := message.LineBytesStrict(false, 0)
|
||||
|
||||
if blocking {
|
||||
client.socket.BlockingWrite(line)
|
||||
session.socket.BlockingWrite(line)
|
||||
} else {
|
||||
client.socket.Write(line)
|
||||
session.socket.Write(line)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -1044,43 +1117,40 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
if client.server.logger.IsLoggingRawIO() {
|
||||
if session.client.server.logger.IsLoggingRawIO() {
|
||||
logline := string(line[:len(line)-2]) // strip "\r\n"
|
||||
client.server.logger.Debug("useroutput", client.nick, " ->", logline)
|
||||
session.client.server.logger.Debug("useroutput", session.client.Nick(), " ->", logline)
|
||||
}
|
||||
|
||||
if blocking {
|
||||
return client.socket.BlockingWrite(line)
|
||||
return session.socket.BlockingWrite(line)
|
||||
} else {
|
||||
return client.socket.Write(line)
|
||||
return session.socket.Write(line)
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends an IRC line to the client.
|
||||
func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) error {
|
||||
func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) (err error) {
|
||||
for _, session := range client.Sessions() {
|
||||
err_ := session.Send(tags, prefix, command, params...)
|
||||
if err_ != nil {
|
||||
err = err_
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (session *Session) Send(tags map[string]string, prefix string, command string, params ...string) (err error) {
|
||||
msg := ircmsg.MakeMessage(tags, prefix, command, params...)
|
||||
if client.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
|
||||
if session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
|
||||
msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
|
||||
}
|
||||
return client.SendRawMessage(msg, false)
|
||||
return session.SendRawMessage(msg, false)
|
||||
}
|
||||
|
||||
// Notice sends the client a notice from the server.
|
||||
func (client *Client) Notice(text string) {
|
||||
limit := 400
|
||||
if client.capabilities.Has(caps.MaxLine) {
|
||||
limit = client.server.Limits().LineLen.Rest - 110
|
||||
}
|
||||
lines := utils.WordWrap(text, limit)
|
||||
|
||||
// force blank lines to be sent if we receive them
|
||||
if len(lines) == 0 {
|
||||
lines = []string{""}
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
client.Send(nil, client.server.name, "NOTICE", client.nick, line)
|
||||
}
|
||||
client.Send(nil, client.server.name, "NOTICE", client.Nick(), text)
|
||||
}
|
||||
|
||||
func (client *Client) addChannel(channel *Channel) {
|
||||
|
@ -11,7 +11,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmatch"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
|
||||
"sync"
|
||||
)
|
||||
@ -131,7 +133,7 @@ func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) {
|
||||
}
|
||||
|
||||
// SetNick sets a client's nickname, validating it against nicknames in use
|
||||
func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
||||
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error {
|
||||
newcfnick, err := CasefoldName(newNick)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -142,21 +144,31 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
||||
}
|
||||
|
||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
|
||||
account := client.Account()
|
||||
bouncerAllowed := client.server.accounts.BouncerAllowed(account, session)
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
currentNewEntry := clients.byNick[newcfnick]
|
||||
currentClient := clients.byNick[newcfnick]
|
||||
// the client may just be changing case
|
||||
if currentNewEntry != nil && currentNewEntry != client {
|
||||
return errNicknameInUse
|
||||
if currentClient != nil && currentClient != client {
|
||||
// these conditions forbid reattaching to an existing session:
|
||||
if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
|
||||
return errNicknameInUse
|
||||
}
|
||||
if !currentClient.AddSession(session) {
|
||||
return errNicknameInUse
|
||||
}
|
||||
// successful reattach:
|
||||
return nil
|
||||
}
|
||||
// analogous checks for skeletons
|
||||
skeletonHolder := clients.bySkeleton[newSkeleton]
|
||||
if skeletonHolder != nil && skeletonHolder != client {
|
||||
return errNicknameInUse
|
||||
}
|
||||
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() {
|
||||
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account {
|
||||
return errNicknameReserved
|
||||
}
|
||||
clients.removeInternal(client)
|
||||
@ -179,24 +191,18 @@ func (clients *ClientManager) AllClients() (result []*Client) {
|
||||
}
|
||||
|
||||
// AllWithCaps returns all clients with the given capabilities.
|
||||
func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (set ClientSet) {
|
||||
set = make(ClientSet)
|
||||
|
||||
func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
var client *Client
|
||||
for _, client = range clients.byNick {
|
||||
// make sure they have all the required caps
|
||||
for _, capab := range capabs {
|
||||
if !client.capabilities.Has(capab) {
|
||||
continue
|
||||
for _, client := range clients.byNick {
|
||||
for _, session := range client.Sessions() {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
}
|
||||
|
||||
set.Add(client)
|
||||
}
|
||||
|
||||
return set
|
||||
return
|
||||
}
|
||||
|
||||
// FindAll returns all clients that match the given userhost mask.
|
||||
|
@ -21,7 +21,7 @@ type Command struct {
|
||||
}
|
||||
|
||||
// Run runs this command with the given client/message.
|
||||
func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.IrcMessage) bool {
|
||||
if !client.registered && !cmd.usablePreReg {
|
||||
client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
|
||||
return false
|
||||
@ -40,22 +40,22 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
|
||||
}
|
||||
|
||||
if client.registered {
|
||||
client.fakelag.Touch()
|
||||
session.fakelag.Touch()
|
||||
}
|
||||
|
||||
rb := NewResponseBuffer(client)
|
||||
rb := NewResponseBuffer(session)
|
||||
rb.Label = GetLabel(msg)
|
||||
exiting := cmd.handler(server, client, msg, rb)
|
||||
rb.Send(true)
|
||||
|
||||
// after each command, see if we can send registration to the client
|
||||
if !client.registered {
|
||||
server.tryRegister(client)
|
||||
server.tryRegister(client, session)
|
||||
}
|
||||
|
||||
// most servers do this only for PING/PONG, but we'll do it for any command:
|
||||
if client.registered {
|
||||
client.idletimer.Touch()
|
||||
session.idletimer.Touch()
|
||||
}
|
||||
|
||||
if !cmd.leaveClientIdle {
|
||||
|
@ -66,7 +66,11 @@ type AccountConfig struct {
|
||||
} `yaml:"login-throttling"`
|
||||
SkipServerPassword bool `yaml:"skip-server-password"`
|
||||
NickReservation NickReservationConfig `yaml:"nick-reservation"`
|
||||
VHosts VHostConfig
|
||||
Bouncer struct {
|
||||
Enabled bool
|
||||
AllowedByDefault bool `yaml:"allowed-by-default"`
|
||||
}
|
||||
VHosts VHostConfig
|
||||
}
|
||||
|
||||
// AccountRegistrationConfig controls account registration.
|
||||
|
@ -46,7 +46,7 @@ func (wc *webircConfig) Populate() (err error) {
|
||||
}
|
||||
|
||||
// ApplyProxiedIP applies the given IP to the client.
|
||||
func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) {
|
||||
func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (success bool) {
|
||||
// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
|
||||
// is whitelisted:
|
||||
if client.isTor {
|
||||
@ -56,13 +56,13 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool)
|
||||
// ensure IP is sane
|
||||
parsedProxiedIP := net.ParseIP(proxiedIP).To16()
|
||||
if parsedProxiedIP == nil {
|
||||
client.Quit(fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP))
|
||||
client.Quit(fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP), session)
|
||||
return false
|
||||
}
|
||||
|
||||
isBanned, banMsg := client.server.checkBans(parsedProxiedIP)
|
||||
if isBanned {
|
||||
client.Quit(banMsg)
|
||||
client.Quit(banMsg, session)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -88,10 +88,10 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool)
|
||||
// PROXY TCP[46] SOURCEIP DESTIP SOURCEPORT DESTPORT\r\n
|
||||
// unfortunately, an ipv6 SOURCEIP can start with a double colon; in this case,
|
||||
// the message is invalid IRC and can't be parsed normally, hence the special handling.
|
||||
func handleProxyCommand(server *Server, client *Client, line string) (err error) {
|
||||
func handleProxyCommand(server *Server, client *Client, session *Session, line string) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
client.Quit(client.t("Bad or unauthorized PROXY command"))
|
||||
client.Quit(client.t("Bad or unauthorized PROXY command"), session)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -102,7 +102,7 @@ func handleProxyCommand(server *Server, client *Client, line string) (err error)
|
||||
|
||||
if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {
|
||||
// assume PROXY connections are always secure
|
||||
if client.ApplyProxiedIP(params[2], true) {
|
||||
if client.ApplyProxiedIP(session, params[2], true) {
|
||||
return nil
|
||||
} else {
|
||||
return errBadProxyLine
|
||||
|
@ -62,6 +62,43 @@ func (server *Server) Languages() (lm *languages.Manager) {
|
||||
return server.Config().languageManager
|
||||
}
|
||||
|
||||
func (client *Client) Sessions() (sessions []*Session) {
|
||||
client.stateMutex.RLock()
|
||||
sessions = make([]*Session, len(client.sessions))
|
||||
copy(sessions, client.sessions)
|
||||
client.stateMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) AddSession(session *Session) (success bool) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
if len(client.sessions) == 0 {
|
||||
return false
|
||||
}
|
||||
session.client = client
|
||||
client.sessions = append(client.sessions, session)
|
||||
return true
|
||||
}
|
||||
|
||||
func (client *Client) removeSession(session *Session) (success bool, length int) {
|
||||
if len(client.sessions) == 0 {
|
||||
return
|
||||
}
|
||||
sessions := make([]*Session, 0, len(client.sessions)-1)
|
||||
for _, currentSession := range client.sessions {
|
||||
if session == currentSession {
|
||||
success = true
|
||||
} else {
|
||||
sessions = append(sessions, currentSession)
|
||||
}
|
||||
}
|
||||
client.sessions = sessions
|
||||
length = len(sessions)
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) Nick() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
@ -167,12 +204,6 @@ func (client *Client) SetAwayMessage(message string) {
|
||||
client.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (client *Client) Destroyed() bool {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.isDestroyed
|
||||
}
|
||||
|
||||
func (client *Client) Account() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
185
irc/handlers.go
185
irc/handlers.go
@ -482,14 +482,20 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
Mode: modes.Away,
|
||||
Op: op,
|
||||
}}
|
||||
rb.Add(nil, server.name, "MODE", client.nick, modech.String())
|
||||
|
||||
details := client.Details()
|
||||
modeString := modech.String()
|
||||
rb.Add(nil, server.name, "MODE", details.nick, modeString)
|
||||
|
||||
// dispatch away-notify
|
||||
for friend := range client.Friends(caps.AwayNotify) {
|
||||
for session := range client.Friends(caps.AwayNotify) {
|
||||
if session != rb.session && rb.session.client == client {
|
||||
session.Send(nil, server.name, "MODE", details.nick, modeString)
|
||||
}
|
||||
if isAway {
|
||||
friend.SendFromClient("", client, nil, "AWAY", awayMessage)
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY", awayMessage)
|
||||
} else {
|
||||
friend.SendFromClient("", client, nil, "AWAY")
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY")
|
||||
}
|
||||
}
|
||||
|
||||
@ -527,23 +533,23 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
switch subCommand {
|
||||
case "LS":
|
||||
if !client.registered {
|
||||
client.capState = caps.NegotiatingState
|
||||
rb.session.capState = caps.NegotiatingState
|
||||
}
|
||||
if len(msg.Params) > 1 && msg.Params[1] == "302" {
|
||||
client.capVersion = 302
|
||||
rb.session.capVersion = 302
|
||||
}
|
||||
// weechat 1.4 has a bug here where it won't accept the CAP reply unless it contains
|
||||
// the server.name source... otherwise it doesn't respond to the CAP message with
|
||||
// anything and just hangs on connection.
|
||||
//TODO(dan): limit number of caps and send it multiline in 3.2 style as appropriate.
|
||||
rb.Add(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(client.capVersion, CapValues))
|
||||
rb.Add(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(rb.session.capVersion, CapValues))
|
||||
|
||||
case "LIST":
|
||||
rb.Add(nil, server.name, "CAP", client.nick, subCommand, client.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1
|
||||
rb.Add(nil, server.name, "CAP", client.nick, subCommand, rb.session.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1
|
||||
|
||||
case "REQ":
|
||||
if !client.registered {
|
||||
client.capState = caps.NegotiatingState
|
||||
rb.session.capState = caps.NegotiatingState
|
||||
}
|
||||
|
||||
// make sure all capabilities actually exist
|
||||
@ -551,8 +557,8 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
rb.Add(nil, server.name, "CAP", client.nick, "NAK", capString)
|
||||
return false
|
||||
}
|
||||
client.capabilities.Union(toAdd)
|
||||
client.capabilities.Subtract(toRemove)
|
||||
rb.session.capabilities.Union(toAdd)
|
||||
rb.session.capabilities.Subtract(toRemove)
|
||||
rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString)
|
||||
|
||||
// if this is the first time the client is requesting a resume token,
|
||||
@ -564,9 +570,12 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
}
|
||||
}
|
||||
|
||||
// update maxlenrest, just in case they altered the maxline cap
|
||||
rb.session.SetMaxlenRest()
|
||||
|
||||
case "END":
|
||||
if !client.registered {
|
||||
client.capState = caps.NegotiatedState
|
||||
rb.session.capState = caps.NegotiatedState
|
||||
}
|
||||
|
||||
default:
|
||||
@ -600,7 +609,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
if success && len(items) > 0 {
|
||||
return
|
||||
}
|
||||
newRb := NewResponseBuffer(client)
|
||||
newRb := NewResponseBuffer(rb.session)
|
||||
newRb.Label = rb.Label // same label, new batch
|
||||
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
|
||||
if hist == nil {
|
||||
@ -1006,12 +1015,12 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
|
||||
for _, mcl := range clientsToKill {
|
||||
mcl.exitedSnomaskSent = true
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason))
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
|
||||
if mcl == client {
|
||||
killClient = true
|
||||
} else {
|
||||
// if mcl == client, we kill them below
|
||||
mcl.destroy(false)
|
||||
mcl.destroy(false, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1240,7 +1249,6 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
return false
|
||||
}
|
||||
channelString = msg.Params[1]
|
||||
rb = NewResponseBuffer(target)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1248,9 +1256,6 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
for _, chname := range channels {
|
||||
server.channels.Join(target, chname, "", true, rb)
|
||||
}
|
||||
if client != target {
|
||||
rb.Send(false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1321,8 +1326,8 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s$r was killed by %s $c[grey][$r%s$c[grey]]"), target.nick, client.nick, comment))
|
||||
target.exitedSnomaskSent = true
|
||||
|
||||
target.Quit(quitMsg)
|
||||
target.destroy(false)
|
||||
target.Quit(quitMsg, nil)
|
||||
target.destroy(false, nil)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1447,12 +1452,12 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
|
||||
for _, mcl := range clientsToKill {
|
||||
mcl.exitedSnomaskSent = true
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason))
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
|
||||
if mcl == client {
|
||||
killClient = true
|
||||
} else {
|
||||
// if mcl == client, we kill them below
|
||||
mcl.destroy(false)
|
||||
mcl.destroy(false, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1660,19 +1665,25 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
}
|
||||
|
||||
// send out changes
|
||||
prefix := client.NickMaskString()
|
||||
if len(applied) > 0 {
|
||||
//TODO(dan): we should change the name of String and make it return a slice here
|
||||
args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
|
||||
for _, member := range channel.Members() {
|
||||
if member == client {
|
||||
rb.Add(nil, client.nickMaskString, "MODE", args...)
|
||||
rb.Add(nil, prefix, "MODE", args...)
|
||||
for _, session := range client.Sessions() {
|
||||
if session != rb.session {
|
||||
session.Send(nil, prefix, "MODE", args...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
member.Send(nil, client.nickMaskString, "MODE", args...)
|
||||
member.Send(nil, prefix, "MODE", args...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...)
|
||||
rb.Add(nil, client.nickMaskString, RPL_CHANNELMODEIS, args...)
|
||||
rb.Add(nil, prefix, RPL_CHANNELMODEIS, args...)
|
||||
rb.Add(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10))
|
||||
}
|
||||
return false
|
||||
@ -1913,7 +1924,7 @@ func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
// NICK <nickname>
|
||||
func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
if client.registered {
|
||||
performNickChange(server, client, client, msg.Params[0], rb)
|
||||
performNickChange(server, client, client, nil, msg.Params[0], rb)
|
||||
} else {
|
||||
client.preregNick = msg.Params[0]
|
||||
}
|
||||
@ -1953,7 +1964,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
|
||||
for i, targetString := range targets {
|
||||
// each target gets distinct msgids
|
||||
splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine))
|
||||
splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
|
||||
now := time.Now().UTC()
|
||||
|
||||
// max of four targets per privmsg
|
||||
@ -1992,10 +2003,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
}
|
||||
tnick := user.Nick()
|
||||
|
||||
if histType == history.Tagmsg && !user.capabilities.Has(caps.MessageTags) {
|
||||
continue // nothing to do
|
||||
}
|
||||
|
||||
nickMaskString := client.NickMaskString()
|
||||
accountName := client.AccountName()
|
||||
// restrict messages appropriately when +R is set
|
||||
@ -2003,19 +2010,36 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
|
||||
allowedTor := !user.isTor || !isRestrictedCTCPMessage(message)
|
||||
if allowedPlusR && allowedTor {
|
||||
if histType == history.Tagmsg {
|
||||
user.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||
} else {
|
||||
user.SendSplitMsgFromClient(client, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||
for _, session := range user.Sessions() {
|
||||
if histType == history.Tagmsg {
|
||||
// don't send TAGMSG at all if they don't have the tags cap
|
||||
if session.capabilities.Has(caps.MessageTags) {
|
||||
session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||
}
|
||||
} else {
|
||||
session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) {
|
||||
// an echo-message may need to be included in the response:
|
||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||
rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||
} else {
|
||||
rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||
}
|
||||
}
|
||||
// an echo-message may need to go out to other client sessions:
|
||||
for _, session := range client.Sessions() {
|
||||
if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() {
|
||||
continue
|
||||
}
|
||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||
session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
|
||||
} else {
|
||||
session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
|
||||
}
|
||||
}
|
||||
if histType != history.Notice && user.HasMode(modes.Away) {
|
||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||
rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage())
|
||||
@ -2084,7 +2108,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
}
|
||||
if !authorized {
|
||||
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
|
||||
client.Quit(client.t("Password incorrect"))
|
||||
client.Quit(client.t("Password incorrect"), rb.session)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2109,7 +2133,9 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name))
|
||||
|
||||
// client may now be unthrottled by the fakelag system
|
||||
client.resetFakelag()
|
||||
for _, session := range client.Sessions() {
|
||||
session.resetFakelag()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@ -2148,7 +2174,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
password := []byte(msg.Params[0])
|
||||
if bcrypt.CompareHashAndPassword(serverPassword, password) != nil {
|
||||
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
|
||||
client.Quit(client.t("Password incorrect"))
|
||||
client.Quit(client.t("Password incorrect"), rb.session)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2180,7 +2206,7 @@ func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
if len(msg.Params) > 0 {
|
||||
reason += ": " + msg.Params[0]
|
||||
}
|
||||
client.Quit(reason)
|
||||
client.Quit(reason, rb.session)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2242,34 +2268,36 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
// send RENAME messages
|
||||
clientPrefix := client.NickMaskString()
|
||||
for _, mcl := range channel.Members() {
|
||||
targetRb := rb
|
||||
targetPrefix := clientPrefix
|
||||
if mcl != client {
|
||||
targetRb = NewResponseBuffer(mcl)
|
||||
targetPrefix = mcl.NickMaskString()
|
||||
}
|
||||
if mcl.capabilities.Has(caps.Rename) {
|
||||
if reason != "" {
|
||||
targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason)
|
||||
} else {
|
||||
targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName)
|
||||
for _, mSession := range mcl.Sessions() {
|
||||
targetRb := rb
|
||||
targetPrefix := clientPrefix
|
||||
if mSession != rb.session {
|
||||
targetRb = NewResponseBuffer(mSession)
|
||||
targetPrefix = mcl.NickMaskString()
|
||||
}
|
||||
} else {
|
||||
if reason != "" {
|
||||
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
|
||||
if mSession.capabilities.Has(caps.Rename) {
|
||||
if reason != "" {
|
||||
targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason)
|
||||
} else {
|
||||
targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName)
|
||||
}
|
||||
} else {
|
||||
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
|
||||
if reason != "" {
|
||||
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
|
||||
} else {
|
||||
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
|
||||
}
|
||||
if mSession.capabilities.Has(caps.ExtendedJoin) {
|
||||
targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname())
|
||||
} else {
|
||||
targetRb.Add(nil, targetPrefix, "JOIN", newName)
|
||||
}
|
||||
channel.SendTopic(mcl, targetRb, false)
|
||||
channel.Names(mcl, targetRb)
|
||||
}
|
||||
if mcl.capabilities.Has(caps.ExtendedJoin) {
|
||||
targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname())
|
||||
} else {
|
||||
targetRb.Add(nil, targetPrefix, "JOIN", newName)
|
||||
if mcl != client {
|
||||
targetRb.Send(false)
|
||||
}
|
||||
channel.SendTopic(mcl, targetRb, false)
|
||||
channel.Names(mcl, targetRb)
|
||||
}
|
||||
if mcl != client {
|
||||
targetRb.Send(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2311,7 +2339,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
rb.Add(nil, server.name, ERR_NOSUCHNICK, client.nick, msg.Params[0], client.t("No such nick"))
|
||||
return false
|
||||
}
|
||||
performNickChange(server, client, target, msg.Params[1], rb)
|
||||
performNickChange(server, client, target, nil, msg.Params[1], rb)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -2334,9 +2362,12 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
client.realname = realname
|
||||
client.stateMutex.Unlock()
|
||||
|
||||
details := client.Details()
|
||||
|
||||
// alert friends
|
||||
for friend := range client.Friends(caps.SetName) {
|
||||
friend.SendFromClient("", client, nil, "SETNAME", realname)
|
||||
now := time.Now().UTC()
|
||||
for session := range client.Friends(caps.SetName) {
|
||||
session.sendFromClientInternal(false, now, "", details.nickMask, details.account, nil, "SETNAME", details.realname)
|
||||
}
|
||||
|
||||
return false
|
||||
@ -2519,7 +2550,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
lkey := strings.ToLower(key)
|
||||
if lkey == "tls" || lkey == "secure" {
|
||||
// only accept "tls" flag if the gateway's connection to us is secure as well
|
||||
if client.HasMode(modes.TLS) || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) {
|
||||
if client.HasMode(modes.TLS) || client.realIP.IsLoopback() {
|
||||
secure = true
|
||||
}
|
||||
}
|
||||
@ -2543,11 +2574,11 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
if strings.HasPrefix(proxiedIP, "[") && strings.HasSuffix(proxiedIP, "]") {
|
||||
proxiedIP = proxiedIP[1 : len(proxiedIP)-1]
|
||||
}
|
||||
return !client.ApplyProxiedIP(proxiedIP, secure)
|
||||
return !client.ApplyProxiedIP(rb.session, proxiedIP, secure)
|
||||
}
|
||||
}
|
||||
|
||||
client.Quit(client.t("WEBIRC command is not usable from your address or incorrect password given"))
|
||||
client.Quit(client.t("WEBIRC command is not usable from your address or incorrect password given"), rb.session)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2568,8 +2599,6 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
mask = casefoldedMask
|
||||
}
|
||||
|
||||
friends := client.Friends()
|
||||
|
||||
//TODO(dan): is this used and would I put this param in the Modern doc?
|
||||
// if not, can we remove it?
|
||||
//var operatorOnly bool
|
||||
@ -2581,8 +2610,12 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
// TODO implement wildcard matching
|
||||
//TODO(dan): ^ only for opers
|
||||
channel := server.channels.Get(mask)
|
||||
if channel != nil {
|
||||
whoChannel(client, channel, friends, rb)
|
||||
if channel != nil && channel.hasClient(client) {
|
||||
for _, member := range channel.Members() {
|
||||
if !member.HasMode(modes.Invisible) {
|
||||
client.rplWhoReply(channel, member, rb)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for mclient := range server.clients.FindAll(mask) {
|
||||
|
@ -45,7 +45,7 @@ type IdleTimer struct {
|
||||
|
||||
// immutable after construction
|
||||
registerTimeout time.Duration
|
||||
client *Client
|
||||
session *Session
|
||||
|
||||
// mutable
|
||||
idleTimeout time.Duration
|
||||
@ -56,14 +56,19 @@ type IdleTimer struct {
|
||||
|
||||
// Initialize sets up an IdleTimer and starts counting idle time;
|
||||
// if there is no activity from the client, it will eventually be stopped.
|
||||
func (it *IdleTimer) Initialize(client *Client) {
|
||||
it.client = client
|
||||
func (it *IdleTimer) Initialize(session *Session) {
|
||||
it.session = session
|
||||
it.registerTimeout = RegisterTimeout
|
||||
it.idleTimeout, it.quitTimeout = it.recomputeDurations()
|
||||
registered := session.client.Registered()
|
||||
|
||||
it.Lock()
|
||||
defer it.Unlock()
|
||||
it.state = TimerUnregistered
|
||||
if registered {
|
||||
it.state = TimerActive
|
||||
} else {
|
||||
it.state = TimerUnregistered
|
||||
}
|
||||
it.resetTimeout()
|
||||
}
|
||||
|
||||
@ -72,12 +77,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
|
||||
totalTimeout := DefaultTotalTimeout
|
||||
// if they have the resume cap, wait longer before pinging them out
|
||||
// to give them a chance to resume their connection
|
||||
if it.client.capabilities.Has(caps.Resume) {
|
||||
if it.session.capabilities.Has(caps.Resume) {
|
||||
totalTimeout = ResumeableTotalTimeout
|
||||
}
|
||||
|
||||
idleTimeout = DefaultIdleTimeout
|
||||
if it.client.isTor {
|
||||
if it.session.client.isTor {
|
||||
idleTimeout = TorIdleTimeout
|
||||
}
|
||||
|
||||
@ -118,10 +123,10 @@ func (it *IdleTimer) processTimeout() {
|
||||
}()
|
||||
|
||||
if previousState == TimerActive {
|
||||
it.client.Ping()
|
||||
it.session.Ping()
|
||||
} else {
|
||||
it.client.Quit(it.quitMessage(previousState))
|
||||
it.client.destroy(false)
|
||||
it.session.client.Quit(it.quitMessage(previousState), it.session)
|
||||
it.session.client.destroy(false, it.session)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ var (
|
||||
)
|
||||
|
||||
// returns whether the change succeeded or failed
|
||||
func performNickChange(server *Server, client *Client, target *Client, newnick string, rb *ResponseBuffer) bool {
|
||||
func performNickChange(server *Server, client *Client, target *Client, session *Session, newnick string, rb *ResponseBuffer) bool {
|
||||
nickname := strings.TrimSpace(newnick)
|
||||
cfnick, err := CasefoldName(nickname)
|
||||
currentNick := client.Nick()
|
||||
@ -44,8 +44,8 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
|
||||
|
||||
hadNick := target.HasNick()
|
||||
origNickMask := target.NickMaskString()
|
||||
whowas := client.WhoWas()
|
||||
err = client.server.clients.SetNick(target, nickname)
|
||||
whowas := target.WhoWas()
|
||||
err = client.server.clients.SetNick(target, session, nickname)
|
||||
if err == errNicknameInUse {
|
||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
|
||||
return false
|
||||
@ -57,16 +57,16 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
|
||||
return false
|
||||
}
|
||||
|
||||
client.nickTimer.Touch()
|
||||
target.nickTimer.Touch()
|
||||
|
||||
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
|
||||
if hadNick {
|
||||
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
|
||||
target.server.whoWas.Append(whowas)
|
||||
rb.Add(nil, origNickMask, "NICK", nickname)
|
||||
for friend := range target.Friends() {
|
||||
if friend != client {
|
||||
friend.Send(nil, origNickMask, "NICK", nickname)
|
||||
for session := range target.Friends() {
|
||||
if session != rb.session {
|
||||
session.Send(nil, origNickMask, "NICK", nickname)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,8 +86,14 @@ func (server *Server) RandomlyRename(client *Client) {
|
||||
buf := make([]byte, 8)
|
||||
rand.Read(buf)
|
||||
nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
|
||||
rb := NewResponseBuffer(client)
|
||||
performNickChange(server, client, client, nick, rb)
|
||||
sessions := client.Sessions()
|
||||
if len(sessions) == 0 {
|
||||
return
|
||||
}
|
||||
// XXX arbitrarily pick the first session to receive error messages;
|
||||
// all other sessions receive a `NICK` line same as a friend would
|
||||
rb := NewResponseBuffer(sessions[0])
|
||||
performNickChange(server, client, client, nil, nick, rb)
|
||||
rb.Send(false)
|
||||
// technically performNickChange can fail to change the nick,
|
||||
// but if they're still delinquent, the timer will get them later
|
||||
|
@ -229,8 +229,8 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
|
||||
return
|
||||
}
|
||||
|
||||
ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()))
|
||||
ghost.destroy(false)
|
||||
ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil)
|
||||
ghost.destroy(false, nil)
|
||||
}
|
||||
|
||||
func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
|
@ -25,9 +25,10 @@ const (
|
||||
type ResponseBuffer struct {
|
||||
Label string
|
||||
batchID string
|
||||
target *Client
|
||||
messages []ircmsg.IrcMessage
|
||||
finalized bool
|
||||
target *Client
|
||||
session *Session
|
||||
}
|
||||
|
||||
// GetLabel returns the label from the given message.
|
||||
@ -37,9 +38,10 @@ func GetLabel(msg ircmsg.IrcMessage) string {
|
||||
}
|
||||
|
||||
// NewResponseBuffer returns a new ResponseBuffer.
|
||||
func NewResponseBuffer(target *Client) *ResponseBuffer {
|
||||
func NewResponseBuffer(session *Session) *ResponseBuffer {
|
||||
return &ResponseBuffer{
|
||||
target: target,
|
||||
session: session,
|
||||
target: session.client,
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,11 +68,11 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
|
||||
msg.UpdateTags(tags)
|
||||
|
||||
// attach account-tag
|
||||
if rb.target.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
|
||||
if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
|
||||
msg.SetTag("account", fromAccount)
|
||||
}
|
||||
// attach message-id
|
||||
if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
|
||||
if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
|
||||
msg.SetTag("draft/msgid", msgid)
|
||||
}
|
||||
|
||||
@ -79,7 +81,7 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
|
||||
|
||||
// AddSplitMessageFromClient adds a new split message from a specific client to our queue.
|
||||
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
|
||||
if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
||||
if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
|
||||
rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
|
||||
} else {
|
||||
for _, messagePair := range message.Wrapped {
|
||||
@ -110,7 +112,7 @@ func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) {
|
||||
if rb.Label != "" {
|
||||
message.SetTag(caps.LabelTagName, rb.Label)
|
||||
}
|
||||
rb.target.SendRawMessage(message, blocking)
|
||||
rb.session.SendRawMessage(message, blocking)
|
||||
}
|
||||
|
||||
func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
|
||||
@ -120,7 +122,7 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
|
||||
}
|
||||
|
||||
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
|
||||
rb.target.SendRawMessage(message, blocking)
|
||||
rb.session.SendRawMessage(message, blocking)
|
||||
}
|
||||
|
||||
// Send sends all messages in the buffer to the client.
|
||||
@ -146,7 +148,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
||||
useLabel := rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
||||
// use a batch if we have a label, and we either currently have 0 or 2+ messages,
|
||||
// or we are doing a Flush() and we have to assume that there will be more messages
|
||||
// in the future.
|
||||
@ -162,7 +164,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||
// send each message out
|
||||
for _, message := range rb.messages {
|
||||
// attach server-time if needed
|
||||
if rb.target.capabilities.Has(caps.ServerTime) && !message.HasTag("time") {
|
||||
if rb.session.capabilities.Has(caps.ServerTime) && !message.HasTag("time") {
|
||||
message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
|
||||
}
|
||||
|
||||
@ -172,7 +174,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||
}
|
||||
|
||||
// send message out
|
||||
rb.target.SendRawMessage(message, blocking)
|
||||
rb.session.SendRawMessage(message, blocking)
|
||||
}
|
||||
|
||||
// end batch if required
|
||||
|
@ -46,13 +46,14 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||
}
|
||||
|
||||
for _, member := range channel.Members() {
|
||||
if member == client && !client.capabilities.Has(caps.EchoMessage) {
|
||||
continue
|
||||
}
|
||||
if member == client {
|
||||
rb.Add(nil, source, "PRIVMSG", channel.name, message)
|
||||
} else {
|
||||
member.Send(nil, source, "PRIVMSG", channel.name, message)
|
||||
for _, session := range member.Sessions() {
|
||||
if member == client && !session.capabilities.Has(caps.EchoMessage) {
|
||||
continue
|
||||
} else if rb.session == session {
|
||||
rb.Add(nil, source, "PRIVMSG", channel.name, message)
|
||||
} else if member == client || session.capabilities.Has(caps.EchoMessage) {
|
||||
session.Send(nil, source, "PRIVMSG", channel.name, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -71,7 +72,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||
cnick := client.Nick()
|
||||
tnick := user.Nick()
|
||||
user.Send(nil, source, "PRIVMSG", tnick, message)
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||
rb.Add(nil, source, "PRIVMSG", tnick, message)
|
||||
}
|
||||
if user.HasMode(modes.Away) {
|
||||
|
@ -41,8 +41,8 @@ var (
|
||||
supportedChannelModesString = modes.SupportedChannelModes.String()
|
||||
|
||||
// SupportedCapabilities are the caps we advertise.
|
||||
// MaxLine, SASL and STS are set during server startup.
|
||||
SupportedCapabilities = caps.NewSet(caps.Acc, caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.Batch, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.LabeledResponse, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.SetName, caps.UserhostInNames)
|
||||
// MaxLine, SASL and STS may be unset during server startup / rehash.
|
||||
SupportedCapabilities = caps.NewCompleteSet()
|
||||
|
||||
// CapValues are the actual values we advertise to v3.2 clients.
|
||||
// actual values are set during server startup.
|
||||
@ -374,7 +374,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b
|
||||
// server functionality
|
||||
//
|
||||
|
||||
func (server *Server) tryRegister(c *Client) {
|
||||
func (server *Server) tryRegister(c *Client, session *Session) {
|
||||
resumed := false
|
||||
// try to complete registration, either via RESUME token or normally
|
||||
if c.resumeDetails != nil {
|
||||
@ -383,7 +383,7 @@ func (server *Server) tryRegister(c *Client) {
|
||||
}
|
||||
resumed = true
|
||||
} else {
|
||||
if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
|
||||
if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
|
||||
return
|
||||
}
|
||||
|
||||
@ -391,13 +391,13 @@ func (server *Server) tryRegister(c *Client) {
|
||||
// before completing the other registration commands
|
||||
config := server.Config()
|
||||
if !c.isAuthorized(config) {
|
||||
c.Quit(c.t("Bad password"))
|
||||
c.destroy(false)
|
||||
c.Quit(c.t("Bad password"), nil)
|
||||
c.destroy(false, nil)
|
||||
return
|
||||
}
|
||||
|
||||
rb := NewResponseBuffer(c)
|
||||
nickAssigned := performNickChange(server, c, c, c.preregNick, rb)
|
||||
rb := NewResponseBuffer(session)
|
||||
nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb)
|
||||
rb.Send(true)
|
||||
if !nickAssigned {
|
||||
c.preregNick = ""
|
||||
@ -407,20 +407,24 @@ func (server *Server) tryRegister(c *Client) {
|
||||
// check KLINEs
|
||||
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
||||
if isBanned {
|
||||
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")))
|
||||
c.destroy(false)
|
||||
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
||||
c.destroy(false, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// registration has succeeded:
|
||||
c.SetRegistered()
|
||||
reattached := session.client != c
|
||||
|
||||
// count new user in statistics
|
||||
server.stats.ChangeTotal(1)
|
||||
if !reattached {
|
||||
// registration has succeeded:
|
||||
c.SetRegistered()
|
||||
|
||||
if !resumed {
|
||||
server.monitorManager.AlertAbout(c, true)
|
||||
// count new user in statistics
|
||||
server.stats.ChangeTotal(1)
|
||||
|
||||
if !resumed {
|
||||
server.monitorManager.AlertAbout(c, true)
|
||||
}
|
||||
}
|
||||
|
||||
// continue registration
|
||||
@ -436,7 +440,7 @@ func (server *Server) tryRegister(c *Client) {
|
||||
//TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter
|
||||
c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString)
|
||||
|
||||
rb := NewResponseBuffer(c)
|
||||
rb := NewResponseBuffer(session)
|
||||
c.RplISupport(rb)
|
||||
server.MOTD(c, rb)
|
||||
rb.Send(true)
|
||||
@ -480,8 +484,7 @@ func (server *Server) MOTD(client *Client, rb *ResponseBuffer) {
|
||||
}
|
||||
|
||||
// WhoisChannelsNames returns the common channel names between two users.
|
||||
func (client *Client) WhoisChannelsNames(target *Client) []string {
|
||||
isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
|
||||
func (client *Client) WhoisChannelsNames(target *Client, multiPrefix bool) []string {
|
||||
var chstrs []string
|
||||
for _, channel := range target.Channels() {
|
||||
// channel is secret and the target can't see it
|
||||
@ -490,7 +493,7 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
|
||||
continue
|
||||
}
|
||||
}
|
||||
chstrs = append(chstrs, channel.ClientPrefixes(target, isMultiPrefix)+channel.name)
|
||||
chstrs = append(chstrs, channel.ClientPrefixes(target, multiPrefix)+channel.name)
|
||||
}
|
||||
return chstrs
|
||||
}
|
||||
@ -501,7 +504,7 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
|
||||
tnick := targetInfo.nick
|
||||
|
||||
whoischannels := client.WhoisChannelsNames(target)
|
||||
whoischannels := client.WhoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix))
|
||||
if whoischannels != nil {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, cnick, tnick, strings.Join(whoischannels, " "))
|
||||
}
|
||||
@ -555,18 +558,12 @@ func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *Response
|
||||
}
|
||||
|
||||
if channel != nil {
|
||||
flags += channel.ClientPrefixes(client, target.capabilities.Has(caps.MultiPrefix))
|
||||
// TODO is this right?
|
||||
flags += channel.ClientPrefixes(client, rb.session.capabilities.Has(caps.MultiPrefix))
|
||||
channelName = channel.name
|
||||
}
|
||||
rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, strconv.Itoa(client.hops)+" "+client.Realname())
|
||||
}
|
||||
|
||||
func whoChannel(client *Client, channel *Channel, friends ClientSet, rb *ResponseBuffer) {
|
||||
for _, member := range channel.Members() {
|
||||
if !client.HasMode(modes.Invisible) || friends[client] {
|
||||
client.rplWhoReply(channel, member, rb)
|
||||
}
|
||||
}
|
||||
// hardcode a hopcount of 0 for now
|
||||
rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, "0 "+client.Realname())
|
||||
}
|
||||
|
||||
// rehash reloads the config and applies the changes from the config file.
|
||||
@ -691,6 +688,8 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
||||
SupportedCapabilities.Enable(caps.MaxLine)
|
||||
value := fmt.Sprintf("%d", config.Limits.LineLen.Rest)
|
||||
CapValues.Set(caps.MaxLine, value)
|
||||
} else {
|
||||
SupportedCapabilities.Disable(caps.MaxLine)
|
||||
}
|
||||
|
||||
// STS
|
||||
@ -699,20 +698,24 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
||||
stsDisabledByRehash := false
|
||||
stsCurrentCapValue, _ := CapValues.Get(caps.STS)
|
||||
server.logger.Debug("server", "STS Vals", stsCurrentCapValue, stsValue, fmt.Sprintf("server[%v] config[%v]", stsPreviouslyEnabled, config.Server.STS.Enabled))
|
||||
if config.Server.STS.Enabled && !stsPreviouslyEnabled {
|
||||
if config.Server.STS.Enabled {
|
||||
// enabling STS
|
||||
SupportedCapabilities.Enable(caps.STS)
|
||||
addedCaps.Add(caps.STS)
|
||||
CapValues.Set(caps.STS, stsValue)
|
||||
} else if !config.Server.STS.Enabled && stsPreviouslyEnabled {
|
||||
if !stsPreviouslyEnabled {
|
||||
addedCaps.Add(caps.STS)
|
||||
CapValues.Set(caps.STS, stsValue)
|
||||
} else if stsValue != stsCurrentCapValue {
|
||||
// STS policy updated
|
||||
CapValues.Set(caps.STS, stsValue)
|
||||
updatedCaps.Add(caps.STS)
|
||||
}
|
||||
} else {
|
||||
// disabling STS
|
||||
SupportedCapabilities.Disable(caps.STS)
|
||||
removedCaps.Add(caps.STS)
|
||||
stsDisabledByRehash = true
|
||||
} else if config.Server.STS.Enabled && stsPreviouslyEnabled && stsValue != stsCurrentCapValue {
|
||||
// STS policy updated
|
||||
CapValues.Set(caps.STS, stsValue)
|
||||
updatedCaps.Add(caps.STS)
|
||||
if stsPreviouslyEnabled {
|
||||
removedCaps.Add(caps.STS)
|
||||
stsDisabledByRehash = true
|
||||
}
|
||||
}
|
||||
|
||||
// resize history buffers as needed
|
||||
@ -730,7 +733,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
||||
}
|
||||
|
||||
// burst new and removed caps
|
||||
var capBurstClients ClientSet
|
||||
var capBurstSessions []*Session
|
||||
added := make(map[caps.Version]string)
|
||||
var removed string
|
||||
|
||||
@ -741,7 +744,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
||||
removedCaps.Union(updatedCaps)
|
||||
|
||||
if !addedCaps.Empty() || !removedCaps.Empty() {
|
||||
capBurstClients = server.clients.AllWithCaps(caps.CapNotify)
|
||||
capBurstSessions = server.clients.AllWithCaps(caps.CapNotify)
|
||||
|
||||
added[caps.Cap301] = addedCaps.String(caps.Cap301, CapValues)
|
||||
added[caps.Cap302] = addedCaps.String(caps.Cap302, CapValues)
|
||||
@ -749,7 +752,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
||||
removed = removedCaps.String(caps.Cap301, CapValues)
|
||||
}
|
||||
|
||||
for sClient := range capBurstClients {
|
||||
for _, sSession := range capBurstSessions {
|
||||
if stsDisabledByRehash {
|
||||
// remove STS policy
|
||||
//TODO(dan): this is an ugly hack. we can write this better.
|
||||
@ -763,10 +766,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
|
||||
}
|
||||
// DEL caps and then send NEW ones so that updated caps get removed/added correctly
|
||||
if !removedCaps.Empty() {
|
||||
sClient.Send(nil, server.name, "CAP", sClient.nick, "DEL", removed)
|
||||
sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "DEL", removed)
|
||||
}
|
||||
if !addedCaps.Empty() {
|
||||
sClient.Send(nil, server.name, "CAP", sClient.nick, "NEW", added[sClient.capVersion])
|
||||
sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "NEW", added[sSession.capVersion])
|
||||
}
|
||||
}
|
||||
|
||||
|
13
oragono.yaml
13
oragono.yaml
@ -262,6 +262,19 @@ accounts:
|
||||
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
|
||||
rename-prefix: Guest-
|
||||
|
||||
# bouncer controls whether oragono can act as a bouncer, i.e., allowing
|
||||
# multiple connections to attach to the same client/nickname identity
|
||||
bouncer:
|
||||
# when disabled, each connection must use a separate nickname (as is the
|
||||
# typical behavior of IRC servers). when enabled, a new connection that
|
||||
# has authenticated with SASL can associate itself with an existing
|
||||
# client
|
||||
enabled: true
|
||||
|
||||
# clients can opt in to bouncer functionality using the cap system, or
|
||||
# via nickserv. if this is enabled, then they have to opt out instead
|
||||
allowed-by-default: false
|
||||
|
||||
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
||||
# hostname/IP) by the HostServ service
|
||||
vhosts:
|
||||
|
Loading…
Reference in New Issue
Block a user