mirror of
				https://github.com/fluffle/goirc
				synced 2025-11-04 03:58:03 +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"
 | 
						CONNECTED    = "CONNECTED"
 | 
				
			||||||
	DISCONNECTED = "DISCONNECTED"
 | 
						DISCONNECTED = "DISCONNECTED"
 | 
				
			||||||
	ACTION       = "ACTION"
 | 
						ACTION       = "ACTION"
 | 
				
			||||||
 | 
						AUTHENTICATE = "AUTHENTICATE"
 | 
				
			||||||
	AWAY         = "AWAY"
 | 
						AWAY         = "AWAY"
 | 
				
			||||||
	CAP          = "CAP"
 | 
						CAP          = "CAP"
 | 
				
			||||||
	CTCP         = "CTCP"
 | 
						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"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/emersion/go-sasl"
 | 
				
			||||||
	"github.com/fluffle/goirc/logging"
 | 
						"github.com/fluffle/goirc/logging"
 | 
				
			||||||
	"github.com/fluffle/goirc/state"
 | 
						"github.com/fluffle/goirc/state"
 | 
				
			||||||
	"golang.org/x/net/proxy"
 | 
						"golang.org/x/net/proxy"
 | 
				
			||||||
| 
						 | 
					@ -51,6 +52,9 @@ type Conn struct {
 | 
				
			||||||
	// Capabilites currently enabled
 | 
						// Capabilites currently enabled
 | 
				
			||||||
	currCaps *capSet
 | 
						currCaps *capSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SASL internals
 | 
				
			||||||
 | 
						saslRemainingData []byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// CancelFunc and WaitGroup for goroutines
 | 
						// CancelFunc and WaitGroup for goroutines
 | 
				
			||||||
	die context.CancelFunc
 | 
						die context.CancelFunc
 | 
				
			||||||
	wg  sync.WaitGroup
 | 
						wg  sync.WaitGroup
 | 
				
			||||||
| 
						 | 
					@ -101,6 +105,9 @@ type Config struct {
 | 
				
			||||||
	// A list of capabilities to request to the server during registration.
 | 
						// A list of capabilities to request to the server during registration.
 | 
				
			||||||
	Capabilites []string
 | 
						Capabilites []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SASL configuration to use to authenticate the connection.
 | 
				
			||||||
 | 
						Sasl sasl.Client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Replaceable function to customise the 433 handler's new nick.
 | 
						// Replaceable function to customise the 433 handler's new nick.
 | 
				
			||||||
	// By default an underscore "_" is appended to the current nick.
 | 
						// By default an underscore "_" is appended to the current nick.
 | 
				
			||||||
	NewNick func(string) string
 | 
						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{
 | 
						conn := &Conn{
 | 
				
			||||||
		cfg:           cfg,
 | 
							cfg:           cfg,
 | 
				
			||||||
		dialer:        dialer,
 | 
							dialer:        dialer,
 | 
				
			||||||
| 
						 | 
					@ -245,10 +257,9 @@ func (conn *Conn) Connected() bool {
 | 
				
			||||||
// affect client behaviour. To disable flood protection temporarily,
 | 
					// affect client behaviour. To disable flood protection temporarily,
 | 
				
			||||||
// for example, a handler could do:
 | 
					// for example, a handler could do:
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
//     conn.Config().Flood = true
 | 
					//	conn.Config().Flood = true
 | 
				
			||||||
//     // Send many lines to the IRC server, risking "excess flood"
 | 
					//	// Send many lines to the IRC server, risking "excess flood"
 | 
				
			||||||
//     conn.Config().Flood = false
 | 
					//	conn.Config().Flood = false
 | 
				
			||||||
//
 | 
					 | 
				
			||||||
func (conn *Conn) Config() *Config {
 | 
					func (conn *Conn) Config() *Config {
 | 
				
			||||||
	return conn.cfg
 | 
						return conn.cfg
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,19 +9,26 @@ import (
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
	"github.com/fluffle/goirc/logging"
 | 
						"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
 | 
					// sets up the internal event handlers to do essential IRC protocol things
 | 
				
			||||||
var intHandlers = map[string]HandlerFunc{
 | 
					var intHandlers = map[string]HandlerFunc{
 | 
				
			||||||
	REGISTER: (*Conn).h_REGISTER,
 | 
						REGISTER:     (*Conn).h_REGISTER,
 | 
				
			||||||
	"001":    (*Conn).h_001,
 | 
						"001":        (*Conn).h_001,
 | 
				
			||||||
	"433":    (*Conn).h_433,
 | 
						"433":        (*Conn).h_433,
 | 
				
			||||||
	CTCP:     (*Conn).h_CTCP,
 | 
						CTCP:         (*Conn).h_CTCP,
 | 
				
			||||||
	NICK:     (*Conn).h_NICK,
 | 
						NICK:         (*Conn).h_NICK,
 | 
				
			||||||
	PING:     (*Conn).h_PING,
 | 
						PING:         (*Conn).h_PING,
 | 
				
			||||||
	CAP:      (*Conn).h_CAP,
 | 
						CAP:          (*Conn).h_CAP,
 | 
				
			||||||
	"410":    (*Conn).h_410,
 | 
						"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.
 | 
					// 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
 | 
						// add capabilites supported by the client
 | 
				
			||||||
	s.Add(defaultCaps...)
 | 
						s.Add(defaultCaps...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if conn.cfg.Sasl != nil {
 | 
				
			||||||
 | 
							// add the SASL cap if enabled
 | 
				
			||||||
 | 
							s.Add(saslCap)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// add capabilites requested by the user
 | 
						// add capabilites requested by the user
 | 
				
			||||||
	s.Add(conn.cfg.Capabilites...)
 | 
						s.Add(conn.cfg.Capabilites...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,10 +91,27 @@ func (conn *Conn) negotiateCapabilities(supportedCaps []string) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (conn *Conn) handleCapAck(caps []string) {
 | 
					func (conn *Conn) handleCapAck(caps []string) {
 | 
				
			||||||
 | 
						gotSasl := false
 | 
				
			||||||
	for _, cap := range caps {
 | 
						for _, cap := range caps {
 | 
				
			||||||
		conn.currCaps.Add(cap)
 | 
							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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	conn.Cap(CAP_END)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (conn *Conn) handleCapNak(caps []string) {
 | 
					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
 | 
					// Handler to trigger a CONNECTED event on receipt of numeric 001
 | 
				
			||||||
// :<server> 001 <nick> :Welcome message <nick>!<user>@<host>
 | 
					// :<server> 001 <nick> :Welcome message <nick>!<user>@<host>
 | 
				
			||||||
func (conn *Conn) h_001(line *Line) {
 | 
					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
 | 
					module github.com/fluffle/goirc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
 | 
						github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
 | 
				
			||||||
	github.com/golang/mock v1.5.0
 | 
						github.com/golang/mock v1.5.0
 | 
				
			||||||
	golang.org/x/net v0.0.0-20210119194325-5f4716e94777
 | 
						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 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
 | 
				
			||||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
 | 
					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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
					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/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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
 | 
					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