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
						1db4171d39
					
				
					 5 changed files with 135 additions and 9 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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,6 +101,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 *SaslAuthenticator
 | 
			
		||||
 | 
			
		||||
	// 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 +219,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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,14 +14,17 @@ import (
 | 
			
		|||
 | 
			
		||||
// sets up the internal event handlers to do essential IRC protocol things
 | 
			
		||||
var intHandlers = map[string]HandlerFunc{
 | 
			
		||||
	REGISTER: (*Conn).h_REGISTER,
 | 
			
		||||
	"001":    (*Conn).h_001,
 | 
			
		||||
	"433":    (*Conn).h_433,
 | 
			
		||||
	CTCP:     (*Conn).h_CTCP,
 | 
			
		||||
	NICK:     (*Conn).h_NICK,
 | 
			
		||||
	PING:     (*Conn).h_PING,
 | 
			
		||||
	CAP:      (*Conn).h_CAP,
 | 
			
		||||
	"410":    (*Conn).h_410,
 | 
			
		||||
	REGISTER:     (*Conn).h_REGISTER,
 | 
			
		||||
	"001":        (*Conn).h_001,
 | 
			
		||||
	"433":        (*Conn).h_433,
 | 
			
		||||
	CTCP:         (*Conn).h_CTCP,
 | 
			
		||||
	NICK:         (*Conn).h_NICK,
 | 
			
		||||
	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 +62,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 +87,19 @@ 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
 | 
			
		||||
			conn.Authenticate(string(conn.cfg.Sasl.mechanism))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !gotSasl {
 | 
			
		||||
		conn.Cap(CAP_END)
 | 
			
		||||
	}
 | 
			
		||||
	conn.Cap(CAP_END)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) handleCapNak(caps []string) {
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +198,32 @@ 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 line.Args[0] != "+" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// start authentication
 | 
			
		||||
	conn.Authenticate(conn.cfg.Sasl.authenticationRequest())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										43
									
								
								client/sasl.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								client/sasl.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
package client
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// saslMechanism is the name of the SASL authentication mechanism used.
 | 
			
		||||
type saslMechanism string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// saslPlain is the username and password based PLAIN
 | 
			
		||||
	// authentication mechanism.
 | 
			
		||||
	saslPlain saslMechanism = "PLAIN"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// saslCap is the IRCv3 capability used for SASL authentication.
 | 
			
		||||
const saslCap = "sasl"
 | 
			
		||||
 | 
			
		||||
// SaslAuthenticator authenticates the connection using SASL in the
 | 
			
		||||
// connection phase.
 | 
			
		||||
type SaslAuthenticator struct {
 | 
			
		||||
	mechanism             saslMechanism
 | 
			
		||||
	authenticationRequest func() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func encodePlainUsernamePassword(username, password string) string {
 | 
			
		||||
	requestBytes := []byte(username)
 | 
			
		||||
	requestBytes = append(requestBytes, byte(0))
 | 
			
		||||
	requestBytes = append(requestBytes, []byte(username)...)
 | 
			
		||||
	requestBytes = append(requestBytes, byte(0))
 | 
			
		||||
	requestBytes = append(requestBytes, []byte(password)...)
 | 
			
		||||
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(requestBytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SaslPlain(username, password string) *SaslAuthenticator {
 | 
			
		||||
	return &SaslAuthenticator{
 | 
			
		||||
		mechanism: saslPlain,
 | 
			
		||||
		authenticationRequest: func() string {
 | 
			
		||||
			return encodePlainUsernamePassword(username, password)
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								client/sasl_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								client/sasl_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
package client
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSaslPlainWorkflow(t *testing.T) {
 | 
			
		||||
	c, s := setUp(t)
 | 
			
		||||
	defer s.tearDown()
 | 
			
		||||
 | 
			
		||||
	c.Config().Sasl = SaslPlain("test", "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 dGVzdAB0ZXN0AHBhc3N3b3Jk")
 | 
			
		||||
	s.nc.Send("904 test :SASL authentication successful")
 | 
			
		||||
	s.nc.Expect("CAP END")
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue