mirror of
				https://github.com/fluffle/goirc
				synced 2025-11-03 19:48:04 +00:00 
			
		
		
		
	Add SASL authentication support
This hacks together support for IRCv3.1 SASL. Currently only SASL PLAIN is supported, but it's implemented in a way that adding support for other types should not require too many changes to the current code.
This commit is contained in:
		
							parent
							
								
									bbbcc9aa5b
								
							
						
					
					
						commit
						c3913a89ef
					
				
					 6 changed files with 133 additions and 14 deletions
				
			
		| 
						 | 
				
			
			@ -10,6 +10,7 @@ const (
 | 
			
		|||
	CONNECTED    = "CONNECTED"
 | 
			
		||||
	DISCONNECTED = "DISCONNECTED"
 | 
			
		||||
	ACTION       = "ACTION"
 | 
			
		||||
	AUTHENTICATE = "AUTHENTICATE"
 | 
			
		||||
	AWAY         = "AWAY"
 | 
			
		||||
	CAP          = "CAP"
 | 
			
		||||
	CTCP         = "CTCP"
 | 
			
		||||
| 
						 | 
				
			
			@ -322,3 +323,8 @@ func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Authenticate send an AUTHENTICATE command to the server.
 | 
			
		||||
func (conn *Conn) Authenticate(message string) {
 | 
			
		||||
	conn.Raw(AUTHENTICATE + " " + message)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/emersion/go-sasl"
 | 
			
		||||
	"github.com/fluffle/goirc/logging"
 | 
			
		||||
	"github.com/fluffle/goirc/state"
 | 
			
		||||
	"golang.org/x/net/proxy"
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +52,9 @@ type Conn struct {
 | 
			
		|||
	// Capabilites currently enabled
 | 
			
		||||
	currCaps *capSet
 | 
			
		||||
 | 
			
		||||
	// SASL internals
 | 
			
		||||
	saslRemainingData []byte
 | 
			
		||||
 | 
			
		||||
	// CancelFunc and WaitGroup for goroutines
 | 
			
		||||
	die context.CancelFunc
 | 
			
		||||
	wg  sync.WaitGroup
 | 
			
		||||
| 
						 | 
				
			
			@ -101,6 +105,9 @@ type Config struct {
 | 
			
		|||
	// A list of capabilities to request to the server during registration.
 | 
			
		||||
	Capabilites []string
 | 
			
		||||
 | 
			
		||||
	// SASL configuration to use to authenticate the connection.
 | 
			
		||||
	Sasl sasl.Client
 | 
			
		||||
 | 
			
		||||
	// Replaceable function to customise the 433 handler's new nick.
 | 
			
		||||
	// By default an underscore "_" is appended to the current nick.
 | 
			
		||||
	NewNick func(string) string
 | 
			
		||||
| 
						 | 
				
			
			@ -216,6 +223,11 @@ func Client(cfg *Config) *Conn {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cfg.Sasl != nil && !cfg.EnableCapabilityNegotiation {
 | 
			
		||||
		logging.Warn("Enabling capability negotiation as it's required for SASL")
 | 
			
		||||
		cfg.EnableCapabilityNegotiation = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conn := &Conn{
 | 
			
		||||
		cfg:           cfg,
 | 
			
		||||
		dialer:        dialer,
 | 
			
		||||
| 
						 | 
				
			
			@ -248,7 +260,6 @@ func (conn *Conn) Connected() bool {
 | 
			
		|||
//	conn.Config().Flood = true
 | 
			
		||||
//	// Send many lines to the IRC server, risking "excess flood"
 | 
			
		||||
//	conn.Config().Flood = false
 | 
			
		||||
//
 | 
			
		||||
func (conn *Conn) Config() *Config {
 | 
			
		||||
	return conn.cfg
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,9 +9,13 @@ import (
 | 
			
		|||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"github.com/fluffle/goirc/logging"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// saslCap is the IRCv3 capability used for SASL authentication.
 | 
			
		||||
const saslCap = "sasl"
 | 
			
		||||
 | 
			
		||||
// sets up the internal event handlers to do essential IRC protocol things
 | 
			
		||||
var intHandlers = map[string]HandlerFunc{
 | 
			
		||||
	REGISTER:     (*Conn).h_REGISTER,
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +26,9 @@ var intHandlers = map[string]HandlerFunc{
 | 
			
		|||
	PING:         (*Conn).h_PING,
 | 
			
		||||
	CAP:          (*Conn).h_CAP,
 | 
			
		||||
	"410":        (*Conn).h_410,
 | 
			
		||||
	AUTHENTICATE: (*Conn).h_AUTHENTICATE,
 | 
			
		||||
	"903":        (*Conn).h_903,
 | 
			
		||||
	"904":        (*Conn).h_904,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// set up the ircv3 capabilities supported by this client which will be requested by default to the server.
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +66,11 @@ func (conn *Conn) getRequestCapabilities() *capSet {
 | 
			
		|||
	// add capabilites supported by the client
 | 
			
		||||
	s.Add(defaultCaps...)
 | 
			
		||||
 | 
			
		||||
	if conn.cfg.Sasl != nil {
 | 
			
		||||
		// add the SASL cap if enabled
 | 
			
		||||
		s.Add(saslCap)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// add capabilites requested by the user
 | 
			
		||||
	s.Add(conn.cfg.Capabilites...)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,10 +91,27 @@ func (conn *Conn) negotiateCapabilities(supportedCaps []string) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) handleCapAck(caps []string) {
 | 
			
		||||
	gotSasl := false
 | 
			
		||||
	for _, cap := range caps {
 | 
			
		||||
		conn.currCaps.Add(cap)
 | 
			
		||||
 | 
			
		||||
		if conn.cfg.Sasl != nil && cap == saslCap {
 | 
			
		||||
			gotSasl = true
 | 
			
		||||
 | 
			
		||||
			mech, ir, err := conn.cfg.Sasl.Start()
 | 
			
		||||
			conn.saslRemainingData = ir
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logging.Warn("SASL authentication failed", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			conn.Authenticate(mech)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !gotSasl {
 | 
			
		||||
		conn.Cap(CAP_END)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) handleCapNak(caps []string) {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +210,50 @@ func (conn *Conn) h_CAP(line *Line) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler for SASL authentication
 | 
			
		||||
func (conn *Conn) h_AUTHENTICATE(line *Line) {
 | 
			
		||||
	if conn.cfg.Sasl == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(conn.saslRemainingData) > 0 {
 | 
			
		||||
		data := base64.StdEncoding.EncodeToString(conn.saslRemainingData)
 | 
			
		||||
 | 
			
		||||
		// TODO: batch data into chunks of 400 bytes per the spec
 | 
			
		||||
 | 
			
		||||
		conn.Authenticate(data)
 | 
			
		||||
		conn.saslRemainingData = nil
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	challenge, err := base64.StdEncoding.DecodeString(line.Args[0])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		// TODO: better error handling here and below
 | 
			
		||||
		logging.Error("Failed to decode SASL challenge", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response, err := conn.cfg.Sasl.Next(challenge)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logging.Error("Failed to generate response for SASL challenge", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: batch data into chunks of 400 bytes per the spec
 | 
			
		||||
	data := base64.StdEncoding.EncodeToString(response)
 | 
			
		||||
	conn.Authenticate(data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler for RPL_SASLSUCCESS.
 | 
			
		||||
func (conn *Conn) h_903(line *Line) {
 | 
			
		||||
	conn.Cap(CAP_END)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler for RPL_SASLFAILURE.
 | 
			
		||||
func (conn *Conn) h_904(line *Line) {
 | 
			
		||||
	// TODO: do something about this?
 | 
			
		||||
	logging.Warn("SASL authentication failed")
 | 
			
		||||
	conn.Cap(CAP_END)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler to trigger a CONNECTED event on receipt of numeric 001
 | 
			
		||||
// :<server> 001 <nick> :Welcome message <nick>!<user>@<host>
 | 
			
		||||
func (conn *Conn) h_001(line *Line) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								client/sasl_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								client/sasl_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
package client
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/emersion/go-sasl"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSaslPlainWorkflow(t *testing.T) {
 | 
			
		||||
	c, s := setUp(t)
 | 
			
		||||
	defer s.tearDown()
 | 
			
		||||
 | 
			
		||||
	c.Config().Sasl = sasl.NewPlainClient("", "example", "password")
 | 
			
		||||
	c.Config().EnableCapabilityNegotiation = true
 | 
			
		||||
 | 
			
		||||
	c.h_REGISTER(&Line{Cmd: REGISTER})
 | 
			
		||||
	s.nc.Expect("CAP LS")
 | 
			
		||||
	s.nc.Expect("NICK test")
 | 
			
		||||
	s.nc.Expect("USER test 12 * :Testing IRC")
 | 
			
		||||
	s.nc.Send("CAP * LS :sasl foobar")
 | 
			
		||||
	s.nc.Expect("CAP REQ :sasl")
 | 
			
		||||
	s.nc.Send("CAP * ACK :sasl")
 | 
			
		||||
	s.nc.Expect("AUTHENTICATE PLAIN")
 | 
			
		||||
	s.nc.Send("AUTHENTICATE +")
 | 
			
		||||
	s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=")
 | 
			
		||||
	s.nc.Send("904 test :SASL authentication successful")
 | 
			
		||||
	s.nc.Expect("CAP END")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
module github.com/fluffle/goirc
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
 | 
			
		||||
	github.com/golang/mock v1.5.0
 | 
			
		||||
	golang.org/x/net v0.0.0-20210119194325-5f4716e94777
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								go.sum
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,9 +1,10 @@
 | 
			
		|||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
 | 
			
		||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
 | 
			
		||||
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
 | 
			
		||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 | 
			
		||||
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue