go-xmpp/xmpp.go

2052 lines
60 KiB
Go
Raw Permalink Normal View History

// Copyright 2011 The Go Authors. All rights reserved.
2011-02-27 18:44:24 -08:00
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(rsc):
// More precise error handling.
// Presence functionality.
// TODO(mattn):
// Add proxy authentication.
// Package xmpp implements a simple Google Talk client
// using the XMPP protocol described in RFC 3920 and RFC 3921.
package xmpp
import (
"bufio"
"bytes"
"crypto/hmac"
2013-05-14 19:24:35 -07:00
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
2011-02-27 18:44:24 -08:00
"crypto/tls"
"crypto/x509"
2011-02-27 18:44:24 -08:00
"encoding/base64"
"encoding/binary"
2011-11-09 06:18:51 -08:00
"encoding/xml"
2011-11-04 06:40:10 -07:00
"errors"
2011-02-27 18:44:24 -08:00
"fmt"
"hash"
2011-02-27 18:44:24 -08:00
"io"
2013-05-14 19:24:35 -07:00
"math/big"
2011-02-27 18:44:24 -08:00
"net"
2011-11-09 06:18:51 -08:00
"net/http"
"net/url"
2011-02-27 18:44:24 -08:00
"os"
"regexp"
"slices"
"strconv"
2011-02-27 18:44:24 -08:00
"strings"
"sync"
2015-02-06 14:16:32 -08:00
"time"
"golang.org/x/crypto/pbkdf2"
2024-01-13 05:05:35 -08:00
"golang.org/x/net/proxy"
2011-02-27 18:44:24 -08:00
)
const (
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"
2024-04-10 06:13:20 -07:00
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"
2011-02-27 18:44:24 -08:00
)
2014-12-13 06:28:57 -08:00
// Default TLS configuration options
var DefaultConfig = &tls.Config{}
2011-02-27 18:44:24 -08:00
// DebugWriter is the writer used to write debugging output to.
var DebugWriter io.Writer = os.Stderr
2014-12-13 06:28:57 -08:00
// Cookie is a unique XMPP session identifier
type Cookie uint64
func getCookie() Cookie {
var buf [8]byte
if _, err := rand.Reader.Read(buf[:]); err != nil {
panic("Failed to read random bytes: " + err.Error())
}
return Cookie(binary.LittleEndian.Uint64(buf[:]))
}
2024-04-10 06:13:20 -07:00
// Fast holds the XEP-0484 fast token, mechanism and expiry date
type Fast struct {
Token string
Mechanism string
Expiry time.Time
}
2022-07-10 10:58:21 -07:00
// Client holds XMPP connection options
2011-02-27 18:44:24 -08:00
type Client struct {
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.
2011-02-27 18:44:24 -08:00
}
func (c *Client) JID() string {
return c.jid
}
2017-11-15 00:46:44 -08:00
func containsIgnoreCase(s, substr string) bool {
s, substr = strings.ToUpper(s), strings.ToUpper(substr)
return strings.Contains(s, substr)
}
func connect(host, user, passwd string, timeout time.Duration) (net.Conn, error) {
2011-02-27 18:44:24 -08:00
addr := host
if strings.TrimSpace(host) == "" {
2011-06-27 18:53:04 -07:00
a := strings.SplitN(user, "@", 2)
2011-02-27 18:44:24 -08:00
if len(a) == 2 {
2016-01-16 09:28:14 -08:00
addr = a[1]
2011-02-27 18:44:24 -08:00
}
}
2011-06-27 18:53:04 -07:00
a := strings.SplitN(host, ":", 2)
2011-02-27 18:44:24 -08:00
if len(a) == 1 {
2016-01-16 09:28:14 -08:00
addr += ":5222"
2011-02-27 18:44:24 -08:00
}
2017-11-15 00:46:44 -08:00
2024-01-13 05:05:35 -08:00
http_proxy := os.Getenv("HTTP_PROXY")
if http_proxy == "" {
http_proxy = os.Getenv("http_proxy")
2011-02-27 18:44:24 -08:00
}
2018-01-30 06:28:48 -08:00
// test for no proxy, takes a comma separated list with substrings to match
2024-01-13 05:05:35 -08:00
if http_proxy != "" {
noproxy := os.Getenv("NO_PROXY")
2017-11-15 04:00:48 -08:00
if noproxy == "" {
noproxy = os.Getenv("no_proxy")
2017-11-15 04:00:48 -08:00
}
2017-11-15 00:46:44 -08:00
if noproxy != "" {
nplist := strings.Split(noproxy, ",")
for _, s := range nplist {
if containsIgnoreCase(addr, s) {
2024-01-13 05:05:35 -08:00
http_proxy = ""
2017-11-15 00:46:44 -08:00
break
}
}
}
}
2024-01-13 05:05:35 -08:00
socks5Target, socks5 := strings.CutPrefix(http_proxy, "socks5://")
if http_proxy != "" && !socks5 {
url, err := url.Parse(http_proxy)
2011-02-27 18:44:24 -08:00
if err == nil {
addr = url.Host
}
}
2024-01-13 05:05:35 -08:00
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
}
2011-02-27 18:44:24 -08:00
}
2024-01-13 05:05:35 -08:00
if http_proxy != "" && !socks5 {
2011-02-27 18:44:24 -08:00
fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host)
fmt.Fprintf(c, "Host: %s\r\n", host)
fmt.Fprintf(c, "\r\n")
br := bufio.NewReader(c)
2011-05-17 17:31:47 -07:00
req, _ := http.NewRequest("CONNECT", host, nil)
resp, err := http.ReadResponse(br, req)
2011-02-27 18:44:24 -08:00
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
2011-06-27 18:53:04 -07:00
f := strings.SplitN(resp.Status, " ", 2)
2011-11-04 06:40:10 -07:00
return nil, errors.New(f[1])
2011-02-27 18:44:24 -08:00
}
}
2013-05-14 19:24:35 -07:00
return c, nil
}
// Options are used to specify additional options for new clients, such as a Resource.
type Options struct {
// Host specifies what host to connect to, as either "hostname" or "hostname:port"
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
Host string
// User specifies what user to authenticate to the remote server.
User string
// Password supplies the password to use for authentication with the remote server.
Password string
// DialTimeout is the time limit for establishing a connection. A
// DialTimeout of zero means no timeout.
DialTimeout time.Duration
// Resource specifies an XMPP client resource, like "bot", instead of accepting one
// from the server. Use "" to let the server generate one for your client.
Resource string
// OAuthScope provides go-xmpp the required scope for OAuth2 authentication.
OAuthScope string
// OAuthToken provides go-xmpp with the required OAuth2 token used to authenticate
OAuthToken string
// OAuthXmlNs provides go-xmpp with the required namespaced used for OAuth2 authentication. This is
// provided to the server as the xmlns:auth attribute of the OAuth2 authentication request.
OAuthXmlNs string
2014-09-14 23:21:06 -07:00
// TLS Config
TLSConfig *tls.Config
// InsecureAllowUnencryptedAuth permits authentication over a TCP connection that has not been promoted to
// TLS by STARTTLS; this could leak authentication information over the network, or permit man in the middle
// attacks.
InsecureAllowUnencryptedAuth bool
// NoTLS directs go-xmpp to not use TLS initially to contact the server; instead, a plain old unencrypted
// TCP connection should be used. (Can be combined with StartTLS to support STARTTLS-based servers.)
NoTLS bool
2013-10-18 00:49:41 -07:00
// StartTLS directs go-xmpp to STARTTLS if the server supports it; go-xmpp will automatically STARTTLS
// if the server requires it regardless of this option.
StartTLS bool
2013-10-18 00:49:41 -07:00
// Debug output
Debug bool
2014-10-04 09:22:05 -07:00
// Use server sessions
Session bool
2014-10-04 09:22:05 -07:00
2014-10-04 09:29:19 -07:00
// Presence Status
2014-10-04 09:22:05 -07:00
Status string
2014-10-04 09:29:19 -07:00
// Status message
StatusMessage string
2023-11-11 04:08:17 -08:00
// Auth mechanism to use
Mechanism string
// XEP-0474: SASL SCRAM Downgrade Protection
SSDP bool
2024-04-10 06:13:20 -07:00
// XEP-0388: Extensible SASL Profile
// Value for software
UserAgentSW string
// XEP-0388: XEP-0388: Extensible SASL Profile
// Value for device
UserAgentDev string
2024-04-10 06:13:20 -07:00
// XEP-0388: Extensible SASL Profile
// Unique stable identifier for the client installation
// MUST be a valid UUIDv4
UserAgentID string
2024-04-10 06:13:20 -07:00
2024-05-11 06:05:41 -07:00
// Enable XEP-0484: Fast Authentication Streamlining Tokens
Fast bool
2024-04-10 06:13:20 -07:00
// 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.
func (o Options) NewClient() (*Client, error) {
host := o.Host
2019-01-08 23:35:32 -08:00
if strings.TrimSpace(host) == "" {
a := strings.SplitN(o.User, "@", 2)
if len(a) == 2 {
if _, addrs, err := net.LookupSRV("xmpp-client", "tcp", a[1]); err == nil {
if len(addrs) > 0 {
// default to first record
host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port)
defP := addrs[0].Priority
for _, adr := range addrs {
if adr.Priority < defP {
host = fmt.Sprintf("%s:%d", adr.Target, adr.Port)
defP = adr.Priority
}
}
} else {
host = a[1]
}
} else {
host = a[1]
}
}
}
c, err := connect(host, o.User, o.Password, o.DialTimeout)
2013-05-14 19:24:35 -07:00
if err != nil {
return nil, err
}
2019-01-08 23:35:32 -08:00
if strings.LastIndex(host, ":") > 0 {
host = host[:strings.LastIndex(host, ":")]
}
2011-02-27 18:44:24 -08:00
client := new(Client)
if o.NoTLS {
client.conn = c
} else {
2014-09-14 23:15:56 -07:00
var tlsconn *tls.Conn
2014-09-14 23:21:06 -07:00
if o.TLSConfig != nil {
tlsconn = tls.Client(c, o.TLSConfig)
host = o.TLSConfig.ServerName
2014-09-14 23:15:56 -07:00
} else {
newconfig := DefaultConfig.Clone()
2017-04-23 03:07:54 -07:00
newconfig.ServerName = host
tlsconn = tls.Client(c, newconfig)
2014-09-14 23:15:56 -07:00
}
if err = tlsconn.Handshake(); err != nil {
return nil, err
}
insecureSkipVerify := DefaultConfig.InsecureSkipVerify
if o.TLSConfig != nil {
insecureSkipVerify = o.TLSConfig.InsecureSkipVerify
}
if !insecureSkipVerify {
if err = tlsconn.VerifyHostname(host); err != nil {
return nil, err
}
}
client.conn = tlsconn
2011-02-27 18:44:24 -08:00
}
if err := client.init(&o); err != nil {
2013-05-14 19:24:35 -07:00
client.Close()
return nil, err
}
2013-05-14 19:24:35 -07:00
return client, nil
}
// NewClient creates a new connection to a host given as "hostname" or "hostname:port".
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
// Default the port to 5222.
2013-10-18 00:49:41 -07:00
func NewClient(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
2013-10-18 00:49:41 -07:00
Debug: debug,
Session: false,
2013-05-14 19:24:35 -07:00
}
return opts.NewClient()
}
2013-05-14 19:24:35 -07:00
2014-12-13 06:28:57 -08:00
// NewClientNoTLS creates a new client without TLS
2013-10-18 00:49:41 -07:00
func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
NoTLS: true,
2013-10-18 00:49:41 -07:00
Debug: debug,
Session: false,
2011-02-27 18:44:24 -08:00
}
return opts.NewClient()
2011-02-27 18:44:24 -08:00
}
2014-12-13 06:28:57 -08:00
// Close closes the XMPP connection
2011-11-04 06:40:10 -07:00
func (c *Client) Close() error {
c.shutdown = true
2014-12-11 02:41:59 -08:00
if c.conn != (*tls.Conn)(nil) {
fmt.Fprintf(c.stanzaWriter, "</stream:stream>\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()
}
}
2014-12-11 02:41:59 -08:00
}
2014-12-13 06:28:57 -08:00
return nil
2013-05-14 19:24:35 -07:00
}
func cnonce() string {
randSize := big.NewInt(0)
randSize.Lsh(big.NewInt(1), 64)
cn, err := rand.Int(rand.Reader, randSize)
if err != nil {
return ""
}
return fmt.Sprintf("%016x", cn)
2011-02-27 18:44:24 -08:00
}
func (c *Client) init(o *Options) error {
var domain string
var user string
a := strings.SplitN(o.User, "@", 2)
// Check if User is not empty. Otherwise, we'll be attempting ANONYMOUS with Host domain.
switch {
case len(o.User) > 0:
if len(a) != 2 {
return errors.New("xmpp: invalid username (want user@domain): " + o.User)
}
user = a[0]
2015-05-16 15:53:09 -07:00
domain = a[1]
case strings.Contains(o.Host, ":"):
domain = strings.SplitN(o.Host, ":", 2)[0]
default:
domain = o.Host
}
2011-02-27 18:44:24 -08:00
// Declare intent to be a jabber client and gather stream features.
f, err := c.startStream(o, domain)
2011-02-27 18:44:24 -08:00
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.
2014-12-13 06:28:57 -08:00
if f, err = c.startTLSIfRequired(f, o, domain); err != nil {
return err
2011-02-27 18:44:24 -08:00
}
2023-11-11 04:08:17 -08:00
var mechanism, channelBinding, clientFirstMessage, clientFinalMessageBare, authMessage string
2024-04-10 06:13:20 -07:00
var bind2Data, resource, userAgentSW, userAgentDev, userAgentID, fastAuth string
2023-11-11 04:08:17 -08:00
var serverSignature, keyingMaterial []byte
var scramPlus, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool
var cbsSlice, mechSlice []string
2023-11-11 04:08:17 -08:00
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 mechSlice {
if m == "ANONYMOUS" {
fmt.Fprintf(c.stanzaWriter, "<auth xmlns='%s' mechanism='ANONYMOUS' />\n", nsSASL)
foundAnonymous = true
break
}
2014-09-14 23:12:12 -07:00
}
if !foundAnonymous {
return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified")
2014-09-14 23:12:12 -07:00
}
} else {
// Even digest forms of authentication are unsafe if we do not know that the host
// we are talking to is the actual server, and not a man in the middle playing
// proxy.
if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth {
return errors.New("refusing to authenticate over unencrypted TCP connection")
2013-05-14 19:24:35 -07:00
}
2023-11-11 04:08:17 -08:00
tlsConn, ok = c.conn.(*tls.Conn)
if ok {
tlsConnOK = true
}
mechanism = ""
if o.Mechanism != "" {
if slices.Contains(mechSlice, o.Mechanism) {
mechanism = o.Mechanism
}
} 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"
}
2023-11-11 04:08:17 -08:00
}
if strings.HasPrefix(mechanism, "SCRAM-SHA") {
if strings.HasSuffix(mechanism, "PLUS") {
scramPlus = true
}
if scramPlus {
for _, cbs := range f.ChannelBindings.ChannelBinding {
cbsSlice = append(cbsSlice, cbs.Type)
}
2023-11-11 04:08:17 -08:00
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
}
2023-11-11 04:08:17 -08:00
}
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
}
2023-11-11 04:08:17 -08:00
default:
return errors.New(mechanism + ": unknown TLS version")
2013-05-14 19:24:35 -07:00
}
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()
}
2023-11-11 04:08:17 -08:00
if len(keyingMaterial) == 0 {
return errors.New(mechanism + ": no keying material")
}
switch {
case tls13 && !serverEndPoint:
2023-11-11 04:08:17 -08:00
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:
2023-11-11 04:08:17 -08:00
channelBinding = base64.StdEncoding.EncodeToString(append([]byte("p=tls-unique,,"), keyingMaterial[:]...))
}
2013-05-14 19:24:35 -07:00
}
var shaNewFn func() hash.Hash
switch mechanism {
2023-11-11 04:08:17 -08:00
case "SCRAM-SHA-512", "SCRAM-SHA-512-PLUS":
shaNewFn = sha512.New
2023-11-11 04:08:17 -08:00
case "SCRAM-SHA-256", "SCRAM-SHA-256-PLUS":
shaNewFn = sha256.New
2023-11-11 04:08:17 -08:00
case "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS":
shaNewFn = sha1.New
default:
return errors.New("unsupported auth mechanism")
}
clientNonce := cnonce()
2023-11-11 04:08:17 -08:00
if scramPlus {
switch {
case tls13 && !serverEndPoint:
2023-11-11 04:08:17 -08:00
clientFirstMessage = "p=tls-exporter,,n=" + user + ",r=" + clientNonce
case serverEndPoint:
clientFirstMessage = "p=tls-server-end-point,,n=" + user + ",r=" + clientNonce
default:
2023-11-11 04:08:17 -08:00
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("<bind xmlns='%s'><tag>%s</tag></bind>",
nsBind2, resource)
}
if o.UserAgentSW != "" {
userAgentSW = fmt.Sprintf("<software>%s</software>", o.UserAgentSW)
} else {
userAgentSW = "<software>go-xmpp</software>"
}
if o.UserAgentDev != "" {
userAgentDev = fmt.Sprintf("<device>%s</device>", o.UserAgentDev)
}
if o.UserAgentID != "" {
userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID)
}
2024-05-11 06:05:41 -07:00
if o.Fast && f.Authentication.Inline.Fast.Mechanism != nil && o.UserAgentID != "" && c.IsEncrypted() {
2024-04-10 06:13:20 -07:00
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("<request-token xmlns='%s' mechanism='%s'/>", nsFast, mech)
} else {
var fastInvalidate string
if o.FastInvalidate {
fastInvalidate = " invalidate='true'"
}
fastAuth = fmt.Sprintf("<fast xmlns='%s'%s/>", nsFast, fastInvalidate)
2024-04-10 06:13:20 -07:00
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)
}
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)
}
}
fmt.Fprintf(c.stanzaWriter,
2024-04-10 06:13:20 -07:00
"<authenticate xmlns='%s' mechanism='%s'><initial-response>%s</initial-response><user-agent%s>%s%s</user-agent>%s%s</authenticate>\n",
nsSASL2, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
} else {
fmt.Fprintf(c.stanzaWriter, "<auth xmlns='%s' mechanism='%s'>%s</auth>\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
}
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
}
return errors.New("auth failure: " + errorMessage)
2024-04-10 06:13:20 -07:00
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 {
2024-04-10 06:13:20 -07:00
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)
}
2024-04-12 03:56:37 -07:00
if o.Session {
// if server support session, open it
cookie := getCookie() // generate new id value for session
fmt.Fprintf(c.stanzaWriter, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>\n", xmlEscape(domain), cookie, nsSession)
}
// We're connected and can now receive and send messages.
fmt.Fprintf(c.stanzaWriter, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>\n", o.Status, o.StatusMessage)
2024-04-10 06:13:20 -07:00
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.")
}
}
2023-11-11 04:08:17 -08:00
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 {
2023-11-11 04:08:17 -08:00
case "SCRAM-SHA-512", "SCRAM-SHA-512-PLUS":
storedKey512 := sha512.Sum512(clientKey)
storedKey = storedKey512[:]
2023-11-11 04:08:17 -08:00
case "SCRAM-SHA-256", "SCRAM-SH-256-PLUS":
storedKey256 := sha256.Sum256(clientKey)
storedKey = storedKey256[:]
2023-11-11 04:08:17 -08:00
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
}
2023-11-11 04:08:17 -08:00
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, "<response xmlns='%s'>%s</response>\n", nsSASL2,
clientFinalMessage)
} else {
fmt.Fprintf(c.stanzaWriter, "<response xmlns='%s'>%s</response>\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, "<auth xmlns='%s' mechanism='X-OAUTH2' auth:service='oauth2' "+
"xmlns:auth='%s'>%s</auth>\n", nsSASL2, o.OAuthXmlNs, enc)
} else {
fmt.Fprintf(c.stanzaWriter, "<auth xmlns='%s' mechanism='X-OAUTH2' auth:service='oauth2' "+
"xmlns:auth='%s'>%s</auth>\n", nsSASL, o.OAuthXmlNs, enc)
}
}
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, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL2, enc)
} else {
fmt.Fprintf(c.conn, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>\n", nsSASL, enc)
}
}
2011-02-27 18:44:24 -08:00
}
if mechanism == "" {
return fmt.Errorf("no viable authentication method available: %v", f.Mechanisms.Mechanism)
}
2011-02-27 18:44:24 -08:00
// Next message should be either success or failure.
name, val, err := c.next()
2013-11-03 23:13:55 -08:00
if err != nil {
return err
}
2011-02-27 18:44:24 -08:00
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
}
2024-04-10 06:13:20 -07:00
if v.Token.Token != "" {
2024-04-12 03:56:37 -07:00
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)
2024-04-10 06:13:20 -07:00
}
2011-02-27 18:44:24 -08:00
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")
}
2023-11-11 04:08:17 -08:00
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)
2011-02-27 18:44:24 -08:00
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
}
return errors.New("auth failure: " + errorMessage)
2011-02-27 18:44:24 -08:00
default:
2011-11-04 06:40:10 -07:00
return errors.New("expected <success> or <failure>, got <" + name.Local + "> in " + name.Space)
2011-02-27 18:44:24 -08:00
}
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
}
2011-02-27 18:44:24 -08:00
}
// 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
}
}
2011-02-27 18:44:24 -08:00
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, "<iq type='set' id='%x'><bind xmlns='%s'></bind></iq>\n", cookie, nsBind)
2024-03-10 05:44:33 -07:00
} else {
fmt.Fprintf(c.stanzaWriter, "<iq type='set' id='%x'><bind xmlns='%s'><resource>%s</resource></bind></iq>\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")
}
2024-03-10 05:44:33 -07:00
}
2011-02-27 18:44:24 -08:00
}
if o.Session {
2023-11-11 04:08:17 -08:00
// if server support session, open it
cookie := getCookie() // generate new id value for session
fmt.Fprintf(c.stanzaWriter, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>\n", xmlEscape(domain), cookie, nsSession)
}
2011-02-27 18:44:24 -08:00
// We're connected and can now receive and send messages.
fmt.Fprintf(c.stanzaWriter, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>\n", o.Status, o.StatusMessage)
2014-10-04 09:29:19 -07:00
2011-02-27 18:44:24 -08:00
return nil
}
// startTlsIfRequired examines the server's stream features and, if STARTTLS is required or supported, performs the TLS handshake.
// f will be updated if the handshake completes, as the new stream's features are typically different from the original.
2014-12-13 06:28:57 -08:00
func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string) (*streamFeatures, error) {
// whether we start tls is a matter of opinion: the server's and the user's.
switch {
case f.StartTLS == nil:
// the server does not support STARTTLS
return f, nil
case !o.StartTLS && f.StartTLS.Required == nil:
return f, nil
case f.StartTLS.Required != nil:
// the server requires STARTTLS.
case !o.StartTLS:
// the user wants STARTTLS and the server supports it.
}
var err error
fmt.Fprintf(c.stanzaWriter, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n")
var k tlsProceed
if err = c.p.DecodeElement(&k, nil); err != nil {
return f, errors.New("unmarshal <proceed>: " + err.Error())
}
tc := o.TLSConfig
if tc == nil {
tc = DefaultConfig.Clone()
2023-11-11 04:08:17 -08:00
// TODO(scott): we should consider using the server's address or reverse lookup
tc.ServerName = domain
}
t := tls.Client(c.conn, tc)
if err = t.Handshake(); err != nil {
return f, errors.New("starttls handshake: " + err.Error())
}
c.conn = t
// restart our declaration of XMPP stream intentions.
tf, err := c.startStream(o, domain)
if err != nil {
return f, err
}
return tf, nil
}
// startStream will start a new XML decoder for the connection, signal the start of a stream to the server and verify that the server has
2014-12-13 06:28:57 -08:00
// also started the stream; if o.Debug is true, startStream will tee decoded XML data to stderr. The features advertised by the server
// will be returned.
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)
2014-12-13 06:28:57 -08:00
} else {
c.p = xml.NewDecoder(c.conn)
c.stanzaWriter = c.conn
}
if c.IsEncrypted() {
_, err := fmt.Fprintf(c.stanzaWriter, "<?xml version='1.0'?>"+
"<stream:stream from='%s' to='%s' xmlns='%s'"+
" xmlns:stream='%s' version='1.0'>\n",
xmlEscape(o.User), xmlEscape(domain), nsClient, nsStream)
if err != nil {
return nil, err
}
} else {
_, err := fmt.Fprintf(c.stanzaWriter, "<?xml version='1.0'?>"+
"<stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>\n",
xmlEscape(domain), nsClient, nsStream)
if err != nil {
return nil, err
}
}
// We expect the server to start a <stream>.
se, err := c.nextStart()
if err != nil {
return nil, err
}
if se.Name.Space != nsStream || se.Name.Local != "stream" {
return nil, fmt.Errorf("expected <stream> but got <%v> in %v", se.Name.Local, se.Name.Space)
}
// Now we're in the stream and can use Unmarshal.
// Next message should be <features> to tell us authentication options.
// See section 4.6 in RFC 3920.
f := new(streamFeatures)
if err = c.p.DecodeElement(f, nil); err != nil {
return f, errors.New("unmarshal <features>: " + err.Error())
}
return f, nil
}
2014-12-29 16:53:23 -08:00
// IsEncrypted will return true if the client is connected using a TLS transport, either because it used.
2014-12-28 16:16:23 -08:00
// TLS to connect from the outset, or because it successfully used STARTTLS to promote a TCP connection to TLS.
func (c *Client) IsEncrypted() bool {
_, ok := c.conn.(*tls.Conn)
return ok
}
2014-12-28 16:16:23 -08:00
// Chat is an incoming or outgoing XMPP chat message.
2011-02-27 18:44:24 -08:00
type Chat struct {
Remote string
Type string
Text string
2017-06-13 03:51:37 -07:00
Subject string
Thread string
Ooburl string
Oobdesc string
Lang string
ID string
ReplaceID string
ReplyID string
StanzaID string
Roster Roster
Other []string
OtherElem []XMLElement
Stamp time.Time
2011-02-27 18:44:24 -08:00
}
2015-04-16 04:35:08 -07:00
type Roster []Contact
type Contact struct {
Remote string
Name string
Group []string
}
2014-12-28 16:16:23 -08:00
// Presence is an XMPP presence notification.
2013-01-18 16:48:50 -08:00
type Presence struct {
2015-10-16 07:24:54 -07:00
From string
To string
Type string
Show string
Status string
2013-01-18 16:48:50 -08:00
}
type IQ struct {
ID string
From string
To string
Type string
Query []byte
}
2014-12-28 16:16:23 -08:00
// Recv waits to receive the next XMPP stanza.
func (c *Client) Recv() (stanza interface{}, err error) {
2011-02-27 18:44:24 -08:00
for {
_, val, err := c.next()
2011-02-27 18:44:24 -08:00
if err != nil {
return Chat{}, err
}
2013-01-18 16:48:50 -08:00
switch v := val.(type) {
2024-04-07 02:25:16 -07:00
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)
2013-01-18 16:48:50 -08:00
case *clientMessage:
if v.Event.XMLNS == XMPPNS_PUBSUB_EVENT {
// Handle Pubsub notifications
switch v.Event.Items.Node {
case XMPPNS_AVATAR_PEP_METADATA:
if len(v.Event.Items.Items) == 0 {
return AvatarMetadata{}, errors.New("No avatar metadata items available")
}
return handleAvatarMetadata(v.Event.Items.Items[0].Body,
v.From)
// I am not sure whether this can even happen.
// XEP-0084 only specifies a subscription to
// the metadata node.
/*case XMPPNS_AVATAR_PEP_DATA:
return handleAvatarData(v.Event.Items.Items[0].Body,
v.From,
v.Event.Items.Items[0].ID)*/
default:
return pubsubClientToReturn(v.Event), nil
}
}
2015-02-06 14:16:32 -08:00
stamp, _ := time.Parse(
"2006-01-02T15:04:05Z",
v.Delay.Stamp,
)
chat := Chat{
Remote: v.From,
Type: v.Type,
Text: v.Body,
2017-06-13 03:51:37 -07:00
Subject: v.Subject,
Thread: v.Thread,
ID: v.ID,
ReplaceID: v.ReplaceID.ID,
ReplyID: v.ReplyID.ID,
StanzaID: v.StanzaID.ID,
Other: v.OtherStrings(),
OtherElem: v.Other,
Stamp: stamp,
Lang: v.Lang,
2015-02-06 14:16:32 -08:00
}
return chat, nil
2015-04-16 04:35:08 -07:00
case *clientQuery:
var r Roster
for _, item := range v.Item {
r = append(r, Contact{item.Jid, item.Name, item.Group})
}
return Chat{Type: "roster", Roster: r}, nil
2013-01-18 16:48:50 -08:00
case *clientPresence:
2015-10-16 04:01:30 -07:00
return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil
case *clientIQ:
switch {
case v.Query.XMLName.Space == "urn:xmpp:ping":
// TODO check more strictly
err := c.SendResultPing(v.ID, v.From)
if err != nil {
return Chat{}, err
2016-09-08 08:56:40 -07:00
}
fallthrough
case v.Type == "error":
switch v.ID {
case "sub1":
// Pubsub subscription failed
var errs []clientPubsubError
err := xml.Unmarshal([]byte(v.Error.InnerXML), &errs)
if err != nil {
return PubsubSubscription{}, err
}
var errsStr []string
for _, e := range errs {
errsStr = append(errsStr, e.XMLName.Local)
}
return PubsubSubscription{
Errors: errsStr,
}, nil
default:
res, err := xml.Marshal(v.Query)
if err != nil {
return Chat{}, err
}
2023-11-11 04:08:17 -08:00
return IQ{
ID: v.ID, From: v.From, To: v.To, Type: v.Type,
Query: res,
}, nil
}
case v.Type == "result":
switch v.ID {
case "sub1":
if v.Query.XMLName.Local == "pubsub" {
// Subscription or unsubscription was successful
var sub clientPubsubSubscription
err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub)
if err != nil {
return PubsubSubscription{}, err
}
return PubsubSubscription{
SubID: sub.SubID,
JID: sub.JID,
Node: sub.Node,
Errors: nil,
}, nil
}
case "unsub1":
if v.Query.XMLName.Local == "pubsub" {
var sub clientPubsubSubscription
err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub)
if err != nil {
return PubsubUnsubscription{}, err
}
return PubsubUnsubscription{
SubID: sub.SubID,
JID: v.From,
Node: sub.Node,
Errors: nil,
}, nil
} else {
// Unsubscribing MAY contain a pubsub element. But it does
// not have to
return PubsubUnsubscription{
SubID: "",
JID: v.From,
Node: "",
Errors: nil,
}, nil
}
case "info1":
if v.Query.XMLName.Space == XMPPNS_DISCO_ITEMS {
var itemsQuery clientDiscoItemsQuery
err := xml.Unmarshal(v.InnerXML, &itemsQuery)
if err != nil {
return []DiscoItem{}, err
}
return DiscoItems{
Jid: v.From,
Items: clientDiscoItemsToReturn(itemsQuery.Items),
}, nil
}
case "info3":
if v.Query.XMLName.Space == XMPPNS_DISCO_INFO {
var disco clientDiscoQuery
err := xml.Unmarshal(v.InnerXML, &disco)
if err != nil {
return DiscoResult{}, err
}
return DiscoResult{
Features: clientFeaturesToReturn(disco.Features),
Identities: clientIdentitiesToReturn(disco.Identities),
}, nil
}
case "items1", "items3":
if v.Query.XMLName.Local == "pubsub" {
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:
if len(p.Items) == 0 {
return AvatarData{}, errors.New("No avatar data items available")
}
return handleAvatarData(p.Items[0].Body,
v.From,
p.Items[0].ID)
case XMPPNS_AVATAR_PEP_METADATA:
if len(p.Items) == 0 {
return AvatarMetadata{}, errors.New("No avatar metadata items available")
}
return handleAvatarMetadata(p.Items[0].Body,
v.From)
default:
return PubsubItems{
p.Node,
pubsubItemsToReturn(p.Items),
}, nil
}
}
// Note: XEP-0084 states that metadata and data
// should be fetched with an id of retrieve1.
// Since we already have PubSub implemented, we
// can just use items1 and items3 to do the same
// as an Avatar node is just a PEP (PubSub) node.
/*case "retrieve1":
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
}*/
default:
res, err := xml.Marshal(v.Query)
if err != nil {
return Chat{}, err
}
2023-11-11 04:08:17 -08:00
return IQ{
ID: v.ID, From: v.From, To: v.To, Type: v.Type,
Query: res,
}, nil
}
case v.Query.XMLName.Local == "":
2019-01-14 18:53:08 -08:00
return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type}, nil
default:
res, err := xml.Marshal(v.Query)
if err != nil {
return Chat{}, err
}
2023-11-11 04:08:17 -08:00
return IQ{
ID: v.ID, From: v.From, To: v.To, Type: v.Type,
Query: res,
}, nil
2019-01-14 18:53:08 -08:00
}
2011-02-27 18:44:24 -08:00
}
}
}
2014-12-28 16:16:23 -08:00
// Send sends the message wrapped inside an XMPP message stanza body.
2014-11-15 10:43:43 -08:00
func (c *Client) Send(chat Chat) (n int, err error) {
var subtext, thdtext, oobtext, msgidtext, msgcorrecttext, replytext string
2017-06-13 03:51:37 -07:00
if chat.Subject != `` {
subtext = `<subject>` + xmlEscape(chat.Subject) + `</subject>`
}
if chat.Thread != `` {
thdtext = `<thread>` + xmlEscape(chat.Thread) + `</thread>`
}
if chat.Ooburl != `` {
oobtext = `<x xmlns="jabber:x:oob"><url>` + xmlEscape(chat.Ooburl) + `</url>`
if chat.Oobdesc != `` {
oobtext += `<desc>` + xmlEscape(chat.Oobdesc) + `</desc>`
}
oobtext += `</x>`
}
if chat.ID != `` {
msgidtext = `id='` + xmlEscape(chat.ID) + `'`
} else {
msgidtext = `id='` + cnonce() + `'`
}
if chat.ReplaceID != `` {
msgcorrecttext = `<replace id='` + xmlEscape(chat.ReplaceID) + `' xmlns='urn:xmpp:message-correct:0'/>`
}
if chat.ReplyID != `` {
replytext = `<reply to='`+ xmlEscape(chat.Remote) + `' id='` + xmlEscape(chat.ReplyID) + `' xmlns='urn:xmpp:reply:0'/>`
}
chat.Text = validUTF8(chat.Text)
stanza := fmt.Sprintf("<message to='%s' type='%s' "+msgidtext+" xml:lang='en'>"+subtext+"<body>%s</body>"+msgcorrecttext+replytext+oobtext+thdtext+"</message>",
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)
2011-02-27 18:44:24 -08:00
}
// SendOOB sends OOB data wrapped inside an XMPP message stanza, without actual body.
func (c *Client) SendOOB(chat Chat) (n int, err error) {
var thdtext, oobtext string
if chat.Thread != `` {
thdtext = `<thread>` + xmlEscape(chat.Thread) + `</thread>`
}
if chat.Ooburl != `` {
oobtext = `<x xmlns="jabber:x:oob"><url>` + xmlEscape(chat.Ooburl) + `</url>`
if chat.Oobdesc != `` {
oobtext += `<desc>` + xmlEscape(chat.Oobdesc) + `</desc>`
}
oobtext += `</x>`
}
stanza := fmt.Sprintf("<message to='%s' type='%s' id='%s' xml:lang='en'>"+oobtext+thdtext+"</message>\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)
}
2014-12-13 06:28:57 -08:00
// SendOrg sends the original text without being wrapped in an XMPP message stanza.
2014-11-15 10:43:43 -08:00
func (c *Client) SendOrg(org string) (n int, err error) {
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)
2013-10-18 00:52:01 -07:00
}
// SendPresence sends Presence wrapped inside XMPP presence stanza.
func (c *Client) SendPresence(presence Presence) (n int, err error) {
// Forge opening presence tag
var buf string = "<presence"
if presence.From != "" {
buf = buf + fmt.Sprintf(" from='%s'", xmlEscape(presence.From))
}
if presence.To != "" {
buf = buf + fmt.Sprintf(" to='%s'", xmlEscape(presence.To))
}
if presence.Type != "" {
// https://www.ietf.org/rfc/rfc3921.txt, 2.2.1, types can only be
// unavailable, subscribe, subscribed, unsubscribe, unsubscribed, probe, error
switch presence.Type {
case "unavailable", "subscribe", "subscribed", "unsubscribe", "unsubscribed", "probe", "error":
buf = buf + fmt.Sprintf(" type='%s'", xmlEscape(presence.Type))
}
}
buf = buf + ">"
// 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("<show>%s</show>", xmlEscape(presence.Show))
}
}
if presence.Status != "" {
buf = buf + fmt.Sprintf("<status>%s</status>", xmlEscape(presence.Status))
}
2024-04-05 02:57:09 -07:00
stanza := fmt.Sprintf(buf + "</presence>\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.
func (c *Client) SendKeepAlive() (n int, err error) {
2017-04-23 03:07:54 -07:00
return fmt.Fprintf(c.conn, " ")
}
2015-01-10 18:43:24 -08:00
// SendHtml sends the message as HTML as defined by XEP-0071
func (c *Client) SendHtml(chat Chat) (n int, err error) {
stanza := fmt.Sprintf("<message to='%s' type='%s' xml:lang='en'><body>%s</body>"+
"<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'>%s</body></html></message>\n",
2015-01-10 18:43:24 -08:00
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)
2015-01-10 18:43:24 -08:00
}
2015-04-16 04:30:36 -07:00
// Roster asks for the chat roster.
func (c *Client) Roster() error {
fmt.Fprintf(c.stanzaWriter, "<iq from='%s' type='get' id='roster1'><query xmlns='jabber:iq:roster'/></iq>\n", xmlEscape(c.jid))
2015-04-16 04:30:36 -07:00
return nil
}
2011-02-27 18:44:24 -08:00
// RFC 3920 C.1 Streams name space
type streamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
Authentication sasl2Authentication
StartTLS *tlsStartTLS
Mechanisms saslMechanisms
ChannelBindings saslChannelBindings
Bind bindBind
Session bool
Limits streamLimits
2011-02-27 18:44:24 -08:00
}
type streamError struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
2011-02-27 18:44:24 -08:00
Any xml.Name
2024-03-10 05:44:33 -07:00
Text struct {
Text string `xml:",chardata"`
Lang string `xml:"lang,attr"`
Xmlns string `xml:"xmlns,attr"`
} `xml:"text"`
2011-02-27 18:44:24 -08:00
}
// RFC 3920 C.3 TLS name space
type tlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required *string `xml:"required"`
2011-02-27 18:44:24 -08:00
}
type tlsProceed struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
2011-02-27 18:44:24 -08:00
}
type tlsFailure struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
2011-02-27 18:44:24 -08:00
}
type sasl2Authentication struct {
XMLName xml.Name `xml:"urn:xmpp:sasl:2 authentication"`
Mechanism []string `xml:"mechanism"`
Inline struct {
Text string `xml:",chardata"`
Bind struct {
2024-04-10 06:13:20 -07:00
XMLName xml.Name `xml:"urn:xmpp:bind:0 bind"`
Xmlns string `xml:"xmlns,attr"`
Text string `xml:",chardata"`
} `xml:"bind"`
2024-04-10 06:13:20 -07:00
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"`
}
2011-02-27 18:44:24 -08:00
// RFC 3920 C.4 SASL name space
type saslMechanisms struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
2012-04-03 09:46:01 -07:00
Mechanism []string `xml:"mechanism"`
2011-02-27 18:44:24 -08:00
}
type saslChannelBindings struct {
2023-11-11 04:08:17 -08:00
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"`
}
2011-02-27 18:44:24 -08:00
type saslAbort struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"`
2011-02-27 18:44:24 -08:00
}
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"`
2024-04-10 06:13:20 -07:00
Xmlns string `xml:"urn:xmpp:bind:0,attr"`
} `xml:"bound"`
2024-04-10 06:13:20 -07:00
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"`
}
2011-02-27 18:44:24 -08:00
type saslSuccess struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
Text string `xml:",chardata"`
2011-02-27 18:44:24 -08:00
}
type sasl2Failure struct {
XMLName xml.Name `xml:"urn:xmpp:sasl:2 failure"`
Any xml.Name `xml:",any"`
Text string `xml:"text"`
}
2011-02-27 18:44:24 -08:00
type saslFailure struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name `xml:",any"`
Text string `xml:"text"`
2011-02-27 18:44:24 -08:00
}
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"`
}
2011-02-27 18:44:24 -08:00
// RFC 3920 C.5 Resource binding name space
type bindBind struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
2011-02-27 18:44:24 -08:00
Resource string
2018-05-05 04:33:05 -07:00
Jid string `xml:"jid"`
2011-02-27 18:44:24 -08:00
}
type clientMessageCorrect struct {
XMLName xml.Name `xml:"urn:xmpp:message-correct:0 replace"`
ID string `xml:"id,attr"`
}
type stanzaID struct {
XMLName xml.Name `xml:"urn:xmpp:sid:0 stanza-id"`
ID string `xml:"id,attr"`
By string `xml:"by,attr"`
}
type clientReply struct {
XMLName xml.Name `xml:"urn:xmpp:reply:0 reply"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
}
2011-02-27 18:44:24 -08:00
// RFC 3921 B.1 jabber:client
type clientMessage struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"jabber:client message"`
From string `xml:"from,attr"`
2014-12-13 06:28:57 -08:00
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"`
2011-02-27 18:44:24 -08:00
2014-12-29 16:53:23 -08:00
// These should technically be []clientText, but string is much more convenient.
Subject string `xml:"subject"`
Body string `xml:"body"`
Thread string `xml:"thread"`
ReplaceID clientMessageCorrect
StanzaID stanzaID
ReplyID clientReply
// Pubsub
Event clientPubsubEvent `xml:"event"`
// Any hasn't matched element
Other []XMLElement `xml:",any"`
2015-02-06 14:16:32 -08:00
Delay Delay `xml:"delay"`
}
func (m *clientMessage) OtherStrings() []string {
a := make([]string, len(m.Other))
for i, e := range m.Other {
a[i] = e.String()
}
return a
}
type XMLElement struct {
XMLName xml.Name
Attr []xml.Attr `xml:",any,attr"` // Save the attributes of the xml element
InnerXML string `xml:",innerxml"`
}
func (e *XMLElement) String() string {
r := bytes.NewReader([]byte(e.InnerXML))
d := xml.NewDecoder(r)
var buf bytes.Buffer
for {
tok, err := d.Token()
if err != nil {
break
}
switch v := tok.(type) {
case xml.StartElement:
err = d.Skip()
case xml.CharData:
_, err = buf.Write(v)
}
if err != nil {
break
}
}
return buf.String()
}
2015-02-06 14:16:32 -08:00
type Delay struct {
Stamp string `xml:"stamp,attr"`
2011-02-27 18:44:24 -08:00
}
type clientPresence struct {
2011-09-28 12:26:19 -07:00
XMLName xml.Name `xml:"jabber:client presence"`
2012-06-16 19:47:48 -07:00
From string `xml:"from,attr"`
2014-12-13 06:28:57 -08:00
ID string `xml:"id,attr"`
2012-06-16 19:47:48 -07:00
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
Lang string `xml:"lang,attr"`
2011-02-27 18:44:24 -08:00
2015-10-16 07:24:54 -07:00
Show string `xml:"show"` // away, chat, dnd, xa
2015-10-16 04:01:30 -07:00
Status string `xml:"status"` // sb []clientText
2013-01-18 16:48:50 -08:00
Priority string `xml:"priority,attr"`
2011-02-27 18:44:24 -08:00
Error *clientError
}
type clientIQ struct {
// info/query
XMLName xml.Name `xml:"jabber:client iq"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, get, result, set
Query XMLElement `xml:",any"`
Error clientError
Bind bindBind
InnerXML []byte `xml:",innerxml"`
2011-02-27 18:44:24 -08:00
}
type clientError struct {
XMLName xml.Name `xml:"jabber:client error"`
Code string `xml:",attr"`
Type string `xml:"type,attr"`
Any xml.Name
InnerXML []byte `xml:",innerxml"`
Text string
2011-02-27 18:44:24 -08:00
}
2015-04-16 04:35:08 -07:00
type clientQuery struct {
Item []rosterItem
}
type rosterItem struct {
XMLName xml.Name `xml:"jabber:iq:roster item"`
Jid string `xml:",attr"`
Name string `xml:",attr"`
Subscription string `xml:",attr"`
Group []string
}
2011-02-27 18:44:24 -08:00
// Scan XML token stream to find next StartElement.
func (c *Client) nextStart() (xml.StartElement, error) {
2011-02-27 18:44:24 -08:00
for {
// 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()
2013-11-04 18:03:26 -08:00
return xml.StartElement{}, err
2011-02-27 18:44:24 -08:00
}
t := xml.CopyToken(to)
2011-02-27 18:44:24 -08:00
switch t := t.(type) {
case xml.StartElement:
c.nextMutex.Unlock()
2011-02-27 18:44:24 -08:00
return t, nil
}
c.nextMutex.Unlock()
2011-02-27 18:44:24 -08:00
}
}
// 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:
2024-04-02 04:39:45 -07:00
// 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()
}
}
2011-02-27 18:44:24 -08:00
// 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 (c *Client) next() (xml.Name, interface{}, error) {
2011-02-27 18:44:24 -08:00
// Read start element to find out what type we want.
se, err := c.nextStart()
2011-02-27 18:44:24 -08:00
if err != nil {
return xml.Name{}, nil, err
}
// Put it in an interface and allocate one.
var nv interface{}
2011-11-04 06:40:10 -07:00
switch se.Name.Space + " " + se.Name.Local {
case nsStream + " features":
nv = &streamFeatures{}
case nsStream + " error":
nv = &streamError{}
case nsTLS + " starttls":
nv = &tlsStartTLS{}
case nsTLS + " proceed":
nv = &tlsProceed{}
case nsTLS + " failure":
nv = &tlsFailure{}
case nsSASL + " mechanisms":
nv = &saslMechanisms{}
case nsSASL2 + " challenge":
nv = &sasl2Challenge{}
2011-11-04 06:40:10 -07:00
case nsSASL + " challenge":
nv = &saslChallenge{}
2011-11-04 06:40:10 -07:00
case nsSASL + " response":
nv = ""
case nsSASL + " abort":
nv = &saslAbort{}
case nsSASL2 + " success":
nv = &sasl2Success{}
2011-11-04 06:40:10 -07:00
case nsSASL + " success":
nv = &saslSuccess{}
case nsSASL2 + " failure":
nv = &sasl2Failure{}
2011-11-04 06:40:10 -07:00
case nsSASL + " failure":
nv = &saslFailure{}
case nsSASLCB + " sasl-channel-binding":
nv = &saslChannelBindings{}
2011-11-04 06:40:10 -07:00
case nsBind + " bind":
nv = &bindBind{}
case nsClient + " message":
nv = &clientMessage{}
case nsClient + " presence":
nv = &clientPresence{}
case nsClient + " iq":
nv = &clientIQ{}
case nsClient + " error":
nv = &clientError{}
default:
2011-11-04 06:40:10 -07:00
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
2011-02-27 18:44:24 -08:00
se.Name.Space + " <" + se.Name.Local + "/>")
}
// Unmarshal into that storage.
2024-04-02 04:39:45 -07:00
c.nextMutex.Lock()
if err = c.p.DecodeElement(nv, &se); err != nil {
2011-02-27 18:44:24 -08:00
return xml.Name{}, nil, err
}
2024-04-02 04:39:45 -07:00
c.nextMutex.Unlock()
return se.Name, nv, err
2011-02-27 18:44:24 -08:00
}
func xmlEscape(s string) string {
var b bytes.Buffer
2017-11-11 09:56:39 -08:00
xml.Escape(&b, []byte(s))
2011-02-27 18:44:24 -08:00
return b.String()
}
type tee struct {
r io.Reader
w io.Writer
}
2011-11-04 06:40:10 -07:00
func (t tee) Read(p []byte) (n int, err error) {
2011-02-27 18:44:24 -08:00
n, err = t.r.Read(p)
if n > 0 {
t.w.Write(p[0:n])
2013-10-18 00:49:41 -07:00
t.w.Write([]byte("\n"))
2011-02-27 18:44:24 -08:00
}
return
}
func validUTF8(s string) string {
// Remove invalid code points.
s = strings.ToValidUTF8(s, "<22>")
reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`)
s = reg.ReplaceAllString(s, "<22>")
return s
}