Add downstream SASL support

This commit is contained in:
Simon Ser 2020-03-16 16:16:27 +01:00
parent 9b777922ae
commit 651e936913
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48

View File

@ -2,6 +2,7 @@ package soju
import ( import (
"crypto/tls" "crypto/tls"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -10,6 +11,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/emersion/go-sasl"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gopkg.in/irc.v3" "gopkg.in/irc.v3"
) )
@ -76,6 +78,8 @@ type downstreamConn struct {
capVersion int capVersion int
caps map[string]bool caps map[string]bool
saslServer sasl.Server
lock sync.Mutex lock sync.Mutex
ourMessages map[*irc.Message]struct{} ourMessages map[*irc.Message]struct{}
} }
@ -342,6 +346,101 @@ func (dc *downstreamConn) handleMessageUnregistered(msg *irc.Message) error {
if err := dc.handleCapCommand(subCmd, msg.Params[1:]); err != nil { if err := dc.handleCapCommand(subCmd, msg.Params[1:]); err != nil {
return err return err
} }
case "AUTHENTICATE":
if !dc.caps["sasl"] {
return ircError{&irc.Message{
Command: err_saslfail,
Params: []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
}}
}
if len(msg.Params) == 0 {
return ircError{&irc.Message{
Command: err_saslfail,
Params: []string{"*", "Missing AUTHENTICATE argument"},
}}
}
if dc.nick == "" {
return ircError{&irc.Message{
Command: err_saslfail,
Params: []string{"*", "Expected NICK command before AUTHENTICATE"},
}}
}
var resp []byte
if dc.saslServer == nil {
mech := strings.ToUpper(msg.Params[0])
switch mech {
case "PLAIN":
dc.saslServer = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
return dc.authenticate(username, password)
}))
default:
return ircError{&irc.Message{
Command: err_saslfail,
Params: []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
}}
}
} else if msg.Params[0] == "*" {
dc.saslServer = nil
return ircError{&irc.Message{
Command: err_saslaborted,
Params: []string{"*", "SASL authentication aborted"},
}}
} else if msg.Params[0] == "+" {
resp = nil
} else {
// TODO: multi-line messages
var err error
resp, err = base64.StdEncoding.DecodeString(msg.Params[0])
if err != nil {
dc.saslServer = nil
return ircError{&irc.Message{
Command: err_saslfail,
Params: []string{"*", "Invalid base64-encoded response"},
}}
}
}
challenge, done, err := dc.saslServer.Next(resp)
if err != nil {
dc.saslServer = nil
if ircErr, ok := err.(ircError); ok && ircErr.Message.Command == irc.ERR_PASSWDMISMATCH {
return ircError{&irc.Message{
Command: err_saslfail,
Params: []string{"*", ircErr.Message.Params[1]},
}}
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: err_saslfail,
Params: []string{"*", "SASL error"},
})
return fmt.Errorf("SASL authentication failed: %v", err)
} else if done {
dc.saslServer = nil
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: rpl_loggedin,
Params: []string{dc.nick, dc.nick, dc.user.Username, "You are now logged in"},
})
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: rpl_saslsuccess,
Params: []string{dc.nick, "SASL authentication successful"},
})
} else {
challengeStr := "+"
if challenge != nil {
challengeStr = base64.StdEncoding.EncodeToString(challenge)
}
// TODO: multi-line messages
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: "AUTHENTICATE",
Params: []string{challengeStr},
})
}
default: default:
dc.logger.Printf("unhandled message: %v", msg) dc.logger.Printf("unhandled message: %v", msg)
return newUnknownCommandError(msg.Command) return newUnknownCommandError(msg.Command)
@ -370,11 +469,11 @@ func (dc *downstreamConn) handleCapCommand(cmd string, args []string) error {
} }
var caps []string var caps []string
/*if dc.capVersion >= 302 { if dc.capVersion >= 302 {
caps = append(caps, "sasl=PLAIN") caps = append(caps, "sasl=PLAIN")
} else { } else {
caps = append(caps, "sasl") caps = append(caps, "sasl")
}*/ }
// TODO: multi-line replies // TODO: multi-line replies
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
@ -421,8 +520,8 @@ func (dc *downstreamConn) handleCapCommand(cmd string, args []string) error {
} }
switch name { switch name {
/*case "sasl": case "sasl":
dc.caps[name] = enable*/ dc.caps[name] = enable
default: default:
ack = false ack = false
} }
@ -457,35 +556,23 @@ func sanityCheckServer(addr string) error {
return conn.Close() return conn.Close()
} }
func (dc *downstreamConn) register() error { func unmarshalUsername(rawUsername string) (username, network string) {
username := dc.rawUsername username = rawUsername
var networkName string
if i := strings.LastIndexAny(username, "/@"); i >= 0 { if i := strings.LastIndexAny(username, "/@"); i >= 0 {
networkName = username[i+1:] network = username[i+1:]
} }
if i := strings.IndexAny(username, "/@"); i >= 0 { if i := strings.IndexAny(username, "/@"); i >= 0 {
username = username[:i] username = username[:i]
} }
dc.username = "~" + username return username, network
password := dc.password
dc.password = ""
u := dc.srv.getUser(username)
if u == nil {
dc.logger.Printf("failed authentication for %q: unknown username", username)
return errAuthFailed
} }
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) func (dc *downstreamConn) setNetwork(networkName string) error {
if err != nil { if networkName == "" {
dc.logger.Printf("failed authentication for %q: %v", username, err) return nil
return errAuthFailed
} }
var network *network network := dc.user.getNetwork(networkName)
if networkName != "" {
network = u.getNetwork(networkName)
if network == nil { if network == nil {
addr := networkName addr := networkName
if !strings.ContainsRune(addr, ':') { if !strings.ContainsRune(addr, ':') {
@ -502,21 +589,58 @@ func (dc *downstreamConn) register() error {
} }
dc.logger.Printf("auto-saving network %q", networkName) dc.logger.Printf("auto-saving network %q", networkName)
network, err = u.createNetwork(networkName, dc.nick) var err error
network, err = dc.user.createNetwork(networkName, dc.nick)
if err != nil { if err != nil {
return err return err
} }
} }
dc.network = network
return nil
}
func (dc *downstreamConn) authenticate(username, password string) error {
username, networkName := unmarshalUsername(username)
u := dc.srv.getUser(username)
if u == nil {
dc.logger.Printf("failed authentication for %q: unknown username", username)
return errAuthFailed
}
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
if err != nil {
dc.logger.Printf("failed authentication for %q: %v", username, err)
return errAuthFailed
}
dc.user = u
return dc.setNetwork(networkName)
}
func (dc *downstreamConn) register() error {
password := dc.password
dc.password = ""
if dc.user == nil {
if err := dc.authenticate(dc.rawUsername, password); err != nil {
return err
}
} else if dc.network == nil {
_, networkName := unmarshalUsername(dc.rawUsername)
if err := dc.setNetwork(networkName); err != nil {
return err
}
} }
dc.registered = true dc.registered = true
dc.user = u dc.username = dc.user.Username
dc.network = network
u.lock.Lock() dc.user.lock.Lock()
firstDownstream := len(u.downstreamConns) == 0 firstDownstream := len(dc.user.downstreamConns) == 0
u.downstreamConns = append(u.downstreamConns, dc) dc.user.downstreamConns = append(dc.user.downstreamConns, dc)
u.lock.Unlock() dc.user.lock.Unlock()
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),