Add downstream SASL support
This commit is contained in:
parent
9b777922ae
commit
651e936913
186
downstream.go
186
downstream.go
@ -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(),
|
||||||
|
Loading…
Reference in New Issue
Block a user