Index: trunk/upstream.go
===================================================================
--- trunk/upstream.go	(revision 93)
+++ trunk/upstream.go	(revision 95)
@@ -3,4 +3,5 @@
 import (
 	"crypto/tls"
+	"encoding/base64"
 	"fmt"
 	"io"
@@ -10,4 +11,5 @@
 	"time"
 
+	"github.com/emersion/go-sasl"
 	"gopkg.in/irc.v3"
 )
@@ -49,4 +51,7 @@
 	history    map[string]uint64
 	caps       map[string]string
+
+	saslClient  sasl.Client
+	saslStarted bool
 }
 
@@ -170,26 +175,148 @@
 		uc.logger.Print(msg)
 	case "CAP":
-		if len(msg.Params) < 2 {
-			return newNeedMoreParamsError(msg.Command)
-		}
-		caps := strings.Fields(msg.Params[len(msg.Params)-1])
-		more := msg.Params[len(msg.Params)-2] == "*"
-
-		for _, s := range caps {
-			kv := strings.SplitN(s, "=", 2)
-			k := strings.ToLower(kv[0])
-			var v string
-			if len(kv) >= 2 {
-				v = kv[1]
-			}
-			uc.caps[k] = v
-		}
-
-		if !more {
+		var subCmd string
+		if err := parseMessageParams(msg, nil, &subCmd); err != nil {
+			return err
+		}
+		subCmd = strings.ToUpper(subCmd)
+		subParams := msg.Params[2:]
+		switch subCmd {
+		case "LS":
+			if len(subParams) < 1 {
+				return newNeedMoreParamsError(msg.Command)
+			}
+			caps := strings.Fields(subParams[len(subParams)-1])
+			more := len(subParams) >= 2 && msg.Params[len(subParams)-2] == "*"
+
+			for _, s := range caps {
+				kv := strings.SplitN(s, "=", 2)
+				k := strings.ToLower(kv[0])
+				var v string
+				if len(kv) == 2 {
+					v = kv[1]
+				}
+				uc.caps[k] = v
+			}
+
+			if more {
+				break // wait to receive all capabilities
+			}
+
+			if uc.requestSASL() {
+				uc.SendMessage(&irc.Message{
+					Command: "CAP",
+					Params:  []string{"REQ", "sasl"},
+				})
+				break // we'll send CAP END after authentication is completed
+			}
+
 			uc.SendMessage(&irc.Message{
 				Command: "CAP",
 				Params:  []string{"END"},
 			})
-		}
+		case "ACK", "NAK":
+			if len(subParams) < 1 {
+				return newNeedMoreParamsError(msg.Command)
+			}
+			caps := strings.Fields(subParams[0])
+
+			for _, name := range caps {
+				if err := uc.handleCapAck(strings.ToLower(name), subCmd == "ACK"); err != nil {
+					return err
+				}
+			}
+
+			if uc.saslClient == nil {
+				uc.SendMessage(&irc.Message{
+					Command: "CAP",
+					Params:  []string{"END"},
+				})
+			}
+		default:
+			uc.logger.Printf("unhandled message: %v", msg)
+		}
+	case "AUTHENTICATE":
+		if uc.saslClient == nil {
+			return fmt.Errorf("received unexpected AUTHENTICATE message")
+		}
+
+		// TODO: if a challenge is 400 bytes long, buffer it
+		var challengeStr string
+		if err := parseMessageParams(msg, &challengeStr); err != nil {
+			uc.SendMessage(&irc.Message{
+				Command: "AUTHENTICATE",
+				Params:  []string{"*"},
+			})
+			return err
+		}
+
+		var challenge []byte
+		if challengeStr != "+" {
+			var err error
+			challenge, err = base64.StdEncoding.DecodeString(challengeStr)
+			if err != nil {
+				uc.SendMessage(&irc.Message{
+					Command: "AUTHENTICATE",
+					Params:  []string{"*"},
+				})
+				return err
+			}
+		}
+
+		var resp []byte
+		var err error
+		if !uc.saslStarted {
+			_, resp, err = uc.saslClient.Start()
+			uc.saslStarted = true
+		} else {
+			resp, err = uc.saslClient.Next(challenge)
+		}
+		if err != nil {
+			uc.SendMessage(&irc.Message{
+				Command: "AUTHENTICATE",
+				Params:  []string{"*"},
+			})
+			return err
+		}
+
+		// TODO: send response in multiple chunks if >= 400 bytes
+		var respStr = "+"
+		if resp != nil {
+			respStr = base64.StdEncoding.EncodeToString(resp)
+		}
+
+		uc.SendMessage(&irc.Message{
+			Command: "AUTHENTICATE",
+			Params:  []string{respStr},
+		})
+	case rpl_loggedin:
+		var account string
+		if err := parseMessageParams(msg, nil, nil, &account); err != nil {
+			return err
+		}
+		uc.logger.Printf("logged in with account %q", account)
+	case rpl_loggedout:
+		uc.logger.Printf("logged out")
+	case err_nicklocked, rpl_saslsuccess, err_saslfail, err_sasltoolong, err_saslaborted:
+		var info string
+		if err := parseMessageParams(msg, nil, &info); err != nil {
+			return err
+		}
+		switch msg.Command {
+		case err_nicklocked:
+			uc.logger.Printf("invalid nick used with SASL authentication: %v", info)
+		case err_saslfail:
+			uc.logger.Printf("SASL authentication failed: %v", info)
+		case err_sasltoolong:
+			uc.logger.Printf("SASL message too long: %v", info)
+		}
+
+		uc.saslClient = nil
+		uc.saslStarted = false
+
+		uc.SendMessage(&irc.Message{
+			Command: "CAP",
+			Params:  []string{"END"},
+		})
 	case irc.RPL_WELCOME:
 		uc.registered = true
@@ -440,5 +567,5 @@
 		// Ignore
 	default:
-		uc.logger.Printf("unhandled upstream message: %v", msg)
+		uc.logger.Printf("unhandled message: %v", msg)
 	}
 	return nil
@@ -478,4 +605,55 @@
 }
 
+func (uc *upstreamConn) requestSASL() bool {
+	if uc.network.SASL.Mechanism == "" {
+		return false
+	}
+
+	v, ok := uc.caps["sasl"]
+	if !ok {
+		return false
+	}
+	if v != "" {
+		mechanisms := strings.Split(v, ",")
+		found := false
+		for _, mech := range mechanisms {
+			if strings.EqualFold(mech, uc.network.SASL.Mechanism) {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+
+	return true
+}
+
+func (uc *upstreamConn) handleCapAck(name string, ok bool) error {
+	auth := &uc.network.SASL
+	switch name {
+	case "sasl":
+		if !ok {
+			uc.logger.Printf("server refused to acknowledge the SASL capability")
+			return nil
+		}
+
+		switch auth.Mechanism {
+		case "PLAIN":
+			uc.logger.Printf("starting SASL PLAIN authentication with username %q", auth.Plain.Username)
+			uc.saslClient = sasl.NewPlainClient("", auth.Plain.Username, auth.Plain.Password)
+		default:
+			return fmt.Errorf("unsupported SASL mechanism %q", name)
+		}
+
+		uc.SendMessage(&irc.Message{
+			Command: "AUTHENTICATE",
+			Params:  []string{auth.Mechanism},
+		})
+	}
+	return nil
+}
+
 func (uc *upstreamConn) readMessages() error {
 	for {
