From 8401b5855faea3d6370ea023f83cb21191c1219c Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 21 Jul 2016 22:55:31 +0200 Subject: [PATCH] Add SASL (PLAIN) support --- irc.go | 20 +++++++++++++++++++ irc_struct.go | 24 +++++++++++++---------- sasl.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ sasl_test.go | 40 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 sasl.go create mode 100644 sasl_test.go diff --git a/irc.go b/irc.go index 9043e88..0ba1d65 100644 --- a/irc.go +++ b/irc.go @@ -439,6 +439,25 @@ func (irc *Connection) Connect(server string) error { if len(irc.Password) > 0 { irc.pwrite <- fmt.Sprintf("PASS %s\r\n", irc.Password) } + + resChan := make(chan *SASLResult) + 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 { + case res := <-resChan: + if res.Failed { + close(resChan) + return res.Err + } + case <-time.After(time.Second * 15): + close(resChan) + 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) return nil @@ -466,6 +485,7 @@ func IRC(nick, user string) *Connection { KeepAlive: 4 * time.Minute, Timeout: 1 * time.Minute, PingFreq: 15 * time.Minute, + SASLMech: "PLAIN", QuitMessage: "", } irc.setupCallbacks() diff --git a/irc_struct.go b/irc_struct.go index 3e4a438..33db846 100644 --- a/irc_struct.go +++ b/irc_struct.go @@ -14,16 +14,20 @@ import ( type Connection struct { sync.WaitGroup - Debug bool - Error chan error - Password string - UseTLS bool - TLSConfig *tls.Config - Version string - Timeout time.Duration - PingFreq time.Duration - KeepAlive time.Duration - Server string + Debug bool + Error chan error + Password string + UseTLS bool + UseSASL bool + SASLLogin string + SASLPassword string + SASLMech string + TLSConfig *tls.Config + Version string + Timeout time.Duration + PingFreq time.Duration + KeepAlive time.Duration + Server string socket net.Conn pwrite chan string diff --git a/sasl.go b/sasl.go new file mode 100644 index 0000000..e5ff9e3 --- /dev/null +++ b/sasl.go @@ -0,0 +1,54 @@ +package irc + +import ( + "encoding/base64" + "errors" + "fmt" + "strings" +) + +type SASLResult struct { + Failed bool + Err error +} + +func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) { + irc.AddCallback("CAP", func(e *Event) { + if len(e.Arguments) == 3 { + if e.Arguments[1] == "LS" { + if !strings.Contains(e.Arguments[2], "sasl") { + result <- &SASLResult{true, errors.New("no SASL capability " + e.Arguments[2])} + } + } + if e.Arguments[1] == "ACK" { + if irc.SASLMech != "PLAIN" { + result <- &SASLResult{true, errors.New("only PLAIN is supported")} + } + irc.SendRaw("AUTHENTICATE " + irc.SASLMech) + } + } + }) + irc.AddCallback("AUTHENTICATE", func(e *Event) { + str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword))) + irc.SendRaw("AUTHENTICATE " + str) + }) + irc.AddCallback("901", func(e *Event) { + irc.SendRaw("CAP END") + irc.SendRaw("QUIT") + result <- &SASLResult{true, errors.New(e.Arguments[1])} + }) + irc.AddCallback("902", func(e *Event) { + irc.SendRaw("CAP END") + irc.SendRaw("QUIT") + result <- &SASLResult{true, errors.New(e.Arguments[1])} + }) + irc.AddCallback("903", func(e *Event) { + irc.SendRaw("CAP END") + result <- &SASLResult{false, nil} + }) + irc.AddCallback("904", func(e *Event) { + irc.SendRaw("CAP END") + irc.SendRaw("QUIT") + result <- &SASLResult{true, errors.New(e.Arguments[1])} + }) +} diff --git a/sasl_test.go b/sasl_test.go new file mode 100644 index 0000000..27aca18 --- /dev/null +++ b/sasl_test.go @@ -0,0 +1,40 @@ +package irc + +import ( + "crypto/tls" + "os" + "testing" + "time" +) + +// set SASLLogin and SASLPassword environment variables before testing +func TestConnectionSASL(t *testing.T) { + SASLServer := "irc.freenode.net:7000" + SASLLogin := os.Getenv("SASLLogin") + SASLPassword := os.Getenv("SASLPassword") + + if SASLLogin == "" { + t.SkipNow() + } + irccon := IRC("go-eventirc", "go-eventirc") + irccon.VerboseCallbackHandler = true + irccon.Debug = true + irccon.UseTLS = true + irccon.UseSASL = true + irccon.SASLLogin = SASLLogin + irccon.SASLPassword = SASLPassword + irccon.TLSConfig = &tls.Config{InsecureSkipVerify: true} + irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) + + irccon.AddCallback("366", func(e *Event) { + irccon.Privmsg("#go-eventirc", "Test Message SASL\n") + time.Sleep(2 * time.Second) + irccon.Quit() + }) + + err := irccon.Connect(SASLServer) + if err != nil { + t.Fatal("SASL failed") + } + irccon.Loop() +}