diff --git a/irc/accounts.go b/irc/accounts.go index 83ab6971..f6481a7e 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -3,9 +3,23 @@ package irc -import "time" +import ( + "encoding/base64" + "strings" + "time" + + "github.com/DanielOaks/girc-go/ircmsg" +) var ( + // EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support. + // This can be moved to some other data structure/place if we need to load/unload mechs later. + EnabledSaslMechanisms = map[string]func(*Server, *Client, string, []byte) bool{ + "PLAIN": authPlainHandler, + "EXTERNAL": authExternalHandler, + } + + // NoAccount is a placeholder which means that the user is not logged into an account. NoAccount = ClientAccount{ Name: "*", // * is used until actual account name is set } @@ -20,3 +34,96 @@ type ClientAccount struct { // Clients that are currently logged into this account (useful for notifications). Clients []*Client } + +// authenticateHandler parses the AUTHENTICATE command (for SASL authentication). +func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + // sasl abort + if len(msg.Params) == 1 && msg.Params[0] == "*" { + if client.saslInProgress { + client.Send(nil, server.nameString, ERR_SASLABORTED, client.nickString, "SASL authentication aborted") + } else { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed") + } + client.saslInProgress = false + client.saslMechanism = "" + client.saslValue = "" + return false + } + + // start new sasl session + if !client.saslInProgress { + mechanism := strings.ToUpper(msg.Params[0]) + _, mechanismIsEnabled := EnabledSaslMechanisms[mechanism] + + if mechanismIsEnabled { + client.saslInProgress = true + client.saslMechanism = mechanism + client.Send(nil, server.nameString, "AUTHENTICATE", "+") + } else { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed") + } + + return false + } + + // continue existing sasl session + rawData := msg.Params[0] + + if len(rawData) > 400 { + client.Send(nil, server.nameString, ERR_SASLTOOLONG, client.nickString, "SASL message too long") + client.saslInProgress = false + client.saslMechanism = "" + client.saslValue = "" + return false + } else if len(rawData) == 400 { + client.saslValue += rawData + // allow 4 'continuation' lines before rejecting for length + if len(client.saslValue) > 400*4 { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Passphrase too long") + client.saslInProgress = false + client.saslMechanism = "" + client.saslValue = "" + return false + } + return false + } else if len(client.saslValue) > 0 { + client.saslValue += rawData + return false + } + client.saslValue += rawData + + data, err := base64.StdEncoding.DecodeString(client.saslValue) + if err != nil { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Invalid b64 encoding") + client.saslInProgress = false + client.saslMechanism = "" + client.saslValue = "" + return false + } + + // call actual handler + handler, handlerExists := EnabledSaslMechanisms[client.saslMechanism] + + // like 100% not required, but it's good to be safe I guess + if !handlerExists { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed") + client.saslInProgress = false + client.saslMechanism = "" + client.saslValue = "" + return false + } + + return handler(server, client, client.saslMechanism, data) +} + +// authPlainHandler parses the SASL PLAIN mechanism. +func authPlainHandler(server *Server, client *Client, mechanism string, value []byte) bool { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Mechanism not yet implemented") + return false +} + +// authExternalHandler parses the SASL EXTERNAL mechanism. +func authExternalHandler(server *Server, client *Client, mechanism string, value []byte) bool { + client.Send(nil, server.nameString, ERR_SASLFAIL, client.nickString, "SASL authentication failed: Mechanism not yet implemented") + return false +} diff --git a/irc/capability.go b/irc/capability.go index 6f784d8d..77af38be 100644 --- a/irc/capability.go +++ b/irc/capability.go @@ -25,6 +25,7 @@ var ( SupportedCapabilities = CapabilitySet{ ExtendedJoin: true, MultiPrefix: true, + SASL: true, ServerTime: true, UserhostInNames: true, } diff --git a/irc/client.go b/irc/client.go index a10b5336..f501b0f8 100644 --- a/irc/client.go +++ b/irc/client.go @@ -27,14 +27,17 @@ var ( ) type Client struct { + account *ClientAccount atime time.Time authorized bool awayMessage string capabilities CapabilitySet capState CapState + certfp string channels ChannelSet ctime time.Time flags map[UserMode]bool + isDestroyed bool isQuitting bool hasQuit bool hops uint @@ -45,13 +48,13 @@ type Client struct { nickMaskString string // cache for nickmask string since it's used with lots of replies quitTimer *time.Timer realname string - account *ClientAccount registered bool + saslInProgress bool + saslMechanism string + saslValue string server *Server socket *Socket username Name - isDestroyed bool - certfp string } func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { diff --git a/irc/commands.go b/irc/commands.go index aae04dad..a30967dc 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -49,6 +49,11 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b // Commands holds all commands executable by a client connected to us. var Commands = map[string]Command{ + "AUTHENTICATE": { + handler: authenticateHandler, + usablePreReg: true, + minParams: 1, + }, "AWAY": { handler: awayHandler, minParams: 0,