Merge pull request #42 from slingamn/isupport_and_302.1
refactor callback/protocol handling
This commit is contained in:
commit
a1d30e7a26
@ -36,7 +36,13 @@ func main() {
|
|||||||
SASLPassword: saslPassword,
|
SASLPassword: saslPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
irc.AddCallback("001", func(e ircevent.Event) { irc.Join(channel) })
|
irc.AddConnectCallback(func(e ircevent.Event) {
|
||||||
|
// attempt to set the BOT mode on ourself:
|
||||||
|
if botMode := irc.ISupport()["BOT"]; botMode != "" {
|
||||||
|
irc.Send("MODE", irc.CurrentNick(), "+"+botMode)
|
||||||
|
}
|
||||||
|
irc.Join(channel)
|
||||||
|
})
|
||||||
irc.AddCallback("JOIN", func(e ircevent.Event) {}) // TODO try to rejoin if we *don't* get this
|
irc.AddCallback("JOIN", func(e ircevent.Event) {}) // TODO try to rejoin if we *don't* get this
|
||||||
irc.AddCallback("PRIVMSG", func(e ircevent.Event) {
|
irc.AddCallback("PRIVMSG", func(e ircevent.Event) {
|
||||||
if len(e.Params) < 2 {
|
if len(e.Params) < 2 {
|
||||||
|
@ -446,13 +446,23 @@ func (irc *Connection) setCurrentNick(nick string) {
|
|||||||
irc.currentNick = nick
|
irc.currentNick = nick
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return IRCv3 CAPs actually enabled on the connection.
|
// Return IRCv3 CAPs actually enabled on the connection, together
|
||||||
func (irc *Connection) AcknowledgedCaps(result []string) {
|
// with their values if applicable. The resulting map is shared,
|
||||||
|
// so do not modify it.
|
||||||
|
func (irc *Connection) AcknowledgedCaps() (result map[string]string) {
|
||||||
irc.stateMutex.Lock()
|
irc.stateMutex.Lock()
|
||||||
defer irc.stateMutex.Unlock()
|
defer irc.stateMutex.Unlock()
|
||||||
result = make([]string, len(irc.acknowledgedCaps))
|
return irc.capsAcked
|
||||||
copy(result[:], irc.acknowledgedCaps[:])
|
}
|
||||||
return
|
|
||||||
|
// Returns the 005 RPL_ISUPPORT tokens sent by the server when the
|
||||||
|
// connection was initiated, parsed into key-value form as a map.
|
||||||
|
// The resulting map is shared, so do not modify it.
|
||||||
|
func (irc *Connection) ISupport() (result map[string]string) {
|
||||||
|
irc.stateMutex.Lock()
|
||||||
|
defer irc.stateMutex.Unlock()
|
||||||
|
// XXX modifications to isupport are not permitted after registration
|
||||||
|
return irc.isupport
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the connection is connected to an IRC server.
|
// Returns true if the connection is connected to an IRC server.
|
||||||
@ -498,7 +508,6 @@ func (irc *Connection) Connect() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mark Server as stopped since there can be an error during connect
|
// mark Server as stopped since there can be an error during connect
|
||||||
irc.acknowledgedCaps = nil
|
|
||||||
irc.running = false
|
irc.running = false
|
||||||
irc.socket = nil
|
irc.socket = nil
|
||||||
irc.currentNick = ""
|
irc.currentNick = ""
|
||||||
@ -588,6 +597,11 @@ func (irc *Connection) Connect() (err error) {
|
|||||||
irc.capsChan = make(chan capResult, len(irc.RequestCaps))
|
irc.capsChan = make(chan capResult, len(irc.RequestCaps))
|
||||||
irc.saslChan = make(chan saslResult, 1)
|
irc.saslChan = make(chan saslResult, 1)
|
||||||
irc.welcomeChan = make(chan empty, 1)
|
irc.welcomeChan = make(chan empty, 1)
|
||||||
|
irc.registered = false
|
||||||
|
irc.isupportPartial = make(map[string]string)
|
||||||
|
irc.isupport = nil
|
||||||
|
irc.capsAcked = make(map[string]string)
|
||||||
|
irc.capsAdvertised = nil
|
||||||
irc.stateMutex.Unlock()
|
irc.stateMutex.Unlock()
|
||||||
|
|
||||||
go irc.readLoop()
|
go irc.readLoop()
|
||||||
@ -642,10 +656,12 @@ func (irc *Connection) negotiateCaps() error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
irc.stateMutex.Lock()
|
irc.stateMutex.Lock()
|
||||||
defer irc.stateMutex.Unlock()
|
defer irc.stateMutex.Unlock()
|
||||||
irc.acknowledgedCaps = acknowledgedCaps
|
for _, c := range acknowledgedCaps {
|
||||||
|
irc.capsAcked[c] = irc.capsAdvertised[c]
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
irc.Send("CAP", "LS")
|
irc.Send("CAP", "LS", "302")
|
||||||
defer func() {
|
defer func() {
|
||||||
irc.Send("CAP", "END")
|
irc.Send("CAP", "END")
|
||||||
}()
|
}()
|
||||||
@ -670,6 +686,11 @@ func (irc *Connection) negotiateCaps() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if irc.UseSASL {
|
if irc.UseSASL {
|
||||||
|
if !sliceContains("sasl", acknowledgedCaps) {
|
||||||
|
return SASLFailed
|
||||||
|
} else {
|
||||||
|
irc.Send("AUTHENTICATE", irc.SASLMech)
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case res := <-irc.saslChan:
|
case res := <-irc.saslChan:
|
||||||
if res.Failed {
|
if res.Failed {
|
||||||
|
@ -4,11 +4,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/goshuirc/irc-go/ircmsg"
|
"github.com/goshuirc/irc-go/ircmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// fake events that we manage specially
|
||||||
|
registrationEvent = "*REGISTRATION"
|
||||||
|
)
|
||||||
|
|
||||||
// Tuple type for uniquely identifying callbacks
|
// Tuple type for uniquely identifying callbacks
|
||||||
type CallbackID struct {
|
type CallbackID struct {
|
||||||
eventCode string
|
eventCode string
|
||||||
@ -16,27 +22,44 @@ type CallbackID struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register a callback to a connection and event code. A callback is a function
|
// Register a callback to a connection and event code. A callback is a function
|
||||||
// which takes only an Event pointer as parameter. Valid event codes are all
|
// which takes only an Event object as parameter. Valid event codes are all
|
||||||
// IRC/CTCP commands and error/response codes. To register a callback for all
|
// IRC/CTCP commands and error/response codes. This function returns the ID of the
|
||||||
// events pass "*" as the event code. This function returns the ID of the
|
|
||||||
// registered callback for later management.
|
// registered callback for later management.
|
||||||
func (irc *Connection) AddCallback(eventCode string, callback func(Event)) CallbackID {
|
func (irc *Connection) AddCallback(eventCode string, callback func(Event)) CallbackID {
|
||||||
|
return irc.addCallback(eventCode, Callback(callback), false, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) addCallback(eventCode string, callback Callback, prepend bool, idNum uint64) CallbackID {
|
||||||
eventCode = strings.ToUpper(eventCode)
|
eventCode = strings.ToUpper(eventCode)
|
||||||
|
if eventCode == "" || strings.HasPrefix(eventCode, "*") {
|
||||||
|
return CallbackID{}
|
||||||
|
}
|
||||||
|
|
||||||
irc.eventsMutex.Lock()
|
irc.eventsMutex.Lock()
|
||||||
defer irc.eventsMutex.Unlock()
|
defer irc.eventsMutex.Unlock()
|
||||||
|
|
||||||
if irc.events == nil {
|
if irc.events == nil {
|
||||||
irc.events = make(map[string]map[uint64]Callback)
|
irc.events = make(map[string][]callbackPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok := irc.events[eventCode]
|
if idNum == 0 {
|
||||||
if !ok {
|
idNum = irc.callbackCounter
|
||||||
irc.events[eventCode] = make(map[uint64]Callback)
|
irc.callbackCounter++
|
||||||
}
|
}
|
||||||
id := CallbackID{eventCode: eventCode, id: irc.idCounter}
|
id := CallbackID{eventCode: eventCode, id: idNum}
|
||||||
irc.idCounter++
|
newPair := callbackPair{id: id.id, callback: callback}
|
||||||
irc.events[eventCode][id.id] = Callback(callback)
|
current := irc.events[eventCode]
|
||||||
|
newList := make([]callbackPair, len(current)+1)
|
||||||
|
start := 0
|
||||||
|
if prepend {
|
||||||
|
newList[start] = newPair
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
copy(newList[start:], current)
|
||||||
|
if !prepend {
|
||||||
|
newList[len(newList)-1] = newPair
|
||||||
|
}
|
||||||
|
irc.events[eventCode] = newList
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +67,27 @@ func (irc *Connection) AddCallback(eventCode string, callback func(Event)) Callb
|
|||||||
func (irc *Connection) RemoveCallback(id CallbackID) {
|
func (irc *Connection) RemoveCallback(id CallbackID) {
|
||||||
irc.eventsMutex.Lock()
|
irc.eventsMutex.Lock()
|
||||||
defer irc.eventsMutex.Unlock()
|
defer irc.eventsMutex.Unlock()
|
||||||
delete(irc.events[id.eventCode], id.id)
|
switch id.eventCode {
|
||||||
|
case registrationEvent:
|
||||||
|
irc.removeCallbackNoMutex(RPL_ENDOFMOTD, id.id)
|
||||||
|
irc.removeCallbackNoMutex(ERR_NOMOTD, id.id)
|
||||||
|
default:
|
||||||
|
irc.removeCallbackNoMutex(id.eventCode, id.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) removeCallbackNoMutex(code string, id uint64) {
|
||||||
|
current := irc.events[code]
|
||||||
|
if len(current) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newList := make([]callbackPair, 0, len(current)-1)
|
||||||
|
for _, p := range current {
|
||||||
|
if p.id != id {
|
||||||
|
newList = append(newList, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
irc.events[code] = newList
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove all callbacks from a given event code.
|
// Remove all callbacks from a given event code.
|
||||||
@ -61,33 +104,32 @@ func (irc *Connection) ReplaceCallback(id CallbackID, callback func(Event)) bool
|
|||||||
irc.eventsMutex.Lock()
|
irc.eventsMutex.Lock()
|
||||||
defer irc.eventsMutex.Unlock()
|
defer irc.eventsMutex.Unlock()
|
||||||
|
|
||||||
if _, ok := irc.events[id.eventCode][id.id]; ok {
|
list := irc.events[id.eventCode]
|
||||||
irc.events[id.eventCode][id.id] = callback
|
for i, p := range list {
|
||||||
return true
|
if p.id == id.id {
|
||||||
|
list[i] = callbackPair{id: id.id, callback: callback}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *Connection) getCallbacks(code string) (result []Callback) {
|
// Convenience function to add a callback that will be called once the
|
||||||
|
// connection is completed (this is traditionally referred to as "connection
|
||||||
|
// registration").
|
||||||
|
func (irc *Connection) AddConnectCallback(callback func(Event)) (id CallbackID) {
|
||||||
|
// XXX: forcibly use the same ID number for both copies of the callback
|
||||||
|
id376 := irc.AddCallback(RPL_ENDOFMOTD, callback)
|
||||||
|
irc.addCallback(ERR_NOMOTD, callback, false, id376.id)
|
||||||
|
return CallbackID{eventCode: registrationEvent, id: id376.id}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) getCallbacks(code string) (result []callbackPair) {
|
||||||
code = strings.ToUpper(code)
|
code = strings.ToUpper(code)
|
||||||
|
|
||||||
irc.eventsMutex.Lock()
|
irc.eventsMutex.Lock()
|
||||||
defer irc.eventsMutex.Unlock()
|
defer irc.eventsMutex.Unlock()
|
||||||
|
return irc.events[code]
|
||||||
cMap := irc.events[code]
|
|
||||||
starMap := irc.events["*"]
|
|
||||||
length := len(cMap) + len(starMap)
|
|
||||||
if length == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result = make([]Callback, 0, length)
|
|
||||||
for _, c := range cMap {
|
|
||||||
result = append(result, c)
|
|
||||||
}
|
|
||||||
for _, c := range starMap {
|
|
||||||
result = append(result, c)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute all callbacks associated with a given event.
|
// Execute all callbacks associated with a given event.
|
||||||
@ -106,12 +148,12 @@ func (irc *Connection) runCallbacks(msg ircmsg.IRCMessage) {
|
|||||||
eventRewriteCTCP(&event)
|
eventRewriteCTCP(&event)
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks := irc.getCallbacks(event.Command)
|
callbackPairs := irc.getCallbacks(event.Command)
|
||||||
|
|
||||||
// just run the callbacks in serial, since it's not safe for them
|
// just run the callbacks in serial, since it's not safe for them
|
||||||
// to take a long time to execute in any case
|
// to take a long time to execute in any case
|
||||||
for _, callback := range callbacks {
|
for _, pair := range callbackPairs {
|
||||||
callback(event)
|
pair.callback(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,12 +178,15 @@ func (irc *Connection) setupCallbacks() {
|
|||||||
|
|
||||||
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
|
// 433: ERR_NICKNAMEINUSE "<nick> :Nickname is already in use"
|
||||||
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
|
// 437: ERR_UNAVAILRESOURCE "<nick/channel> :Nick/channel is temporarily unavailable"
|
||||||
irc.AddCallback("433", irc.handleUnavailableNick)
|
irc.AddCallback(ERR_NICKNAMEINUSE, irc.handleUnavailableNick)
|
||||||
irc.AddCallback("437", irc.handleUnavailableNick)
|
irc.AddCallback(ERR_UNAVAILRESOURCE, irc.handleUnavailableNick)
|
||||||
|
|
||||||
// 1: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
|
// 001: RPL_WELCOME "Welcome to the Internet Relay Network <nick>!<user>@<host>"
|
||||||
// Set irc.currentNick to the actually used nick in this connection.
|
// Set irc.currentNick to the actually used nick in this connection.
|
||||||
irc.AddCallback("001", irc.handleRplWelcome)
|
irc.AddCallback(RPL_WELCOME, irc.handleRplWelcome)
|
||||||
|
|
||||||
|
// 005: RPL_ISUPPORT, conveys supported server features
|
||||||
|
irc.AddCallback(RPL_ISUPPORT, irc.handleISupport)
|
||||||
|
|
||||||
// respond to NICK from the server (in response to our own NICK, or sent unprompted)
|
// respond to NICK from the server (in response to our own NICK, or sent unprompted)
|
||||||
irc.AddCallback("NICK", func(e Event) {
|
irc.AddCallback("NICK", func(e Event) {
|
||||||
@ -156,36 +201,7 @@ func (irc *Connection) setupCallbacks() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
irc.AddCallback("CAP", func(e Event) {
|
irc.AddCallback("CAP", irc.handleCAP)
|
||||||
if len(e.Params) != 3 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
command := e.Params[1]
|
|
||||||
capsChan := irc.capsChan
|
|
||||||
|
|
||||||
// TODO this assumes all the caps on one line
|
|
||||||
// TODO support CAP LS 302
|
|
||||||
if command == "LS" {
|
|
||||||
capsList := strings.Fields(e.Params[2])
|
|
||||||
for _, capName := range irc.RequestCaps {
|
|
||||||
if sliceContains(capName, capsList) {
|
|
||||||
irc.Send("CAP", "REQ", capName)
|
|
||||||
} else {
|
|
||||||
select {
|
|
||||||
case capsChan <- capResult{capName, false}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if command == "ACK" || command == "NAK" {
|
|
||||||
for _, capName := range strings.Fields(e.Params[2]) {
|
|
||||||
select {
|
|
||||||
case capsChan <- capResult{capName, command == "ACK"}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if irc.UseSASL {
|
if irc.UseSASL {
|
||||||
irc.setupSASLCallbacks()
|
irc.setupSASLCallbacks()
|
||||||
@ -194,6 +210,11 @@ func (irc *Connection) setupCallbacks() {
|
|||||||
if irc.EnableCTCP {
|
if irc.EnableCTCP {
|
||||||
irc.setupCTCPCallbacks()
|
irc.setupCTCPCallbacks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepend our own callbacks for the end of registration,
|
||||||
|
// so they happen before any client-added callbacks
|
||||||
|
irc.addCallback(RPL_ENDOFMOTD, irc.handleRegistration, true, 0)
|
||||||
|
irc.addCallback(ERR_NOMOTD, irc.handleRegistration, true, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *Connection) handleRplWelcome(e Event) {
|
func (irc *Connection) handleRplWelcome(e Event) {
|
||||||
@ -204,12 +225,29 @@ func (irc *Connection) handleRplWelcome(e Event) {
|
|||||||
if len(e.Params) > 0 {
|
if len(e.Params) > 0 {
|
||||||
irc.currentNick = e.Params[0]
|
irc.currentNick = e.Params[0]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) handleRegistration(e Event) {
|
||||||
// wake up Connect() if applicable
|
// wake up Connect() if applicable
|
||||||
select {
|
defer func() {
|
||||||
case irc.welcomeChan <- empty{}:
|
select {
|
||||||
default:
|
case irc.welcomeChan <- empty{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
irc.stateMutex.Lock()
|
||||||
|
defer irc.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if irc.registered {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
irc.registered = true
|
||||||
|
|
||||||
|
// mark the isupport complete
|
||||||
|
irc.isupport = irc.isupportPartial
|
||||||
|
irc.isupportPartial = nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *Connection) handleUnavailableNick(e Event) {
|
func (irc *Connection) handleUnavailableNick(e Event) {
|
||||||
@ -229,3 +267,126 @@ func (irc *Connection) handleUnavailableNick(e Event) {
|
|||||||
irc.Send("NICK", nickToTry)
|
irc.Send("NICK", nickToTry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) handleISupport(e Event) {
|
||||||
|
irc.stateMutex.Lock()
|
||||||
|
defer irc.stateMutex.Unlock()
|
||||||
|
|
||||||
|
// TODO handle 005 changes after registration
|
||||||
|
if irc.isupportPartial == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(e.Params) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, token := range e.Params[1 : len(e.Params)-1] {
|
||||||
|
equalsIdx := strings.IndexByte(token, '=')
|
||||||
|
if equalsIdx == -1 {
|
||||||
|
irc.isupportPartial[token] = "" // no value
|
||||||
|
} else {
|
||||||
|
irc.isupportPartial[token[:equalsIdx]] = unescapeISupportValue(token[equalsIdx+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unescapeISupportValue(in string) (out string) {
|
||||||
|
if strings.IndexByte(in, '\\') == -1 {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
var buf strings.Builder
|
||||||
|
for i := 0; i < len(in); {
|
||||||
|
if in[i] == '\\' && i+3 < len(in) && in[i+1] == 'x' {
|
||||||
|
hex := in[i+2 : i+4]
|
||||||
|
if octet, err := strconv.ParseInt(hex, 16, 8); err == nil {
|
||||||
|
buf.WriteByte(byte(octet))
|
||||||
|
i += 4
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteByte(in[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) handleCAP(e Event) {
|
||||||
|
if len(e.Params) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ack := false
|
||||||
|
// CAP <NICK | * > <SUBCOMMAND> PARAMS...
|
||||||
|
switch e.Params[1] {
|
||||||
|
case "LS":
|
||||||
|
irc.handleCAPLS(e.Params[2:])
|
||||||
|
case "ACK":
|
||||||
|
ack = true
|
||||||
|
fallthrough
|
||||||
|
case "NAK":
|
||||||
|
for _, token := range strings.Fields(e.Params[2]) {
|
||||||
|
name, _ := splitCAPToken(token)
|
||||||
|
if sliceContains(name, irc.RequestCaps) {
|
||||||
|
select {
|
||||||
|
case irc.capsChan <- capResult{capName: name, ack: ack}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irc *Connection) handleCAPLS(params []string) {
|
||||||
|
var capsToReq, capsNotFound []string
|
||||||
|
defer func() {
|
||||||
|
for _, c := range capsToReq {
|
||||||
|
irc.Send("CAP", "REQ", c)
|
||||||
|
}
|
||||||
|
for _, c := range capsNotFound {
|
||||||
|
select {
|
||||||
|
case irc.capsChan <- capResult{capName: c, ack: false}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
irc.stateMutex.Lock()
|
||||||
|
defer irc.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if irc.registered {
|
||||||
|
// TODO server could probably trick us into panic here by sending
|
||||||
|
// additional LS before the end of registration
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if irc.capsAdvertised == nil {
|
||||||
|
irc.capsAdvertised = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiline responses to CAP LS 302 start with a 4-parameter form:
|
||||||
|
// CAP * LS * :account-notify away-notify [...]
|
||||||
|
// and end with a 3-parameter form:
|
||||||
|
// CAP * LS :userhost-in-names znc.in/playback [...]
|
||||||
|
final := len(params) == 1
|
||||||
|
for _, token := range strings.Fields(params[len(params)-1]) {
|
||||||
|
name, value := splitCAPToken(token)
|
||||||
|
irc.capsAdvertised[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if final {
|
||||||
|
for _, c := range irc.RequestCaps {
|
||||||
|
if _, ok := irc.capsAdvertised[c]; ok {
|
||||||
|
capsToReq = append(capsToReq, c)
|
||||||
|
} else {
|
||||||
|
capsNotFound = append(capsNotFound, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitCAPToken(token string) (name, value string) {
|
||||||
|
equalIdx := strings.IndexByte(token, '=')
|
||||||
|
if equalIdx == -1 {
|
||||||
|
return token, ""
|
||||||
|
} else {
|
||||||
|
return token[:equalIdx], token[equalIdx+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -18,3 +18,19 @@ func TestParse(t *testing.T) {
|
|||||||
t.Fatal("Parse failed: host")
|
t.Fatal("Parse failed: host")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertEqual(found, expected string, t *testing.T) {
|
||||||
|
if found != expected {
|
||||||
|
t.Errorf("expected `%s`, got `%s`\n", expected, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnescapeIsupport(t *testing.T) {
|
||||||
|
assertEqual(unescapeISupportValue(""), "", t)
|
||||||
|
assertEqual(unescapeISupportValue("a"), "a", t)
|
||||||
|
assertEqual(unescapeISupportValue(`\x20`), " ", t)
|
||||||
|
assertEqual(unescapeISupportValue(`\x20b`), " b", t)
|
||||||
|
assertEqual(unescapeISupportValue(`a\x20`), "a ", t)
|
||||||
|
assertEqual(unescapeISupportValue(`a\x20b`), "a b", t)
|
||||||
|
assertEqual(unescapeISupportValue(`\x20\x20`), " ", t)
|
||||||
|
}
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type saslResult struct {
|
type saslResult struct {
|
||||||
@ -21,16 +20,6 @@ func sliceContains(str string, list []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a space-separated list of arguments contains a value.
|
|
||||||
func listContains(list string, value string) bool {
|
|
||||||
for _, arg_name := range strings.Split(strings.TrimSpace(list), " ") {
|
|
||||||
if arg_name == value {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (irc *Connection) submitSASLResult(r saslResult) {
|
func (irc *Connection) submitSASLResult(r saslResult) {
|
||||||
select {
|
select {
|
||||||
case irc.saslChan <- r:
|
case irc.saslChan <- r:
|
||||||
@ -39,43 +28,35 @@ func (irc *Connection) submitSASLResult(r saslResult) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (irc *Connection) setupSASLCallbacks() {
|
func (irc *Connection) setupSASLCallbacks() {
|
||||||
irc.AddCallback("CAP", func(e Event) {
|
|
||||||
if len(e.Params) == 3 {
|
|
||||||
if e.Params[1] == "LS" {
|
|
||||||
if !listContains(e.Params[2], "sasl") {
|
|
||||||
irc.submitSASLResult(saslResult{true, errors.New("no SASL capability " + e.Params[2])})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e.Params[1] == "ACK" && listContains(e.Params[2], "sasl") {
|
|
||||||
irc.Send("AUTHENTICATE", irc.SASLMech)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
irc.AddCallback("AUTHENTICATE", func(e Event) {
|
irc.AddCallback("AUTHENTICATE", func(e Event) {
|
||||||
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
|
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
|
||||||
irc.Send("AUTHENTICATE", str)
|
irc.Send("AUTHENTICATE", str)
|
||||||
})
|
})
|
||||||
|
|
||||||
irc.AddCallback("901", func(e Event) {
|
irc.AddCallback(RPL_LOGGEDOUT, func(e Event) {
|
||||||
irc.SendRaw("CAP END")
|
irc.SendRaw("CAP END")
|
||||||
irc.SendRaw("QUIT")
|
irc.SendRaw("QUIT")
|
||||||
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
|
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
|
||||||
})
|
})
|
||||||
|
|
||||||
irc.AddCallback("902", func(e Event) {
|
irc.AddCallback(ERR_NICKLOCKED, func(e Event) {
|
||||||
irc.SendRaw("CAP END")
|
irc.SendRaw("CAP END")
|
||||||
irc.SendRaw("QUIT")
|
irc.SendRaw("QUIT")
|
||||||
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
|
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
|
||||||
})
|
})
|
||||||
|
|
||||||
irc.AddCallback("903", func(e Event) {
|
irc.AddCallback(RPL_SASLSUCCESS, func(e Event) {
|
||||||
irc.submitSASLResult(saslResult{false, nil})
|
irc.submitSASLResult(saslResult{false, nil})
|
||||||
})
|
})
|
||||||
|
|
||||||
irc.AddCallback("904", func(e Event) {
|
irc.AddCallback(ERR_SASLFAIL, func(e Event) {
|
||||||
irc.SendRaw("CAP END")
|
irc.SendRaw("CAP END")
|
||||||
irc.SendRaw("QUIT")
|
irc.SendRaw("QUIT")
|
||||||
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
|
irc.submitSASLResult(saslResult{true, errors.New(e.Params[1])})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// this could potentially happen with auto-login via certfp?
|
||||||
|
irc.AddCallback(ERR_SASLALREADY, func(e Event) {
|
||||||
|
irc.submitSASLResult(saslResult{false, nil})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func runCAPTest(caps []string, useSASL bool, t *testing.T) {
|
|||||||
irccon.AddCallback("001", func(e Event) { irccon.Join("#go-eventirc") })
|
irccon.AddCallback("001", func(e Event) { irccon.Join("#go-eventirc") })
|
||||||
|
|
||||||
irccon.AddCallback("366", func(e Event) {
|
irccon.AddCallback("366", func(e Event) {
|
||||||
irccon.Privmsg("#go-eventirc", "Test Message SASL\n")
|
irccon.Privmsg("#go-eventirc", "Test Message SASL")
|
||||||
irccon.Quit()
|
irccon.Quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -89,3 +89,18 @@ func TestConnectionNonexistentCAPs(t *testing.T) {
|
|||||||
func TestConnectionGoodCAPs(t *testing.T) {
|
func TestConnectionGoodCAPs(t *testing.T) {
|
||||||
runCAPTest([]string{"server-time", "message-tags"}, false, t)
|
runCAPTest([]string{"server-time", "message-tags"}, false, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSASLFail(t *testing.T) {
|
||||||
|
irccon := connForTesting("go-eventirc", "go-eventirc", true)
|
||||||
|
irccon.Debug = true
|
||||||
|
irccon.UseTLS = true
|
||||||
|
setSaslTestCreds(irccon, t)
|
||||||
|
irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
irccon.AddCallback("001", func(e Event) { irccon.Join("#go-eventirc") })
|
||||||
|
// intentionally break the password
|
||||||
|
irccon.SASLPassword = irccon.SASLPassword + "_"
|
||||||
|
err := irccon.Connect()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("successfully connected with invalid password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,6 +19,11 @@ type empty struct{}
|
|||||||
|
|
||||||
type Callback func(Event)
|
type Callback func(Event)
|
||||||
|
|
||||||
|
type callbackPair struct {
|
||||||
|
id uint64
|
||||||
|
callback Callback
|
||||||
|
}
|
||||||
|
|
||||||
type capResult struct {
|
type capResult struct {
|
||||||
capName string
|
capName string
|
||||||
ack bool
|
ack bool
|
||||||
@ -62,9 +67,13 @@ type Connection struct {
|
|||||||
pingSent bool // we sent PING and are waiting for PONG
|
pingSent bool // we sent PING and are waiting for PONG
|
||||||
|
|
||||||
// IRC protocol connection state
|
// IRC protocol connection state
|
||||||
currentNick string // nickname assigned by the server, empty before registration
|
currentNick string // nickname assigned by the server, empty before registration
|
||||||
acknowledgedCaps []string
|
capsAdvertised map[string]string
|
||||||
nickCounter int
|
capsAcked map[string]string
|
||||||
|
isupport map[string]string
|
||||||
|
isupportPartial map[string]string
|
||||||
|
nickCounter int
|
||||||
|
registered bool
|
||||||
// Connect() builds these with sufficient capacity to receive all expected
|
// Connect() builds these with sufficient capacity to receive all expected
|
||||||
// responses during negotiation. Sends to them are nonblocking, so anything
|
// responses during negotiation. Sends to them are nonblocking, so anything
|
||||||
// sent outside of negotiation will not cause the relevant callbacks to block.
|
// sent outside of negotiation will not cause the relevant callbacks to block.
|
||||||
@ -73,9 +82,13 @@ type Connection struct {
|
|||||||
capsChan chan capResult // transmits the final status of each CAP negotiated
|
capsChan chan capResult // transmits the final status of each CAP negotiated
|
||||||
|
|
||||||
// callback state
|
// callback state
|
||||||
eventsMutex sync.Mutex
|
eventsMutex sync.Mutex
|
||||||
events map[string]map[uint64]Callback
|
events map[string][]callbackPair
|
||||||
idCounter uint64 // assign unique IDs to callbacks
|
// we assign ID numbers to callbacks so they can be removed. normally
|
||||||
|
// the ID number is globally unique (generated by incrementing this counter).
|
||||||
|
// if we add a callback in two places we might reuse the number (XXX)
|
||||||
|
callbackCounter uint64
|
||||||
|
// did we initialize the callbacks needed for the library itself?
|
||||||
hasBaseCallbacks bool
|
hasBaseCallbacks bool
|
||||||
|
|
||||||
Log *log.Logger
|
Log *log.Logger
|
||||||
|
@ -56,27 +56,6 @@ func TestRemoveCallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWildcardCallback(t *testing.T) {
|
|
||||||
irccon := connForTesting("go-eventirc", "go-eventirc", false)
|
|
||||||
debugTest(irccon)
|
|
||||||
|
|
||||||
done := make(chan int, 10)
|
|
||||||
|
|
||||||
irccon.AddCallback("TEST", func(e Event) { done <- 1 })
|
|
||||||
irccon.AddCallback("*", func(e Event) { done <- 2 })
|
|
||||||
|
|
||||||
irccon.runCallbacks(mockEvent("TEST"))
|
|
||||||
|
|
||||||
var results []int
|
|
||||||
|
|
||||||
results = append(results, <-done)
|
|
||||||
results = append(results, <-done)
|
|
||||||
|
|
||||||
if !compareResults(results, 1, 2) {
|
|
||||||
t.Error("Wildcard callback not called")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearCallback(t *testing.T) {
|
func TestClearCallback(t *testing.T) {
|
||||||
irccon := connForTesting("go-eventirc", "go-eventirc", false)
|
irccon := connForTesting("go-eventirc", "go-eventirc", false)
|
||||||
debugTest(irccon)
|
debugTest(irccon)
|
||||||
@ -332,3 +311,24 @@ func TestConnectionNickInUse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Errorf("expected %s and a suffixed version, got %s and %s", ircnick, nick1, nick2)
|
t.Errorf("expected %s and a suffixed version, got %s and %s", ircnick, nick1, nick2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConnectionCallbacks(t *testing.T) {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
ircnick := randStr(8)
|
||||||
|
irccon1 := connForTesting(ircnick, "IRCTest1", false)
|
||||||
|
debugTest(irccon1)
|
||||||
|
resultChan := make(chan map[string]string, 1)
|
||||||
|
irccon1.AddConnectCallback(func(e Event) {
|
||||||
|
resultChan <- irccon1.ISupport()
|
||||||
|
})
|
||||||
|
err := irccon1.Connect()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
go irccon1.Loop()
|
||||||
|
isupport := <-resultChan
|
||||||
|
if casemapping := isupport["CASEMAPPING"]; casemapping == "" {
|
||||||
|
t.Errorf("casemapping not detected in 005 RPL_ISUPPORT output; this is unheard of")
|
||||||
|
}
|
||||||
|
irccon1.Quit()
|
||||||
|
}
|
||||||
|
17
ircevent/numerics.go
Normal file
17
ircevent/numerics.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package ircevent
|
||||||
|
|
||||||
|
const (
|
||||||
|
RPL_WELCOME = "001"
|
||||||
|
RPL_ISUPPORT = "005"
|
||||||
|
RPL_ENDOFMOTD = "376"
|
||||||
|
ERR_NOMOTD = "422"
|
||||||
|
ERR_NICKNAMEINUSE = "433"
|
||||||
|
ERR_UNAVAILRESOURCE = "437"
|
||||||
|
// SASL
|
||||||
|
RPL_LOGGEDIN = "900"
|
||||||
|
RPL_LOGGEDOUT = "901"
|
||||||
|
ERR_NICKLOCKED = "902"
|
||||||
|
RPL_SASLSUCCESS = "903"
|
||||||
|
ERR_SASLFAIL = "904"
|
||||||
|
ERR_SASLALREADY = "907"
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user