IRCv3 Capability Negotiation

This adds the ability to negotiate arbitrary IRCv3 capabilities on
connection. Capabilities to be requested can be set in `irc.RequestCaps`,
and after registration the negotiated capabilities can be seen in
`irc.AcknowledgedCaps`.
This commit is contained in:
Russ Garrett 2017-01-25 17:36:15 +00:00
parent e1d00ae799
commit 744f1e3a7a
No known key found for this signature in database
GPG Key ID: 68880BB652AB0570
4 changed files with 86 additions and 27 deletions

80
irc.go
View File

@ -461,26 +461,84 @@ func (irc *Connection) Connect(server string) error {
irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password) irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password)
} }
resChan := make(chan *SASLResult) err = irc.negotiateCaps()
if err != nil {
return err
}
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user)
return nil
}
// Negotiate IRCv3 capabilities
func (irc *Connection) negotiateCaps() error {
saslResChan := make(chan *SASLResult)
if irc.UseSASL {
irc.RequestCaps = append(irc.RequestCaps, "sasl")
irc.setupSASLCallbacks(saslResChan)
}
if len(irc.RequestCaps) == 0 {
return nil
}
cap_chan := make(chan bool, len(irc.RequestCaps))
irc.AddCallback("CAP", func(e *Event) {
if len(e.Arguments) != 3 {
return
}
command := e.Arguments[1]
if command == "LS" {
missing_caps := len(irc.RequestCaps)
for _, cap_name := range strings.Split(e.Arguments[2], " ") {
for _, req_cap := range irc.RequestCaps {
if cap_name == req_cap {
irc.pwrite <- fmt.Sprintf("CAP REQ :%s\r\n", cap_name)
missing_caps--
}
}
}
for i := 0; i < missing_caps; i++ {
cap_chan <- true
}
} else if command == "ACK" || command == "NAK" {
for _, cap_name := range strings.Split(strings.TrimSpace(e.Arguments[2]), " ") {
if cap_name == "" {
continue
}
if command == "ACK" {
irc.AcknowledgedCaps = append(irc.AcknowledgedCaps, cap_name)
}
cap_chan <- true
}
}
})
irc.pwrite <- "CAP LS\r\n"
if irc.UseSASL { if irc.UseSASL {
irc.setupSASLCallbacks(resChan)
irc.pwrite <- fmt.Sprintf("CAP LS\r\n")
// request SASL
irc.pwrite <- fmt.Sprintf("CAP REQ :sasl\r\n")
// if sasl request doesn't complete in 15 seconds, close chan and timeout
select { select {
case res := <-resChan: case res := <-saslResChan:
if res.Failed { if res.Failed {
close(resChan) close(saslResChan)
return res.Err return res.Err
} }
case <-time.After(time.Second * 15): case <-time.After(time.Second * 15):
close(resChan) close(saslResChan)
return errors.New("SASL setup timed out. This shouldn't happen.") return errors.New("SASL setup timed out. This shouldn't happen.")
} }
} }
irc.pwrite <- fmt.Sprintf("NICK %s\r\n", irc.nick)
irc.pwrite <- fmt.Sprintf("USER %s 0.0.0.0 0.0.0.0 :%s\r\n", irc.user, irc.user) // Wait for all capabilities to be ACKed or NAKed before ending negotiation
for i := 0; i < len(irc.RequestCaps); i++ {
<-cap_chan
}
irc.pwrite <- fmt.Sprintf("CAP END\r\n")
return nil return nil
} }

View File

@ -43,7 +43,6 @@ func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) {
result <- &SASLResult{true, errors.New(e.Arguments[1])} result <- &SASLResult{true, errors.New(e.Arguments[1])}
}) })
irc.AddCallback("903", func(e *Event) { irc.AddCallback("903", func(e *Event) {
irc.SendRaw("CAP END")
result <- &SASLResult{false, nil} result <- &SASLResult{false, nil}
}) })
irc.AddCallback("904", func(e *Event) { irc.AddCallback("904", func(e *Event) {

View File

@ -34,7 +34,7 @@ func TestConnectionSASL(t *testing.T) {
err := irccon.Connect(SASLServer) err := irccon.Connect(SASLServer)
if err != nil { if err != nil {
t.Fatal("SASL failed") t.Fatalf("SASL failed: %s", err)
} }
irccon.Loop() irccon.Loop()
} }

View File

@ -20,6 +20,8 @@ type Connection struct {
Password string Password string
UseTLS bool UseTLS bool
UseSASL bool UseSASL bool
RequestCaps []string
AcknowledgedCaps []string
SASLLogin string SASLLogin string
SASLPassword string SASLPassword string
SASLMech string SASLMech string