diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index a639985ee..b6b462667 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -12,7 +12,7 @@ jobs: uses: golangci/golangci-lint-action@v3 with: version: latest - args: "-v --new-from-rev HEAD~5" + args: "-v --new-from-rev HEAD~5 --timeout=5m" test-build-upload: strategy: matrix: diff --git a/go.mod b/go.mod index 3f27dbd5a..fb29a6574 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/labstack/echo/v4 v4.12.0 github.com/lrstanley/girc v0.0.0-20240519163535-a518c5b87a79 github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 - github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be + github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8 github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba @@ -150,6 +150,4 @@ require ( //replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 -go 1.21 - -toolchain go1.22.3 +go 1.22.0 diff --git a/go.sum b/go.sum index 06a17f149..54ae75af2 100644 --- a/go.sum +++ b/go.sum @@ -968,8 +968,8 @@ github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kN github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 h1:pmPKkN3RJM9wVMZidR99epzK0+gatQiqVtvP1FacZcQ= github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696/go.mod h1:c6MxwqHD+0HvtAJjsHMIdPCiAwGiQwPRPTp69ACMg8A= -github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be h1:zlirT+LngOJ60G6FVzI87DljGZLUnfNzmXja61EjtYM= -github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q= +github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8 h1:UkpezVlW6/j4qB2FSsz4na2FfZUbIDMAblpjw0Cgqcg= +github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8/go.mod h1:Vl95jJ8rOBCHUR++A1n6nM5sMk1oHcwInsaxRTR9NM4= github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 h1:9XSppnbvvReVom+wphkeF4lbhuT6vCYIdyzpwFtW89c= github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27/go.mod h1:/x38AoZf70fK9yZ5gs3BNCaF7/J4QEo4ZpwtLjX95eQ= github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 h1:GslZKF7lW7oSisycGLpxPO+TnKJuA4VZuTWIfYZrClc= diff --git a/vendor/github.com/matterbridge/go-xmpp/README.md b/vendor/github.com/matterbridge/go-xmpp/README.md index 7808206ab..5f5751a59 100644 --- a/vendor/github.com/matterbridge/go-xmpp/README.md +++ b/vendor/github.com/matterbridge/go-xmpp/README.md @@ -3,4 +3,4 @@ go-xmpp go xmpp library (original was written by russ cox ) -[Documentation](https://godoc.org/github.com/mattn/go-xmpp) +[Documentation](https://godoc.org/github.com/xmppo/go-xmpp) diff --git a/vendor/github.com/matterbridge/go-xmpp/xmpp.go b/vendor/github.com/matterbridge/go-xmpp/xmpp.go index 30a7ff74a..6f9e87a69 100644 --- a/vendor/github.com/matterbridge/go-xmpp/xmpp.go +++ b/vendor/github.com/matterbridge/go-xmpp/xmpp.go @@ -15,31 +15,48 @@ package xmpp import ( "bufio" "bytes" - "crypto/md5" + "crypto/hmac" "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/binary" "encoding/xml" "errors" "fmt" + "hash" "io" "math/big" "net" "net/http" "net/url" "os" + "regexp" + "slices" + "strconv" "strings" + "sync" "time" + + "golang.org/x/crypto/pbkdf2" + "golang.org/x/net/proxy" ) const ( - nsStream = "http://etherx.jabber.org/streams" - nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" - nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" - nsBind = "urn:ietf:params:xml:ns:xmpp-bind" - nsClient = "jabber:client" - nsSession = "urn:ietf:params:xml:ns:xmpp-session" + nsStream = "http://etherx.jabber.org/streams" + nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" + nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" + nsSASL2 = "urn:xmpp:sasl:2" + nsBind = "urn:ietf:params:xml:ns:xmpp-bind" + nsBind2 = "urn:xmpp:bind:0" + nsFast = "urn:xmpp:fast:0" + nsSASLCB = "urn:xmpp:sasl-cb:0" + nsClient = "jabber:client" + nsSession = "urn:ietf:params:xml:ns:xmpp-session" + nsStreamLimits = "urn:xmpp:stream-limits:0" ) // Default TLS configuration options @@ -59,12 +76,26 @@ func getCookie() Cookie { return Cookie(binary.LittleEndian.Uint64(buf[:])) } -// Client holds XMPP connection opitons +// Fast holds the XEP-0484 fast token, mechanism and expiry date +type Fast struct { + Token string + Mechanism string + Expiry time.Time +} + +// Client holds XMPP connection options type Client struct { - conn net.Conn // connection to server - jid string // Jabber ID for our connection - domain string - p *xml.Decoder + conn net.Conn // connection to server + jid string // Jabber ID for our connection + domain string + nextMutex sync.Mutex // Mutex to prevent multiple access to xml.Decoder + shutdown bool // Variable signalling that the stream will be closed + p *xml.Decoder + stanzaWriter io.Writer + LimitMaxBytes int // Maximum stanza size (XEP-0478: Stream Limits Advertisement) + LimitIdleSeconds int // Maximum idle seconds (XEP-0478: Stream Limits Advertisement) + Mechanism string // SCRAM mechanism used. + Fast Fast // XEP-0484 FAST Token, mechanism and expiry. } func (c *Client) JID() string { @@ -90,12 +121,12 @@ func connect(host, user, passwd string, timeout time.Duration) (net.Conn, error) addr += ":5222" } - proxy := os.Getenv("HTTP_PROXY") - if proxy == "" { - proxy = os.Getenv("http_proxy") + http_proxy := os.Getenv("HTTP_PROXY") + if http_proxy == "" { + http_proxy = os.Getenv("http_proxy") } // test for no proxy, takes a comma separated list with substrings to match - if proxy != "" { + if http_proxy != "" { noproxy := os.Getenv("NO_PROXY") if noproxy == "" { noproxy = os.Getenv("no_proxy") @@ -104,25 +135,38 @@ func connect(host, user, passwd string, timeout time.Duration) (net.Conn, error) nplist := strings.Split(noproxy, ",") for _, s := range nplist { if containsIgnoreCase(addr, s) { - proxy = "" + http_proxy = "" break } } } } - if proxy != "" { - url, err := url.Parse(proxy) + socks5Target, socks5 := strings.CutPrefix(http_proxy, "socks5://") + if http_proxy != "" && !socks5 { + url, err := url.Parse(http_proxy) if err == nil { addr = url.Host } } - - c, err := net.DialTimeout("tcp", addr, timeout) - if err != nil { - return nil, err + var c net.Conn + var err error + if socks5 { + dialer, err := proxy.SOCKS5("tcp", socks5Target, nil, nil) + if err != nil { + return nil, err + } + c, err = dialer.Dial("tcp", addr) + if err != nil { + return nil, err + } + } else { + c, err = net.DialTimeout("tcp", addr, timeout) + if err != nil { + return nil, err + } } - if proxy != "" { + if http_proxy != "" && !socks5 { fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host) fmt.Fprintf(c, "Host: %s\r\n", host) fmt.Fprintf(c, "\r\n") @@ -198,6 +242,40 @@ type Options struct { // Status message StatusMessage string + + // Auth mechanism to use + Mechanism string + + // XEP-0474: SASL SCRAM Downgrade Protection + SSDP bool + + // XEP-0388: Extensible SASL Profile + // Value for software + UserAgentSW string + + // XEP-0388: XEP-0388: Extensible SASL Profile + // Value for device + UserAgentDev string + + // XEP-0388: Extensible SASL Profile + // Unique stable identifier for the client installation + // MUST be a valid UUIDv4 + UserAgentID string + + // Enable XEP-0484: Fast Authentication Streamlining Tokens + Fast bool + + // XEP-0484: Fast Authentication Streamlining Tokens + // Fast Token + FastToken string + + // XEP-0484: Fast Authentication Streamlining Tokens + // Fast Mechanism + FastMechanism string + + // XEP-0484: Fast Authentication Streamlining Tokens + // Invalidate the current token + FastInvalidate bool } // NewClient establishes a new Client connection based on a set of Options. @@ -299,31 +377,31 @@ func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) { // Close closes the XMPP connection func (c *Client) Close() error { + c.shutdown = true if c.conn != (*tls.Conn)(nil) { - return c.conn.Close() + fmt.Fprintf(c.stanzaWriter, "\n") + go func() { + <-time.After(10 * time.Second) + c.conn.Close() + }() + // Wait for the server also closing the stream. + for { + ee, err := c.nextEnd() + // If the server already closed the stream it is + // likely to receive an error when trying to parse + // the stream. Therefore the connection is also closed + // if an error is received. + if err != nil { + return c.conn.Close() + } + if ee.Name.Local == "stream" { + return c.conn.Close() + } + } } return nil } -func saslDigestResponse(username, realm, passwd, nonce, cnonceStr, authenticate, digestURI, nonceCountStr string) string { - h := func(text string) []byte { - h := md5.New() - h.Write([]byte(text)) - return h.Sum(nil) - } - hex := func(bytes []byte) string { - return fmt.Sprintf("%x", bytes) - } - kd := func(secret, data string) []byte { - return h(secret + ":" + data) - } - - a1 := string(h(username+":"+realm+":"+passwd)) + ":" + nonce + ":" + cnonceStr - a2 := authenticate + ":" + digestURI - response := hex(kd(hex(h(a1)), nonce+":"+nonceCountStr+":"+cnonceStr+":auth:"+hex(h(a2)))) - return response -} - func cnonce() string { randSize := big.NewInt(0) randSize.Lsh(big.NewInt(1), 64) @@ -357,17 +435,47 @@ func (c *Client) init(o *Options) error { if err != nil { return err } + // Make the max. stanza size limit available. + if f.Limits.MaxBytes != "" { + c.LimitMaxBytes, err = strconv.Atoi(f.Limits.MaxBytes) + if err != nil { + c.LimitMaxBytes = 0 + } + } + // Make the servers time limit after which it might consider the stream idle available. + if f.Limits.IdleSeconds != "" { + c.LimitIdleSeconds, err = strconv.Atoi(f.Limits.IdleSeconds) + if err != nil { + c.LimitIdleSeconds = 0 + } + } // If the server requires we STARTTLS, attempt to do so. if f, err = c.startTLSIfRequired(f, o, domain); err != nil { return err } - + var mechanism, channelBinding, clientFirstMessage, clientFinalMessageBare, authMessage string + var bind2Data, resource, userAgentSW, userAgentDev, userAgentID, fastAuth string + var serverSignature, keyingMaterial []byte + var scramPlus, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool + var cbsSlice, mechSlice []string + var tlsConn *tls.Conn + // Use SASL2 if available + if f.Authentication.Mechanism != nil && c.IsEncrypted() { + sasl2 = true + mechSlice = f.Authentication.Mechanism + // Detect whether bind2 is available + if f.Authentication.Inline.Bind.Xmlns != "" { + bind2 = true + } + } else { + mechSlice = f.Mechanisms.Mechanism + } if o.User == "" && o.Password == "" { foundAnonymous := false - for _, m := range f.Mechanisms.Mechanism { + for _, m := range mechSlice { if m == "ANONYMOUS" { - fmt.Fprintf(c.conn, "\n", nsSASL) + fmt.Fprintf(c.stanzaWriter, "\n", nsSASL) foundAnonymous = true break } @@ -383,85 +491,530 @@ func (c *Client) init(o *Options) error { return errors.New("refusing to authenticate over unencrypted TCP connection") } - mechanism := "" - for _, m := range f.Mechanisms.Mechanism { - if m == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" { - mechanism = m - // Oauth authentication: send base64-encoded \x00 user \x00 token. - raw := "\x00" + user + "\x00" + o.OAuthToken - enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) - base64.StdEncoding.Encode(enc, []byte(raw)) - fmt.Fprintf(c.conn, "%s\n", nsSASL, o.OAuthXmlNs, enc) - break + tlsConn, ok = c.conn.(*tls.Conn) + if ok { + tlsConnOK = true + } + mechanism = "" + if o.Mechanism != "" { + if slices.Contains(mechSlice, o.Mechanism) { + mechanism = o.Mechanism } - if m == "PLAIN" { - mechanism = m - // Plain authentication: send base64-encoded \x00 user \x00 password. - raw := "\x00" + user + "\x00" + o.Password - enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) - base64.StdEncoding.Encode(enc, []byte(raw)) - fmt.Fprintf(c.conn, "%s\n", nsSASL, enc) - break + } else { + switch { + case slices.Contains(mechSlice, "SCRAM-SHA-512-PLUS") && tlsConnOK: + mechanism = "SCRAM-SHA-512-PLUS" + case slices.Contains(mechSlice, "SCRAM-SHA-256-PLUS") && tlsConnOK: + mechanism = "SCRAM-SHA-256-PLUS" + case slices.Contains(mechSlice, "SCRAM-SHA-1-PLUS") && tlsConnOK: + mechanism = "SCRAM-SHA-1-PLUS" + case slices.Contains(mechSlice, "SCRAM-SHA-512"): + mechanism = "SCRAM-SHA-512" + case slices.Contains(mechSlice, "SCRAM-SHA-256"): + mechanism = "SCRAM-SHA-256" + case slices.Contains(mechSlice, "SCRAM-SHA-1"): + mechanism = "SCRAM-SHA-1" + case slices.Contains(mechSlice, "X-OAUTH2"): + mechanism = "X-OAUTH2" + case slices.Contains(mechSlice, "PLAIN") && tlsConnOK: + mechanism = "PLAIN" + } + } + if strings.HasPrefix(mechanism, "SCRAM-SHA") { + if strings.HasSuffix(mechanism, "PLUS") { + scramPlus = true } - if m == "DIGEST-MD5" { - mechanism = m - // Digest-MD5 authentication - fmt.Fprintf(c.conn, "\n", nsSASL) - var ch saslChallenge - if err = c.p.DecodeElement(&ch, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) + if scramPlus { + for _, cbs := range f.ChannelBindings.ChannelBinding { + cbsSlice = append(cbsSlice, cbs.Type) } - b, err := base64.StdEncoding.DecodeString(string(ch)) - if err != nil { - return err + tlsState := tlsConn.ConnectionState() + switch tlsState.Version { + case tls.VersionTLS13: + tls13 = true + if slices.Contains(cbsSlice, "tls-server-end-point") && !slices.Contains(cbsSlice, "tls-exporter") { + serverEndPoint = true + } else { + keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32) + if err != nil { + return err + } + } + case tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12: + if slices.Contains(cbsSlice, "tls-server-end-point") && !slices.Contains(cbsSlice, "tls-unique") { + serverEndPoint = true + } else { + keyingMaterial = tlsState.TLSUnique + } + default: + return errors.New(mechanism + ": unknown TLS version") + } + if serverEndPoint { + var h hash.Hash + // This material is not necessary for `tls-server-end-point` binding, but it is required to check that + // the TLS connection was not renegotiated. This function will fail if that's the case (see + // https://pkg.go.dev/crypto/tls#ConnectionState.ExportKeyingMaterial + _, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32) + if err != nil { + return err + } + switch tlsState.PeerCertificates[0].SignatureAlgorithm { + case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.ECDSAWithSHA1, + x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS: + h = sha256.New() + case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS: + h = sha512.New384() + case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS: + h = sha512.New() + } + h.Write(tlsState.PeerCertificates[0].Raw) + keyingMaterial = h.Sum(nil) + h.Reset() } - tokens := map[string]string{} - for _, token := range strings.Split(string(b), ",") { - kv := strings.SplitN(strings.TrimSpace(token), "=", 2) - if len(kv) == 2 { - if kv[1][0] == '"' && kv[1][len(kv[1])-1] == '"' { - kv[1] = kv[1][1 : len(kv[1])-1] + if len(keyingMaterial) == 0 { + return errors.New(mechanism + ": no keying material") + } + switch { + case tls13 && !serverEndPoint: + channelBinding = base64.StdEncoding.EncodeToString(append([]byte("p=tls-exporter,,"), keyingMaterial[:]...)) + case serverEndPoint: + channelBinding = base64.StdEncoding.EncodeToString(append([]byte("p=tls-server-end-point,,"), keyingMaterial[:]...)) + default: + channelBinding = base64.StdEncoding.EncodeToString(append([]byte("p=tls-unique,,"), keyingMaterial[:]...)) + } + } + var shaNewFn func() hash.Hash + switch mechanism { + case "SCRAM-SHA-512", "SCRAM-SHA-512-PLUS": + shaNewFn = sha512.New + case "SCRAM-SHA-256", "SCRAM-SHA-256-PLUS": + shaNewFn = sha256.New + case "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS": + shaNewFn = sha1.New + default: + return errors.New("unsupported auth mechanism") + } + clientNonce := cnonce() + if scramPlus { + switch { + case tls13 && !serverEndPoint: + clientFirstMessage = "p=tls-exporter,,n=" + user + ",r=" + clientNonce + case serverEndPoint: + clientFirstMessage = "p=tls-server-end-point,,n=" + user + ",r=" + clientNonce + default: + clientFirstMessage = "p=tls-unique,,n=" + user + ",r=" + clientNonce + } + } else { + clientFirstMessage = "n,,n=" + user + ",r=" + clientNonce + } + if sasl2 { + if bind2 { + if o.UserAgentSW != "" { + resource = o.UserAgentSW + } else { + resource = "go-xmpp" + } + bind2Data = fmt.Sprintf("%s", + nsBind2, resource) + } + if o.UserAgentSW != "" { + userAgentSW = fmt.Sprintf("%s", o.UserAgentSW) + } else { + userAgentSW = "go-xmpp" + } + if o.UserAgentDev != "" { + userAgentDev = fmt.Sprintf("%s", o.UserAgentDev) + } + if o.UserAgentID != "" { + userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID) + } + if o.Fast && f.Authentication.Inline.Fast.Mechanism != nil && o.UserAgentID != "" && c.IsEncrypted() { + var mech string + if o.FastToken == "" { + m := f.Authentication.Inline.Fast.Mechanism + switch { + case slices.Contains(m, "HT-SHA-256-EXPR") && tls13: + mech = "HT-SHA-256-EXPR" + case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13: + mech = "HT-SHA-256-UNIQ" + case slices.Contains(m, "HT-SHA-256-ENDP"): + mech = "HT-SHA-256-ENDP" + case slices.Contains(m, "HT-SHA-256-NONE"): + mech = "HT-SHA-256-NONE" + default: + return fmt.Errorf("fast: unsupported auth mechanism %s", m) + } + fastAuth = fmt.Sprintf("", nsFast, mech) + } else { + var fastInvalidate string + if o.FastInvalidate { + fastInvalidate = " invalidate='true'" + } + fastAuth = fmt.Sprintf("", nsFast, fastInvalidate) + tlsState := tlsConn.ConnectionState() + mechanism = o.FastMechanism + switch mechanism { + case "HT-SHA-256-EXPR": + keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32) + if err != nil { + return err + } + case "HT-SHA-256-UNIQ": + keyingMaterial = tlsState.TLSUnique + case "HT-SHA-256-ENDP": + var h hash.Hash + switch tlsState.PeerCertificates[0].SignatureAlgorithm { + case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.ECDSAWithSHA1, + x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS: + h = sha256.New() + case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS: + h = sha512.New384() + case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS: + h = sha512.New() + } + h.Write(tlsState.PeerCertificates[0].Raw) + keyingMaterial = h.Sum(nil) + h.Reset() + case "HT-SHA-256-NONE": + keyingMaterial = []byte("") + default: + return fmt.Errorf("fast: unsupported auth mechanism %s", mechanism) } - tokens[kv[0]] = kv[1] + h := hmac.New(sha256.New, []byte(o.FastToken)) + initiator := append([]byte("Initiator")[:], keyingMaterial[:]...) + _, err = h.Write(initiator) + if err != nil { + return err + } + initiatorHashedToken := h.Sum(nil) + user := strings.Split(o.User, "@")[0] + clientFirstMessage = user + "\x00" + string(initiatorHashedToken) } } - realm, _ := tokens["realm"] - nonce, _ := tokens["nonce"] - qop, _ := tokens["qop"] - charset, _ := tokens["charset"] - cnonceStr := cnonce() - digestURI := "xmpp/" + domain - nonceCount := fmt.Sprintf("%08x", 1) - digest := saslDigestResponse(user, realm, o.Password, nonce, cnonceStr, "AUTHENTICATE", digestURI, nonceCount) - message := "username=\"" + user + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", cnonce=\"" + cnonceStr + - "\", nc=" + nonceCount + ", qop=" + qop + ", digest-uri=\"" + digestURI + "\", response=" + digest + ", charset=" + charset - - fmt.Fprintf(c.conn, "%s\n", nsSASL, base64.StdEncoding.EncodeToString([]byte(message))) - - var rspauth saslRspAuth - if err = c.p.DecodeElement(&rspauth, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) + fmt.Fprintf(c.stanzaWriter, + "%s%s%s%s%s\n", + nsSASL2, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth) + } else { + fmt.Fprintf(c.stanzaWriter, "%s\n", + nsSASL, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage))) + } + var sfm string + _, val, err := c.next() + if err != nil { + return err + } + switch v := val.(type) { + case *sasl2Failure: + errorMessage := v.Text + if errorMessage == "" { + // v.Any is type of sub-element in failure, + // which gives a description of what failed if there was no text element + errorMessage = v.Any.Local } - b, err = base64.StdEncoding.DecodeString(string(rspauth)) - if err != nil { - return err + return errors.New("auth failure: " + errorMessage) + case *saslFailure: + errorMessage := v.Text + if errorMessage == "" { + // v.Any is type of sub-element in failure, + // which gives a description of what failed if there was no text element + errorMessage = v.Any.Local } - fmt.Fprintf(c.conn, "\n", nsSASL) - break + return errors.New("auth failure: " + errorMessage) + case *sasl2Success: + if strings.HasPrefix(mechanism, "SCRAM-SHA") { + successMsg, err := base64.StdEncoding.DecodeString(v.AdditionalData) + if err != nil { + return err + } + if !strings.HasPrefix(string(successMsg), "v=") { + return errors.New("server sent unexpected content in SCRAM success message") + } + c.Mechanism = mechanism + } + if strings.HasPrefix(mechanism, "HT-SHA") { + // TODO: Check whether server implementations already support + // https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html#section-3.3 + h := hmac.New(sha256.New, []byte(o.FastToken)) + responder := append([]byte("Responder")[:], keyingMaterial[:]...) + _, err = h.Write(responder) + if err != nil { + return err + } + responderMsgRcv, err := base64.StdEncoding.DecodeString(v.AdditionalData) + if err != nil { + return err + } + responderMsgCalc := h.Sum(nil) + if string(responderMsgCalc) != string(responderMsgRcv) { + return fmt.Errorf("server sent unexpected content in FAST success message") + } + c.Mechanism = mechanism + } + if bind2 { + c.jid = v.AuthorizationIdentifier + } + if v.Token.Token != "" && v.Token.Token != o.FastToken { + m := f.Authentication.Inline.Fast.Mechanism + switch { + case slices.Contains(m, "HT-SHA-256-EXPR") && tls13: + c.Fast.Mechanism = "HT-SHA-256-EXPR" + case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13: + c.Fast.Mechanism = "HT-SHA-256-UNIQ" + case slices.Contains(m, "HT-SHA-256-ENDP"): + c.Fast.Mechanism = "HT-SHA-256-ENDP" + case slices.Contains(m, "HT-SHA-256-NONE"): + c.Fast.Mechanism = "HT-SHA-256-NONE" + } + c.Fast.Token = v.Token.Token + c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry) + } + if o.Session { + // if server support session, open it + cookie := getCookie() // generate new id value for session + fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(domain), cookie, nsSession) + } + + // We're connected and can now receive and send messages. + fmt.Fprintf(c.stanzaWriter, "%s%s\n", o.Status, o.StatusMessage) + return nil + case *sasl2Challenge: + sfm = v.Text + case *saslChallenge: + sfm = v.Text + } + b, err := base64.StdEncoding.DecodeString(sfm) + if err != nil { + return err + } + var serverNonce, dgProtect string + var salt []byte + var iterations int + for _, serverReply := range strings.Split(string(b), ",") { + switch { + case strings.HasPrefix(serverReply, "r="): + serverNonce = strings.SplitN(serverReply, "=", 2)[1] + if !strings.HasPrefix(serverNonce, clientNonce) { + return errors.New("SCRAM: server nonce didn't start with client nonce") + } + case strings.HasPrefix(serverReply, "s="): + salt, err = base64.StdEncoding.DecodeString(strings.SplitN(serverReply, "=", 2)[1]) + if err != nil { + return err + } + if string(salt) == "" { + return errors.New("SCRAM: server sent empty salt") + } + case strings.HasPrefix(serverReply, "i="): + iterations, err = strconv.Atoi(strings.SplitN(serverReply, + "=", 2)[1]) + if err != nil { + return err + } + case strings.HasPrefix(serverReply, "d=") && o.SSDP: + serverDgProtectHash := strings.SplitN(serverReply, "=", 2)[1] + slices.Sort(f.Mechanisms.Mechanism) + for _, mech := range f.Mechanisms.Mechanism { + if dgProtect == "" { + dgProtect = mech + } else { + dgProtect = dgProtect + "," + mech + } + } + dgProtect = dgProtect + "|" + slices.Sort(cbsSlice) + for i, cb := range cbsSlice { + if i == 0 { + dgProtect = dgProtect + cb + } else { + dgProtect = dgProtect + "," + cb + } + } + dgh := shaNewFn() + dgh.Write([]byte(dgProtect)) + dHash := dgh.Sum(nil) + dHashb64 := base64.StdEncoding.EncodeToString(dHash) + if dHashb64 != serverDgProtectHash { + return errors.New("SCRAM: downgrade protection hash mismatch") + } + dgh.Reset() + case strings.HasPrefix(serverReply, "m="): + return errors.New("SCRAM: server sent reserved 'm' attribute.") + } + } + if scramPlus { + clientFinalMessageBare = "c=" + channelBinding + ",r=" + serverNonce + } else { + clientFinalMessageBare = "c=biws,r=" + serverNonce + } + saltedPassword := pbkdf2.Key([]byte(o.Password), salt, + iterations, shaNewFn().Size(), shaNewFn) + h := hmac.New(shaNewFn, saltedPassword) + _, err = h.Write([]byte("Client Key")) + if err != nil { + return err + } + clientKey := h.Sum(nil) + h.Reset() + var storedKey []byte + switch mechanism { + case "SCRAM-SHA-512", "SCRAM-SHA-512-PLUS": + storedKey512 := sha512.Sum512(clientKey) + storedKey = storedKey512[:] + case "SCRAM-SHA-256", "SCRAM-SH-256-PLUS": + storedKey256 := sha256.Sum256(clientKey) + storedKey = storedKey256[:] + case "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS": + storedKey1 := sha1.Sum(clientKey) + storedKey = storedKey1[:] + } + _, err = h.Write([]byte("Server Key")) + if err != nil { + return err + } + serverFirstMessage, err := base64.StdEncoding.DecodeString(sfm) + if err != nil { + return err + } + authMessage = strings.SplitAfter(clientFirstMessage, ",,")[1] + "," + + string(serverFirstMessage) + "," + clientFinalMessageBare + h = hmac.New(shaNewFn, storedKey[:]) + _, err = h.Write([]byte(authMessage)) + if err != nil { + return err + } + clientSignature := h.Sum(nil) + h.Reset() + if len(clientKey) != len(clientSignature) { + return errors.New("SCRAM: client key and signature length mismatch") + } + clientProof := make([]byte, len(clientKey)) + for i := range clientKey { + clientProof[i] = clientKey[i] ^ clientSignature[i] + } + h = hmac.New(shaNewFn, saltedPassword) + _, err = h.Write([]byte("Server Key")) + if err != nil { + return err + } + serverKey := h.Sum(nil) + h.Reset() + h = hmac.New(shaNewFn, serverKey) + _, err = h.Write([]byte(authMessage)) + if err != nil { + return err + } + serverSignature = h.Sum(nil) + if string(serverSignature) == "" { + return errors.New("SCRAM: calculated an empty server signature") + } + clientFinalMessage := base64.StdEncoding.EncodeToString([]byte(clientFinalMessageBare + + ",p=" + base64.StdEncoding.EncodeToString(clientProof))) + if sasl2 { + fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL2, + clientFinalMessage) + } else { + fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL, + clientFinalMessage) + } + } + if mechanism == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" { + // Oauth authentication: send base64-encoded \x00 user \x00 token. + raw := "\x00" + user + "\x00" + o.OAuthToken + enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) + base64.StdEncoding.Encode(enc, []byte(raw)) + if sasl2 { + fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL2, o.OAuthXmlNs, enc) + } else { + fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL, o.OAuthXmlNs, enc) } } - if mechanism == "" { - return fmt.Errorf("PLAIN authentication is not an option: %v", f.Mechanisms.Mechanism) + if mechanism == "PLAIN" { + // Plain authentication: send base64-encoded \x00 user \x00 password. + raw := "\x00" + user + "\x00" + o.Password + enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) + base64.StdEncoding.Encode(enc, []byte(raw)) + if sasl2 { + fmt.Fprintf(c.conn, "%s\n", nsSASL2, enc) + } else { + fmt.Fprintf(c.conn, "%s\n", nsSASL, enc) + } } } + if mechanism == "" { + return fmt.Errorf("no viable authentication method available: %v", f.Mechanisms.Mechanism) + } // Next message should be either success or failure. - name, val, err := next(c.p) + name, val, err := c.next() if err != nil { return err } switch v := val.(type) { + case *sasl2Success: + if strings.HasPrefix(mechanism, "SCRAM-SHA") { + successMsg, err := base64.StdEncoding.DecodeString(v.AdditionalData) + if err != nil { + return err + } + if !strings.HasPrefix(string(successMsg), "v=") { + return errors.New("server sent unexpected content in SCRAM success message") + } + serverSignatureReply := strings.SplitN(string(successMsg), "v=", 2)[1] + serverSignatureRemote, err := base64.StdEncoding.DecodeString(serverSignatureReply) + if err != nil { + return err + } + if string(serverSignature) != string(serverSignatureRemote) { + return errors.New("SCRAM: server signature mismatch") + } + c.Mechanism = mechanism + } + if bind2 { + c.jid = v.AuthorizationIdentifier + } + if v.Token.Token != "" { + m := f.Authentication.Inline.Fast.Mechanism + switch { + case slices.Contains(m, "HT-SHA-256-EXPR") && tls13: + c.Fast.Mechanism = "HT-SHA-256-EXPR" + case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13: + c.Fast.Mechanism = "HT-SHA-256-UNIQ" + case slices.Contains(m, "HT-SHA-256-ENDP"): + c.Fast.Mechanism = "HT-SHA-256-ENDP" + case slices.Contains(m, "HT-SHA-256-NONE"): + c.Fast.Mechanism = "HT-SHA-256-NONE" + } + c.Fast.Token = v.Token.Token + c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry) + } case *saslSuccess: + if strings.HasPrefix(mechanism, "SCRAM-SHA") { + successMsg, err := base64.StdEncoding.DecodeString(v.Text) + if err != nil { + return err + } + if !strings.HasPrefix(string(successMsg), "v=") { + return errors.New("server sent unexpected content in SCRAM success message") + } + serverSignatureReply := strings.SplitN(string(successMsg), "v=", 2)[1] + serverSignatureRemote, err := base64.StdEncoding.DecodeString(serverSignatureReply) + if err != nil { + return err + } + if string(serverSignature) != string(serverSignatureRemote) { + return errors.New("SCRAM: server signature mismatch") + } + c.Mechanism = mechanism + } + case *sasl2Failure: + errorMessage := v.Text + if errorMessage == "" { + // v.Any is type of sub-element in failure, + // which gives a description of what failed if there was no text element + errorMessage = v.Any.Local + } + return errors.New("auth failure: " + errorMessage) case *saslFailure: errorMessage := v.Text if errorMessage == "" { @@ -474,38 +1027,68 @@ func (c *Client) init(o *Options) error { return errors.New("expected or , got <" + name.Local + "> in " + name.Space) } - // Now that we're authenticated, we're supposed to start the stream over again. - // Declare intent to be a jabber client. - if f, err = c.startStream(o, domain); err != nil { - return err - } - - // Generate a unique cookie - cookie := getCookie() - - // Send IQ message asking to bind to the local user name. - if o.Resource == "" { - fmt.Fprintf(c.conn, "\n", cookie, nsBind) - } else { - fmt.Fprintf(c.conn, "%s\n", cookie, nsBind, o.Resource) + if !sasl2 { + // Now that we're authenticated, we're supposed to start the stream over again. + // Declare intent to be a jabber client. + if f, err = c.startStream(o, domain); err != nil { + return err + } } - var iq clientIQ - if err = c.p.DecodeElement(&iq, nil); err != nil { - return errors.New("unmarshal : " + err.Error()) + // Make the max. stanza size limit available. + if f.Limits.MaxBytes != "" { + c.LimitMaxBytes, err = strconv.Atoi(f.Limits.MaxBytes) + if err != nil { + c.LimitMaxBytes = 0 + } } - if &iq.Bind == nil { - return errors.New(" result missing ") + // Make the servers time limit after which it might consider the stream idle available. + if f.Limits.IdleSeconds != "" { + c.LimitIdleSeconds, err = strconv.Atoi(f.Limits.IdleSeconds) + if err != nil { + c.LimitIdleSeconds = 0 + } } - c.jid = iq.Bind.Jid // our local id - c.domain = domain + if !bind2 { + // Generate a unique cookie + cookie := getCookie() + + // Send IQ message asking to bind to the local user name. + if o.Resource == "" { + fmt.Fprintf(c.stanzaWriter, "\n", cookie, nsBind) + } else { + fmt.Fprintf(c.stanzaWriter, "%s\n", cookie, nsBind, o.Resource) + } + _, val, err = c.next() + if err != nil { + return err + } + switch v := val.(type) { + case *streamError: + errorMessage := v.Text.Text + if errorMessage == "" { + // v.Any is type of sub-element in failure, + // which gives a description of what failed if there was no text element + errorMessage = v.Any.Space + } + return errors.New("stream error: " + errorMessage) + case *clientIQ: + if v.Bind.XMLName.Space == nsBind { + c.jid = v.Bind.Jid // our local id + c.domain = domain + } else { + return errors.New("bind: unexpected reply to xmpp-bind IQ") + } + } + } if o.Session { // if server support session, open it - fmt.Fprintf(c.conn, "", xmlEscape(domain), cookie, nsSession) + cookie := getCookie() // generate new id value for session + fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(domain), cookie, nsSession) } // We're connected and can now receive and send messages. - fmt.Fprintf(c.conn, "%s%s", o.Status, o.StatusMessage) + fmt.Fprintf(c.stanzaWriter, "%s%s\n", o.Status, o.StatusMessage) return nil } @@ -527,7 +1110,7 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string } var err error - fmt.Fprintf(c.conn, "\n") + fmt.Fprintf(c.stanzaWriter, "\n") var k tlsProceed if err = c.p.DecodeElement(&k, nil); err != nil { return f, errors.New("unmarshal : " + err.Error()) @@ -560,20 +1143,31 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) { if o.Debug { c.p = xml.NewDecoder(tee{c.conn, DebugWriter}) + c.stanzaWriter = io.MultiWriter(c.conn, DebugWriter) } else { c.p = xml.NewDecoder(c.conn) + c.stanzaWriter = c.conn } - _, err := fmt.Fprintf(c.conn, "\n"+ - "\n", - xmlEscape(domain), nsClient, nsStream) - if err != nil { - return nil, err + if c.IsEncrypted() { + _, err := fmt.Fprintf(c.stanzaWriter, ""+ + "\n", + xmlEscape(o.User), xmlEscape(domain), nsClient, nsStream) + if err != nil { + return nil, err + } + } else { + _, err := fmt.Fprintf(c.stanzaWriter, ""+ + "\n", + xmlEscape(domain), nsClient, nsStream) + if err != nil { + return nil, err + } } // We expect the server to start a . - se, err := nextStart(c.p) + se, err := c.nextStart() if err != nil { return nil, err } @@ -607,6 +1201,7 @@ type Chat struct { Thread string Ooburl string Oobdesc string + Lang string ID string ReplaceID string Roster Roster @@ -641,14 +1236,21 @@ type IQ struct { } // Recv waits to receive the next XMPP stanza. -// Return type is either a presence notification or a chat message. func (c *Client) Recv() (stanza interface{}, err error) { for { - _, val, err := next(c.p) + _, val, err := c.next() if err != nil { return Chat{}, err } switch v := val.(type) { + case *streamError: + errorMessage := v.Text.Text + if errorMessage == "" { + // v.Any is type of sub-element in failure, + // which gives a description of what failed if there was no text element + errorMessage = v.Any.Space + } + return Chat{}, errors.New("stream error: " + errorMessage) case *clientMessage: if v.Event.XMLNS == XMPPNS_PUBSUB_EVENT { // Handle Pubsub notifications @@ -687,6 +1289,7 @@ func (c *Client) Recv() (stanza interface{}, err error) { Other: v.OtherStrings(), OtherElem: v.Other, Stamp: stamp, + Lang: v.Lang, } return chat, nil case *clientQuery: @@ -724,6 +1327,16 @@ func (c *Client) Recv() (stanza interface{}, err error) { return PubsubSubscription{ Errors: errsStr, }, nil + default: + res, err := xml.Marshal(v.Query) + if err != nil { + return Chat{}, err + } + + return IQ{ + ID: v.ID, From: v.From, To: v.To, Type: v.Type, + Query: res, + }, nil } case v.Type == "result": switch v.ID { @@ -830,23 +1443,31 @@ func (c *Client) Recv() (stanza interface{}, err error) { // can just use items1 and items3 to do the same // as an Avatar node is just a PEP (PubSub) node. /*case "retrieve1": - if v.Query.XMLName.Local == "pubsub" { - var p clientPubsubItems - err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) - if err != nil { - return PubsubItems{}, err - } + var p clientPubsubItems + err := xml.Unmarshal([]byte(v.Query.InnerXML), &p) + if err != nil { + return PubsubItems{}, err + } - switch p.Node { - case XMPPNS_AVATAR_PEP_DATA: - return handleAvatarData(p.Items[0].Body, - v.From, - p.Items[0].ID) - case XMPPNS_AVATAR_PEP_METADATA: - return handleAvatarMetadata(p.Items[0].Body, - v.From) - } + switch p.Node { + case XMPPNS_AVATAR_PEP_DATA: + return handleAvatarData(p.Items[0].Body, + v.From, + p.Items[0].ID) + case XMPPNS_AVATAR_PEP_METADATA: + return handleAvatarMetadata(p.Items[0].Body, + v }*/ + default: + res, err := xml.Marshal(v.Query) + if err != nil { + return Chat{}, err + } + + return IQ{ + ID: v.ID, From: v.From, To: v.To, Type: v.Type, + Query: res, + }, nil } case v.Query.XMLName.Local == "": return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type}, nil @@ -881,7 +1502,6 @@ func (c *Client) Send(chat Chat) (n int, err error) { } oobtext += `` } - if chat.ID != `` { msgidtext = `id='` + xmlEscape(chat.ID) + `'` } else { @@ -892,9 +1512,17 @@ func (c *Client) Send(chat Chat) (n int, err error) { msgcorrecttext = `` } - stanza := "" + subtext + "%s" + msgcorrecttext + oobtext + thdtext + "" + chat.Text = validUTF8(chat.Text) - return fmt.Fprintf(c.conn, stanza, xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) + stanza := fmt.Sprintf(""+subtext+"%s"+msgcorrecttext+oobtext+thdtext+"", + xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) + + if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { + return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", + len(stanza), c.LimitMaxBytes) + } + + return fmt.Fprint(c.stanzaWriter, stanza) } // SendOOB sends OOB data wrapped inside an XMPP message stanza, without actual body. @@ -910,17 +1538,72 @@ func (c *Client) SendOOB(chat Chat) (n int, err error) { } oobtext += `` } - return fmt.Fprintf(c.conn, ""+oobtext+thdtext+"", + stanza := fmt.Sprintf(""+oobtext+thdtext+"\n", xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce()) + if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { + return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", + len(stanza), c.LimitMaxBytes) + } + return fmt.Fprint(c.stanzaWriter, stanza) } // SendOrg sends the original text without being wrapped in an XMPP message stanza. func (c *Client) SendOrg(org string) (n int, err error) { - return fmt.Fprint(c.conn, org) + stanza := fmt.Sprint(org + "\n") + if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { + return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", + len(stanza), c.LimitMaxBytes) + } + return fmt.Fprint(c.stanzaWriter, stanza) } +// SendPresence sends Presence wrapped inside XMPP presence stanza. func (c *Client) SendPresence(presence Presence) (n int, err error) { - return fmt.Fprintf(c.conn, "", xmlEscape(presence.From), xmlEscape(presence.To)) + // Forge opening presence tag + var buf string = "" + + // TODO: there may be optional tag "priority", but former presence type does not take this into account + // so either we must follow std, change type xmpp.Presence and break backward compatibility + // or leave it as-is and potentially break client software + + if presence.Show != "" { + // https://www.ietf.org/rfc/rfc3921.txt 2.2.2.1, show can be only + // away, chat, dnd, xa + switch presence.Show { + case "away", "chat", "dnd", "xa": + buf = buf + fmt.Sprintf("%s", xmlEscape(presence.Show)) + } + } + + if presence.Status != "" { + buf = buf + fmt.Sprintf("%s", xmlEscape(presence.Status)) + } + + stanza := fmt.Sprintf(buf + "\n") + if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { + return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", + len(stanza), c.LimitMaxBytes) + } + return fmt.Fprint(c.stanzaWriter, stanza) } // SendKeepAlive sends a "whitespace keepalive" as described in chapter 4.6.1 of RFC6120. @@ -930,31 +1613,42 @@ func (c *Client) SendKeepAlive() (n int, err error) { // SendHtml sends the message as HTML as defined by XEP-0071 func (c *Client) SendHtml(chat Chat) (n int, err error) { - return fmt.Fprintf(c.conn, ""+ - "%s"+ - "%s", + stanza := fmt.Sprintf("%s"+ + "%s\n", xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text) + if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes { + return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)", + len(stanza), c.LimitMaxBytes) + } + return fmt.Fprint(c.stanzaWriter, stanza) } // Roster asks for the chat roster. func (c *Client) Roster() error { - fmt.Fprintf(c.conn, "\n", xmlEscape(c.jid)) + fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(c.jid)) return nil } // RFC 3920 C.1 Streams name space type streamFeatures struct { - XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` - StartTLS *tlsStartTLS - Mechanisms saslMechanisms - Bind bindBind - Session bool + XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` + Authentication sasl2Authentication + StartTLS *tlsStartTLS + Mechanisms saslMechanisms + ChannelBindings saslChannelBindings + Bind bindBind + Session bool + Limits streamLimits } type streamError struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"` Any xml.Name - Text string + Text struct { + Text string `xml:",chardata"` + Lang string `xml:"lang,attr"` + Xmlns string `xml:"xmlns,attr"` + } `xml:"text"` } // RFC 3920 C.3 TLS name space @@ -971,29 +1665,71 @@ type tlsFailure struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"` } +type sasl2Authentication struct { + XMLName xml.Name `xml:"urn:xmpp:sasl:2 authentication"` + Mechanism []string `xml:"mechanism"` + Inline struct { + Text string `xml:",chardata"` + Bind struct { + XMLName xml.Name `xml:"urn:xmpp:bind:0 bind"` + Xmlns string `xml:"xmlns,attr"` + Text string `xml:",chardata"` + } `xml:"bind"` + Fast struct { + XMLName xml.Name `xml:"urn:xmpp:fast:0 fast"` + Text string `xml:",chardata"` + Tls0rtt string `xml:"tls-0rtt,attr"` + Mechanism []string `xml:"mechanism"` + } `xml:"fast"` + } `xml:"inline"` +} + // RFC 3920 C.4 SASL name space type saslMechanisms struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` Mechanism []string `xml:"mechanism"` } -type saslAuth struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"` - Mechanism string `xml:",attr"` +type saslChannelBindings struct { + XMLName xml.Name `xml:"sasl-channel-binding"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + ChannelBinding []struct { + Text string `xml:",chardata"` + Type string `xml:"type,attr"` + } `xml:"channel-binding"` } -type saslChallenge string - -type saslRspAuth string - -type saslResponse string - type saslAbort struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"` } +type sasl2Success struct { + XMLName xml.Name `xml:"urn:xmpp:sasl:2 success"` + Text string `xml:",chardata"` + AdditionalData string `xml:"additional-data"` + AuthorizationIdentifier string `xml:"authorization-identifier"` + Bound struct { + Text string `xml:",chardata"` + Xmlns string `xml:"urn:xmpp:bind:0,attr"` + } `xml:"bound"` + Token struct { + Text string `xml:",chardata"` + Xmlns string `xml:"urn:xmpp:fast:0,attr"` + Expiry string `xml:"expiry,attr"` + Token string `xml:"token,attr"` + } `xml:"token"` +} + type saslSuccess struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"` + Text string `xml:",chardata"` +} + +type sasl2Failure struct { + XMLName xml.Name `xml:"urn:xmpp:sasl:2 failure"` + Any xml.Name `xml:",any"` + Text string `xml:"text"` } type saslFailure struct { @@ -1002,6 +1738,24 @@ type saslFailure struct { Text string `xml:"text"` } +type sasl2Challenge struct { + XMLName xml.Name `xml:"urn:xmpp:sasl:2 challenge"` + Text string `xml:",chardata"` +} + +type saslChallenge struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl challenge"` + Text string `xml:",chardata"` +} + +type streamLimits struct { + XMLName xml.Name `xml:"limits"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + MaxBytes string `xml:"max-bytes"` + IdleSeconds string `xml:"idle-seconds"` +} + // RFC 3920 C.5 Resource binding name space type bindBind struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` @@ -1021,6 +1775,7 @@ type clientMessage struct { ID string `xml:"id,attr"` To string `xml:"to,attr"` Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal + Lang string `xml:"lang,attr"` // These should technically be []clientText, but string is much more convenient. Subject string `xml:"subject"` @@ -1047,7 +1802,8 @@ func (m *clientMessage) OtherStrings() []string { type XMLElement struct { XMLName xml.Name - InnerXML string `xml:",innerxml"` + Attr []xml.Attr `xml:",any,attr"` // Save the attributes of the xml element + InnerXML string `xml:",innerxml"` } func (e *XMLElement) String() string { @@ -1076,11 +1832,6 @@ type Delay struct { Stamp string `xml:"stamp,attr"` } -type clientText struct { - Lang string `xml:",attr"` - Body string `xml:"chardata"` -} - type clientPresence struct { XMLName xml.Name `xml:"jabber:client presence"` From string `xml:"from,attr"` @@ -1131,25 +1882,60 @@ type rosterItem struct { } // Scan XML token stream to find next StartElement. -func nextStart(p *xml.Decoder) (xml.StartElement, error) { +func (c *Client) nextStart() (xml.StartElement, error) { for { - t, err := p.Token() - if err != nil || t == nil { + // Do not read from the stream if it's + // going to be closed. + if c.shutdown { + return xml.StartElement{}, io.EOF + } + c.nextMutex.Lock() + to, err := c.p.Token() + if err != nil || to == nil { + c.nextMutex.Unlock() return xml.StartElement{}, err } + t := xml.CopyToken(to) switch t := t.(type) { case xml.StartElement: + c.nextMutex.Unlock() return t, nil } + c.nextMutex.Unlock() + } +} + +// Scan XML token stream to find next EndElement +func (c *Client) nextEnd() (xml.EndElement, error) { + c.p.Strict = false + for { + c.nextMutex.Lock() + to, err := c.p.Token() + if err != nil || to == nil { + c.nextMutex.Unlock() + return xml.EndElement{}, err + } + t := xml.CopyToken(to) + switch t := t.(type) { + case xml.EndElement: + // Do not unlock mutex if the stream is closed to + // prevent further reading on the stream. + if t.Name.Local == "stream" { + return t, nil + } + c.nextMutex.Unlock() + return t, nil + } + c.nextMutex.Unlock() } } // Scan XML token stream for next element and save into val. // If val == nil, allocate new element based on proto map. // Either way, return val. -func next(p *xml.Decoder) (xml.Name, interface{}, error) { +func (c *Client) next() (xml.Name, interface{}, error) { // Read start element to find out what type we want. - se, err := nextStart(p) + se, err := c.nextStart() if err != nil { return xml.Name{}, nil, err } @@ -1169,16 +1955,24 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { nv = &tlsFailure{} case nsSASL + " mechanisms": nv = &saslMechanisms{} + case nsSASL2 + " challenge": + nv = &sasl2Challenge{} case nsSASL + " challenge": - nv = "" + nv = &saslChallenge{} case nsSASL + " response": nv = "" case nsSASL + " abort": nv = &saslAbort{} + case nsSASL2 + " success": + nv = &sasl2Success{} case nsSASL + " success": nv = &saslSuccess{} + case nsSASL2 + " failure": + nv = &sasl2Failure{} case nsSASL + " failure": nv = &saslFailure{} + case nsSASLCB + " sasl-channel-binding": + nv = &saslChannelBindings{} case nsBind + " bind": nv = &bindBind{} case nsClient + " message": @@ -1195,9 +1989,11 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { } // Unmarshal into that storage. - if err = p.DecodeElement(nv, &se); err != nil { + c.nextMutex.Lock() + if err = c.p.DecodeElement(nv, &se); err != nil { return xml.Name{}, nil, err } + c.nextMutex.Unlock() return se.Name, nv, err } @@ -1222,3 +2018,12 @@ func (t tee) Read(p []byte) (n int, err error) { } return } + +func validUTF8(s string) string { + // Remove invalid code points. + s = strings.ToValidUTF8(s, "�") + reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`) + s = reg.ReplaceAllString(s, "�") + + return s +} diff --git a/vendor/github.com/matterbridge/go-xmpp/xmpp_information_query.go b/vendor/github.com/matterbridge/go-xmpp/xmpp_information_query.go index 90dee9557..3bab3bb19 100644 --- a/vendor/github.com/matterbridge/go-xmpp/xmpp_information_query.go +++ b/vendor/github.com/matterbridge/go-xmpp/xmpp_information_query.go @@ -5,12 +5,14 @@ import ( "strconv" ) -const IQTypeGet = "get" -const IQTypeSet = "set" -const IQTypeResult = "result" +const ( + IQTypeGet = "get" + IQTypeSet = "set" + IQTypeResult = "result" +) func (c *Client) Discovery() (string, error) { - // use getCookie for a pseudo random id. + // use UUIDv4 for a pseudo random id. reqID := strconv.FormatUint(uint64(getCookie()), 10) return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "") } @@ -34,14 +36,14 @@ func (c *Client) DiscoverEntityItems(jid string) (string, error) { // RawInformationQuery sends an information query request to the server. func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) { - const xmlIQ = "%s" - _, err := fmt.Fprintf(c.conn, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body) + const xmlIQ = "%s\n" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body) return id, err } // rawInformation send a IQ request with the payload body to the server func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) { - const xmlIQ = "%s" - _, err := fmt.Fprintf(c.conn, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body) + const xmlIQ = "%s\n" + _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body) return id, err } diff --git a/vendor/github.com/matterbridge/go-xmpp/xmpp_muc.go b/vendor/github.com/matterbridge/go-xmpp/xmpp_muc.go index 840f4f97b..a80c3e3b4 100644 --- a/vendor/github.com/matterbridge/go-xmpp/xmpp_muc.go +++ b/vendor/github.com/matterbridge/go-xmpp/xmpp_muc.go @@ -25,7 +25,7 @@ const ( // Send sends room topic wrapped inside an XMPP message stanza body. func (c *Client) SendTopic(chat Chat) (n int, err error) { - return fmt.Fprintf(c.conn, ""+"%s", + return fmt.Fprintf(c.stanzaWriter, ""+"%s\n", xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text)) } @@ -33,10 +33,10 @@ func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) { if nick == "" { nick = c.jid } - return fmt.Fprintf(c.conn, "\n"+ + return fmt.Fprintf(c.stanzaWriter, ""+ ""+ - "\n"+ - "", + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC) } @@ -47,34 +47,34 @@ func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_da } switch history_type { case NoHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC) case CharHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, history) case StanzaHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, history) case SecondsHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, history) case SinceHistory: if history_date != nil { - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339)) } } @@ -88,40 +88,40 @@ func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_typ } switch history_type { case NoHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ "%s"+ - "\n"+ - "", + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password)) case CharHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "%s\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "%s"+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history) case StanzaHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "%s\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "%s"+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history) case SecondsHistory: - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "%s\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "%s"+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history) case SinceHistory: if history_date != nil { - return fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "%s\n"+ - "\n"+ - "", + return fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "%s"+ + ""+ + "\n", xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339)) } } @@ -130,6 +130,6 @@ func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_typ // xep-0045 7.14 func (c *Client) LeaveMUC(jid string) (n int, err error) { - return fmt.Fprintf(c.conn, "", + return fmt.Fprintf(c.stanzaWriter, "\n", c.jid, xmlEscape(jid)) } diff --git a/vendor/github.com/matterbridge/go-xmpp/xmpp_ping.go b/vendor/github.com/matterbridge/go-xmpp/xmpp_ping.go index 39269d80b..f851d415e 100644 --- a/vendor/github.com/matterbridge/go-xmpp/xmpp_ping.go +++ b/vendor/github.com/matterbridge/go-xmpp/xmpp_ping.go @@ -11,23 +11,23 @@ func (c *Client) PingC2S(jid, server string) error { if server == "" { server = c.domain } - _, err := fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "", + _, err := fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "\n", xmlEscape(jid), xmlEscape(server)) return err } func (c *Client) PingS2S(fromServer, toServer string) error { - _, err := fmt.Fprintf(c.conn, "\n"+ - "\n"+ - "", + _, err := fmt.Fprintf(c.stanzaWriter, ""+ + ""+ + "\n", xmlEscape(fromServer), xmlEscape(toServer)) return err } func (c *Client) SendResultPing(id, toServer string) error { - _, err := fmt.Fprintf(c.conn, "", + _, err := fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(toServer), xmlEscape(id)) return err } diff --git a/vendor/github.com/matterbridge/go-xmpp/xmpp_pubsub.go b/vendor/github.com/matterbridge/go-xmpp/xmpp_pubsub.go index 74e60d869..c5378cc73 100644 --- a/vendor/github.com/matterbridge/go-xmpp/xmpp_pubsub.go +++ b/vendor/github.com/matterbridge/go-xmpp/xmpp_pubsub.go @@ -22,11 +22,6 @@ type clientPubsubItems struct { Items []clientPubsubItem `xml:"item"` } -type clientPubsub struct { - XMLName xml.Name `xml:"pubsub"` - Items clientPubsubItems `xml:"items"` -} - type clientPubsubEvent struct { XMLName xml.Name `xml:"event"` XMLNS string `xml:"xmlns,attr"` diff --git a/vendor/github.com/matterbridge/go-xmpp/xmpp_subscription.go b/vendor/github.com/matterbridge/go-xmpp/xmpp_subscription.go index eb293141a..37a03d242 100644 --- a/vendor/github.com/matterbridge/go-xmpp/xmpp_subscription.go +++ b/vendor/github.com/matterbridge/go-xmpp/xmpp_subscription.go @@ -5,16 +5,21 @@ import ( ) func (c *Client) ApproveSubscription(jid string) { - fmt.Fprintf(c.conn, "", + fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(jid)) } func (c *Client) RevokeSubscription(jid string) { - fmt.Fprintf(c.conn, "", + fmt.Fprintf(c.stanzaWriter, "\n", + xmlEscape(jid)) +} + +func (c *Client) RetrieveSubscription(jid string) { + fmt.Fprintf(c.conn, "\n", xmlEscape(jid)) } func (c *Client) RequestSubscription(jid string) { - fmt.Fprintf(c.conn, "", + fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(jid)) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 71349373a..09312e2da 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -209,8 +209,8 @@ github.com/magiconair/properties github.com/matterbridge/Rocket.Chat.Go.SDK/models github.com/matterbridge/Rocket.Chat.Go.SDK/realtime github.com/matterbridge/Rocket.Chat.Go.SDK/rest -# github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be -## explicit +# github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8 +## explicit; go 1.21.5 github.com/matterbridge/go-xmpp # github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 ## explicit; go 1.17