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

initial implementation of bouncer functionality

This commit is contained in:
Shivaram Lingamneni 2019-04-12 00:08:46 -04:00
parent a8f04ecc4d
commit c2faeed4b5
19 changed files with 733 additions and 441 deletions

View File

@ -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():

View File

@ -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)
}

View File

@ -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",
}
)

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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.

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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])
}
}

View File

@ -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: