Skip to content

Commit

Permalink
fix #102
Browse files Browse the repository at this point in the history
Implement multiline SASL PLAIN responses; support SASL EXTERNAL
  • Loading branch information
slingamn committed Jan 30, 2024
1 parent 5474a63 commit f0db02e
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 6 deletions.
11 changes: 10 additions & 1 deletion ircevent/examples/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,19 @@ func main() {
UseTLS: true,
TLSConfig: &tls.Config{InsecureSkipVerify: true},
RequestCaps: []string{"server-time", "message-tags"},
SASLLogin: saslLogin, // SASL will be enabled automatically if these are set
SASLLogin: saslLogin, // SASL PLAIN will be enabled automatically if these are set
SASLPassword: saslPassword,
}

if certKeyFile := os.Getenv("IRCEVENT_SASL_CLIENTCERT"); certKeyFile != "" {
clientCert, err := tls.LoadX509KeyPair(certKeyFile, certKeyFile)
if err != nil {
log.Fatalf("could not load client certificate: %v", err)
}
irc.TLSConfig.Certificates = []tls.Certificate{clientCert}
irc.SASLMech = "EXTERNAL" // overrides automatic SASL PLAIN
}

irc.AddConnectCallback(func(e ircmsg.Message) {
// attempt to set the BOT mode on ourself:
if botMode := irc.ISupport()["BOT"]; botMode != "" {
Expand Down
4 changes: 2 additions & 2 deletions ircevent/irc.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,8 +659,8 @@ func (irc *Connection) Connect() (err error) {
if irc.SASLMech == "" {
irc.SASLMech = "PLAIN"
}
if irc.SASLMech != "PLAIN" {
return errors.New("only SASL PLAIN is supported")
if !(irc.SASLMech == "PLAIN" || irc.SASLMech == "EXTERNAL") {
return fmt.Errorf("unsupported SASL mechanism %s", irc.SASLMech)
}
if irc.MaxLineLen == 0 {
irc.MaxLineLen = 512
Expand Down
54 changes: 51 additions & 3 deletions ircevent/irc_sasl.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package ircevent

import (
"bytes"
"encoding/base64"
"errors"
"fmt"

"github.com/ergochat/irc-go/ircmsg"
)
Expand All @@ -29,10 +29,58 @@ func (irc *Connection) submitSASLResult(r saslResult) {
}
}

func splitSaslResponse(raw []byte) (result []string) {
// https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command
// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
// and each chunk is sent as a separate AUTHENTICATE command. Empty (zero-length)
// responses are sent as AUTHENTICATE +. If the last chunk was exactly 400 bytes
// long, it must also be followed by AUTHENTICATE + to signal end of response."

if len(raw) == 0 {
return []string{"+"}
}

response := base64.StdEncoding.EncodeToString(raw)
lastLen := 0
for len(response) > 0 {
// TODO once we require go 1.21, this can be: lastLen = min(len(response), 400)
lastLen = len(response)
if lastLen > 400 {
lastLen = 400
}
result = append(result, response[:lastLen])
response = response[lastLen:]
}

if lastLen == 400 {
result = append(result, "+")
}

return result
}

func (irc *Connection) composeSaslPlainResponse() []byte {
var buf bytes.Buffer
buf.WriteString(irc.SASLLogin) // optional authzid, included for compatibility
buf.WriteByte('\x00')
buf.WriteString(irc.SASLLogin) // authcid
buf.WriteByte('\x00')
buf.WriteString(irc.SASLPassword) // passwd
return buf.Bytes()
}

func (irc *Connection) setupSASLCallbacks() {
irc.AddCallback("AUTHENTICATE", func(e ircmsg.Message) {
str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword)))
irc.Send("AUTHENTICATE", str)
switch irc.SASLMech {
case "PLAIN":
for _, resp := range splitSaslResponse(irc.composeSaslPlainResponse()) {
irc.Send("AUTHENTICATE", resp)
}
case "EXTERNAL":
irc.Send("AUTHENTICATE", "+")
default:
// impossible, nothing to do
}
})

irc.AddCallback(RPL_LOGGEDOUT, func(e ircmsg.Message) {
Expand Down
26 changes: 26 additions & 0 deletions ircevent/irc_sasl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,29 @@ func TestSASLFail(t *testing.T) {
t.Errorf("successfully connected with invalid password")
}
}

func TestSplitResponse(t *testing.T) {
assertEqual(splitSaslResponse([]byte{}), []string{"+"})
assertEqual(splitSaslResponse(
[]byte("shivaram\x00shivaram\x00shivarampassphrase")),
[]string{"c2hpdmFyYW0Ac2hpdmFyYW0Ac2hpdmFyYW1wYXNzcGhyYXNl"},
)

// from the examples in the spec:
assertEqual(
splitSaslResponse([]byte("\x00emersion\x00Est ut beatae omnis ipsam. Quis fugiat deleniti totam qui. Ipsum quam a dolorum tempora velit laborum odit. Et saepe voluptate sed cumque vel. Voluptas sint ab pariatur libero veritatis corrupti. Vero iure omnis ullam. Vero beatae dolores facere fugiat ipsam. Ea est pariatur minima nobis sunt aut ut. Dolores ut laudantium maiores temporibus voluptates. Reiciendis impedit omnis et unde delectus quas ab. Quae eligendi necessitatibus doloribus molestias tempora magnam assumenda.")),
[]string{
"AGVtZXJzaW9uAEVzdCB1dCBiZWF0YWUgb21uaXMgaXBzYW0uIFF1aXMgZnVnaWF0IGRlbGVuaXRpIHRvdGFtIHF1aS4gSXBzdW0gcXVhbSBhIGRvbG9ydW0gdGVtcG9yYSB2ZWxpdCBsYWJvcnVtIG9kaXQuIEV0IHNhZXBlIHZvbHVwdGF0ZSBzZWQgY3VtcXVlIHZlbC4gVm9sdXB0YXMgc2ludCBhYiBwYXJpYXR1ciBsaWJlcm8gdmVyaXRhdGlzIGNvcnJ1cHRpLiBWZXJvIGl1cmUgb21uaXMgdWxsYW0uIFZlcm8gYmVhdGFlIGRvbG9yZXMgZmFjZXJlIGZ1Z2lhdCBpcHNhbS4gRWEgZXN0IHBhcmlhdHVyIG1pbmltYSBub2JpcyBz",
"dW50IGF1dCB1dC4gRG9sb3JlcyB1dCBsYXVkYW50aXVtIG1haW9yZXMgdGVtcG9yaWJ1cyB2b2x1cHRhdGVzLiBSZWljaWVuZGlzIGltcGVkaXQgb21uaXMgZXQgdW5kZSBkZWxlY3R1cyBxdWFzIGFiLiBRdWFlIGVsaWdlbmRpIG5lY2Vzc2l0YXRpYnVzIGRvbG9yaWJ1cyBtb2xlc3RpYXMgdGVtcG9yYSBtYWduYW0gYXNzdW1lbmRhLg==",
},
)

// 400 byte line must be followed by +:
assertEqual(
splitSaslResponse([]byte("slingamn\x00slingamn\x001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")),
[]string{
"c2xpbmdhbW4Ac2xpbmdhbW4AMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMQ==",
"+",
},
)
}

0 comments on commit f0db02e

Please sign in to comment.