diff --git a/builtin.go b/builtin.go index d8ef40a..ad79bf1 100644 --- a/builtin.go +++ b/builtin.go @@ -59,6 +59,15 @@ func (c *Client) registerBuiltins() { c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY)) c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT)) c.Handlers.register(true, ALLEVENTS, HandlerFunc(handleTags)) + + // SASL IRCv3 support. + c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL)) + c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL)) + c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError)) + c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError)) + c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError)) + c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError)) + c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError)) } // Nickname collisions. diff --git a/cap.go b/cap.go index 1333d50..c77f47e 100644 --- a/cap.go +++ b/cap.go @@ -6,6 +6,7 @@ package girc import ( "bytes" + "encoding/base64" "fmt" "io" "strings" @@ -34,6 +35,10 @@ func (c *Client) listCAP() { func possibleCapList(c *Client) map[string][]string { out := make(map[string][]string) + if c.Config.SASL != nil { + out["sasl"] = nil + } + for k := range c.Config.SupportedCaps { out[k] = c.Config.SupportedCaps[k] } @@ -150,14 +155,96 @@ func handleCAP(c *Client, e Event) { if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK { c.state.mu.Lock() c.state.enabledCap = strings.Split(e.Trailing, " ") + + // Do we need to do sasl auth? + wantsSASL := false + for i := 0; i < len(c.state.enabledCap); i++ { + if c.state.enabledCap[i] == "sasl" { + wantsSASL = true + break + } + } c.state.mu.Unlock() + if wantsSASL { + c.write(&Event{Command: AUTHENTICATE, Params: []string{"PLAIN"}}) + // Don't "CAP END", since we want to authenticate. + return + } + // Let the server know that we're done. c.write(&Event{Command: CAP, Params: []string{CAP_END}}) return } } +// SASLAuth contains the user and password needed for PLAIN SASL authentication. +type SASLAuth struct { + User string // User is the username for SASL. + Pass string // Pass is the password for SASL. +} + +func (sasl *SASLAuth) encode() (chunks []string) { + in := []byte(sasl.User) + + in = append(in, 0x0) + in = append(in, []byte(sasl.User)...) + in = append(in, 0x0) + in = append(in, []byte(sasl.Pass)...) + + out := base64.StdEncoding.EncodeToString(in) + + for { + if len(out) > 400 { + chunks = append(chunks, out[0:399]) + out = out[400:] + continue + } + + if len(out) <= 400 { + chunks = append(chunks, out) + break + } + } + + return chunks +} + +func handleSASL(c *Client, e Event) { + if e.Command == RPL_SASLSUCCESS || e.Command == ERR_SASLALREADY { + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + if len(e.Params) == 1 && e.Params[0] == "+" { + // Assume they want us to handle sending auth. + auth := c.Config.SASL.encode() + + // Send in 400 byte chunks. If the last chuck is exactly 400 bytes, + // send a "AUTHENTICATE +" 0-byte response to let the server know + // that we're done. + for i := 0; i < len(auth); i++ { + c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[i]}}) + + if i-1 == len(auth) && len(auth[i]) == 400 { + c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}}) + } + } + return + } +} + +func handleSASLError(c *Client, e Event) { + if c.Config.SASL != nil { + return + } + + // This is supposed to panic. Per the IRCv3 spec, one must disconnect upon + // authentication error. Maybe though, just a QUIT would be better? + panic(fmt.Sprintf("unable to use SASL authentication: %s (%s)", e.Command, e.Trailing)) +} + // handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is // what occurs (when enabled) when a servers services change the hostname of // a user. Traditionally, this was simply resolved with a quick QUIT and JOIN, diff --git a/client.go b/client.go index 1832c39..dc427a4 100644 --- a/client.go +++ b/client.go @@ -71,6 +71,9 @@ type Config struct { // Name is the "realname" that's used during connection. This only has an // affect during the dial process. Name string + // SASL contains the necessary authentication data to authenticate + // with SASL. At this time, only PLAIN is supported. + SASL *SASLAuth // Proxy is a proxy based address, used during the dial process when // connecting to the server. This only has an affect during the dial // process. Currently, x/net/proxy only supports socks5, however you can