From a26d5d5b816b9e5ad7ad829862de370089962e36 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Tue, 21 Jul 2020 21:47:29 +0200 Subject: [PATCH] add basic PLAIN-only SASL support. --- client.go | 110 ++++++++++++++++++++++++++++++++++++++++++++---------- config.go | 9 +++++ 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/client.go b/client.go index 0047751..a6673c1 100644 --- a/client.go +++ b/client.go @@ -2,9 +2,11 @@ package irc import ( "bufio" + "bytes" "context" "crypto/rand" "crypto/tls" + "encoding/base64" "encoding/binary" "encoding/hex" "errors" @@ -35,6 +37,7 @@ var supportedCaps = []string{ "account-tag", "echo-message", "draft/languages", + "sasl", } // ErrNoConnection is returned if you try to do something requiring a connection, @@ -850,30 +853,33 @@ func (client *Client) handleEvent(event *Event) { // Nick rotation case "packet.431", "packet.432", "packet.433", "packet.436": { - client.mutex.RLock() - hasRegistered := client.nick != "" - client.mutex.RUnlock() - - if !hasRegistered { - nick := event.Args[1] + // Ignore if client is registered + if client.Nick() != "" { + break + } + // Ignore if in middle of SASL authentication + if event.Verb() == "433" && client.Value("sasl.usingMethod") != nil { + break + } - // "AltN" -> "AltN+1", ... - prev := client.config.Nick - sent := false - for _, alt := range client.config.Alternatives { - if nick == prev { - _ = client.Sendf("NICK %s", alt) - sent = true - break - } + nick := event.Args[1] - prev = alt + // "AltN" -> "AltN+1", ... + prev := client.config.Nick + sent := false + for _, alt := range client.config.Alternatives { + if nick == prev { + _ = client.Sendf("NICK %s", alt) + sent = true + break } - if !sent { - // "LastAlt" -> "Nick23962" - _ = client.Sendf("NICK %s%05d", client.config.Nick, mathRand.Int31n(99999)) - } + prev = alt + } + + if !sent { + // "LastAlt" -> "Nick23962" + _ = client.Sendf("NICK %s%05d", client.config.Nick, mathRand.Int31n(99999)) } } @@ -958,6 +964,30 @@ func (client *Client) handleEvent(event *Event) { // Special cases for supported tokens switch token { + case "sasl": + { + if client.config.SASL == nil { + break + } + + mechanisms := strings.Split(client.capData[token], ",") + selectedMechanism := "" + if len(mechanisms) == 0 || mechanisms[0] == "" { + selectedMechanism = "PLAIN" + } + for _, mechanism := range mechanisms { + if mechanism == "PLAIN" && selectedMechanism == "" { + selectedMechanism = "PLAIN" + } + } + + // TODO: Add better mechanisms + if selectedMechanism != "" { + _ = client.Sendf("AUTHENTICATE %s", selectedMechanism) + client.SetValue("sasl.usingMethod", "PLAIN") + } + } + case "draft/languages": { if len(client.config.Languages) == 0 { @@ -1051,6 +1081,46 @@ func (client *Client) handleEvent(event *Event) { } } + // SASL + case "packet.authenticate": + { + if event.Arg(0) != "+" { + break + } + + method, ok := client.Value("sasl.usingMethod").(string) + if !ok { + break + } + + switch method { + case "PLAIN": + { + parts := [][]byte{ + []byte(client.config.SASL.AuthenticationIdentity), + []byte(client.config.SASL.AuthorizationIdentity), + []byte(client.config.SASL.Password), + } + plainString := base64.StdEncoding.EncodeToString(bytes.Join(parts, []byte{0x00})) + + _ = client.Sendf("AUTHENTICATE %s", plainString) + } + } + } + case "packet.904": // Auth failed + { + // Cancel authentication. + _ = client.Sendf("AUTHENTICATE *") + client.SetValue("sasl.usingMethod", (interface{})(nil)) + } + case "packet.903", "packet.906": // Auth ended + { + // A bit dirty, but it'll get the nick rotation started again. + if client.Nick() == "" { + _ = client.Sendf("NICK %s", client.config.Nick) + } + } + // User/host detection case "packet.352": // WHO reply { diff --git a/config.go b/config.go index d88683c..ace85ee 100644 --- a/config.go +++ b/config.go @@ -36,6 +36,15 @@ type Config struct { // Auto-join on invite (bad idea). AutoJoinInvites bool `json:"autoJoinInvites"` + + // Use SASL authorization if supported. + SASL *SASLConfig `json:"sasl"` +} + +type SASLConfig struct { + AuthenticationIdentity string `json:"authenticationIdentity"` + AuthorizationIdentity string `json:"authorizationIdentity"` + Password string `json:"password"` } // WithDefaults returns the config with the default values