mirror of https://github.com/ergochat/ergo.git
use SASL utilities from irc-go
This commit is contained in:
parent
43b9c9a4bd
commit
9c71a96809
2
go.mod
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
|
||||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
||||
github.com/ergochat/irc-go v0.4.0
|
||||
github.com/ergochat/irc-go v0.5.0-rc1
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/go-test/deep v1.0.6 // indirect
|
||||
github.com/gofrs/flock v0.8.1
|
||||
|
|
2
go.sum
2
go.sum
|
@ -12,6 +12,8 @@ github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5
|
|||
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
|
||||
github.com/ergochat/irc-go v0.4.0 h1:0YibCKfAAtwxQdNjLQd9xpIEPisLcJ45f8FNsMHAuZc=
|
||||
github.com/ergochat/irc-go v0.4.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
||||
github.com/ergochat/irc-go v0.5.0-rc1 h1:kFoIHExoNFQ2CV+iShAVna/H4xrXQB4t4jK5Sep2j9k=
|
||||
github.com/ergochat/irc-go v0.5.0-rc1/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
|
||||
github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
|
||||
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/ergochat/irc-go/ircfmt"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircreader"
|
||||
"github.com/ergochat/irc-go/ircutils"
|
||||
"github.com/xdg-go/scram"
|
||||
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
|
@ -120,13 +121,20 @@ type Client struct {
|
|||
|
||||
type saslStatus struct {
|
||||
mechanism string
|
||||
value strings.Builder
|
||||
value ircutils.SASLBuffer
|
||||
scramConv *scram.ServerConversation
|
||||
oauthConv *oauth2.OAuthBearerServer
|
||||
}
|
||||
|
||||
func (s *saslStatus) Initialize() {
|
||||
s.value.Initialize(saslMaxResponseLength)
|
||||
}
|
||||
|
||||
func (s *saslStatus) Clear() {
|
||||
*s = saslStatus{}
|
||||
s.mechanism = ""
|
||||
s.value.Clear()
|
||||
s.scramConv = nil
|
||||
s.oauthConv = nil
|
||||
}
|
||||
|
||||
// what stage the client is at w.r.t. the PASS command:
|
||||
|
@ -364,6 +372,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
|||
isTor: wConn.Tor,
|
||||
hideSTS: wConn.Tor || wConn.HideSTS,
|
||||
}
|
||||
session.sasl.Initialize()
|
||||
client.sessions = []*Session{session}
|
||||
|
||||
session.resetFakelag()
|
||||
|
|
|
@ -8,7 +8,6 @@ package irc
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
@ -180,7 +179,6 @@ func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
|||
}
|
||||
|
||||
const (
|
||||
saslMaxArgLength = 400 // required by SASL spec
|
||||
saslMaxResponseLength = 8192 // implementation-defined sanity check, long enough for bearer tokens
|
||||
)
|
||||
|
||||
|
@ -207,7 +205,7 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
|||
return false
|
||||
}
|
||||
|
||||
// start new sasl session
|
||||
// start new sasl session: parameter is the authentication mechanism
|
||||
if session.sasl.mechanism == "" {
|
||||
throttled, remainingTime := client.loginThrottle.Touch()
|
||||
if throttled {
|
||||
|
@ -246,44 +244,28 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
|||
return false
|
||||
}
|
||||
|
||||
// continue existing sasl session
|
||||
rawData := msg.Params[0]
|
||||
|
||||
// https://ircv3.net/specs/extensions/sasl-3.1:
|
||||
// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
|
||||
// and each chunk is sent as a separate AUTHENTICATE command."
|
||||
if len(rawData) > saslMaxArgLength {
|
||||
// continue existing sasl session: parameter is a message chunk
|
||||
done, value, err := session.sasl.value.Add(msg.Params[0])
|
||||
if err == nil {
|
||||
if done {
|
||||
// call actual handler
|
||||
handler := EnabledSaslMechanisms[session.sasl.mechanism]
|
||||
return handler(server, client, session, value, rb)
|
||||
} else {
|
||||
return false // wait for continuation line
|
||||
}
|
||||
}
|
||||
// else: error handling
|
||||
switch err {
|
||||
case ircutils.ErrSASLTooLong:
|
||||
rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
|
||||
session.sasl.Clear()
|
||||
return false
|
||||
} else if len(rawData) == saslMaxArgLength {
|
||||
if session.sasl.value.Len() >= saslMaxResponseLength {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
|
||||
session.sasl.Clear()
|
||||
return false
|
||||
}
|
||||
session.sasl.value.WriteString(rawData)
|
||||
return false
|
||||
case ircutils.ErrSASLLimitExceeded:
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
|
||||
default:
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
|
||||
}
|
||||
if rawData != "+" {
|
||||
session.sasl.value.WriteString(rawData)
|
||||
}
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if session.sasl.value.Len() > 0 {
|
||||
data, err = base64.StdEncoding.DecodeString(session.sasl.value.String())
|
||||
session.sasl.value.Reset()
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
|
||||
session.sasl.Clear()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// call actual handler
|
||||
handler := EnabledSaslMechanisms[session.sasl.mechanism]
|
||||
return handler(server, client, session, data, rb)
|
||||
session.sasl.Clear()
|
||||
return false
|
||||
}
|
||||
|
||||
// AUTHENTICATE PLAIN
|
||||
|
@ -495,21 +477,9 @@ func authOauthBearerHandler(server *Server, client *Client, session *Session, va
|
|||
|
||||
// helper to b64 a sasl response and chunk it into 400-byte lines
|
||||
// as per https://ircv3.net/specs/extensions/sasl-3.1
|
||||
// TODO replace this with ircutils.EncodeSASLResponse
|
||||
func sendSASLChallenge(server *Server, rb *ResponseBuffer, challenge []byte) {
|
||||
challengeStr := base64.StdEncoding.EncodeToString(challenge)
|
||||
lastLen := 0
|
||||
for len(challengeStr) > 0 {
|
||||
end := saslMaxArgLength
|
||||
if end > len(challengeStr) {
|
||||
end = len(challengeStr)
|
||||
}
|
||||
lastLen = end
|
||||
rb.Add(nil, server.name, "AUTHENTICATE", challengeStr[:end])
|
||||
challengeStr = challengeStr[end:]
|
||||
}
|
||||
if lastLen == saslMaxArgLength {
|
||||
rb.Add(nil, server.name, "AUTHENTICATE", "+")
|
||||
for _, chunk := range ircutils.EncodeSASLResponse(challenge) {
|
||||
rb.Add(nil, server.name, "AUTHENTICATE", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package ircutils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSASLLimitExceeded = errors.New("SASL total response size exceeded configured limit")
|
||||
ErrSASLTooLong = errors.New("SASL response chunk exceeded 400-byte limit")
|
||||
)
|
||||
|
||||
// EncodeSASLResponse encodes a raw SASL response as parameters to successive
|
||||
// AUTHENTICATE commands, as described in the IRCv3 SASL specification.
|
||||
func EncodeSASLResponse(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
|
||||
}
|
||||
|
||||
// SASLBuffer handles buffering and decoding SASL responses sent as parameters
|
||||
// to AUTHENTICATE commands, as described in the IRCv3 SASL specification.
|
||||
// Do not copy a SASLBuffer after first use.
|
||||
type SASLBuffer struct {
|
||||
maxLength int
|
||||
buffer strings.Builder
|
||||
}
|
||||
|
||||
// NewSASLBuffer returns a new SASLBuffer. maxLength is the maximum amount of
|
||||
// base64'ed data to buffer (0 for no limit).
|
||||
func NewSASLBuffer(maxLength int) *SASLBuffer {
|
||||
result := new(SASLBuffer)
|
||||
result.Initialize(maxLength)
|
||||
return result
|
||||
}
|
||||
|
||||
// Initialize initializes a SASLBuffer in place.
|
||||
func (b *SASLBuffer) Initialize(maxLength int) {
|
||||
b.maxLength = maxLength
|
||||
}
|
||||
|
||||
// Add processes an additional SASL response chunk sent via AUTHENTICATE.
|
||||
// If the response is complete, it resets the buffer and returns the decoded
|
||||
// response along with any decoding or protocol errors detected.
|
||||
func (b *SASLBuffer) Add(value string) (done bool, output []byte, err error) {
|
||||
if value == "+" {
|
||||
output, err = b.getAndReset()
|
||||
return true, output, err
|
||||
}
|
||||
|
||||
if len(value) > 400 {
|
||||
b.buffer.Reset()
|
||||
return true, nil, ErrSASLTooLong
|
||||
}
|
||||
|
||||
if b.maxLength != 0 && (b.buffer.Len()+len(value)) > b.maxLength {
|
||||
b.buffer.Reset()
|
||||
return true, nil, ErrSASLLimitExceeded
|
||||
}
|
||||
|
||||
b.buffer.WriteString(value)
|
||||
if len(value) < 400 {
|
||||
output, err = b.getAndReset()
|
||||
return true, output, err
|
||||
} else {
|
||||
// 400 bytes, wait for continuation line or +
|
||||
return false, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Clear resets the buffer state.
|
||||
func (b *SASLBuffer) Clear() {
|
||||
b.buffer.Reset()
|
||||
}
|
||||
|
||||
func (b *SASLBuffer) getAndReset() (output []byte, err error) {
|
||||
output, err = base64.StdEncoding.DecodeString(b.buffer.String())
|
||||
b.buffer.Reset()
|
||||
return
|
||||
}
|
|
@ -16,7 +16,7 @@ github.com/ergochat/confusables
|
|||
# github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
|
||||
## explicit; go 1.18
|
||||
github.com/ergochat/go-ident
|
||||
# github.com/ergochat/irc-go v0.4.0
|
||||
# github.com/ergochat/irc-go v0.5.0-rc1
|
||||
## explicit; go 1.15
|
||||
github.com/ergochat/irc-go/ircfmt
|
||||
github.com/ergochat/irc-go/ircmsg
|
||||
|
@ -30,8 +30,6 @@ github.com/go-sql-driver/mysql
|
|||
# github.com/gofrs/flock v0.8.1
|
||||
## explicit
|
||||
github.com/gofrs/flock
|
||||
# github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
## explicit
|
||||
# github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
## explicit; go 1.18
|
||||
github.com/golang-jwt/jwt/v5
|
||||
|
|
Loading…
Reference in New Issue