pax_global_header 0000666 0000000 0000000 00000000064 15116612257 0014520 g ustar 00root root 0000000 0000000 52 comment=1931e729fff5e7f0915ef44b5ade0867dff235fc
go-xmpp-0.3.1/ 0000775 0000000 0000000 00000000000 15116612257 0013110 5 ustar 00root root 0000000 0000000 go-xmpp-0.3.1/.travis.yml 0000664 0000000 0000000 00000000057 15116612257 0015223 0 ustar 00root root 0000000 0000000 language: go
go:
- tip
script:
- go test
go-xmpp-0.3.1/LICENSE 0000664 0000000 0000000 00000002707 15116612257 0014123 0 ustar 00root root 0000000 0000000 Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
go-xmpp-0.3.1/README.md 0000664 0000000 0000000 00000000203 15116612257 0014362 0 ustar 00root root 0000000 0000000 go-xmpp
=======
go xmpp library (original was written by russ cox )
[Documentation](https://godoc.org/github.com/xmppo/go-xmpp)
go-xmpp-0.3.1/_example/ 0000775 0000000 0000000 00000000000 15116612257 0014702 5 ustar 00root root 0000000 0000000 go-xmpp-0.3.1/_example/example-gui.go 0000664 0000000 0000000 00000004504 15116612257 0017451 0 ustar 00root root 0000000 0000000 package main
import (
"crypto/tls"
"log"
"os"
"strings"
"github.com/mattn/go-gtk/gtk"
"github.com/xmppo/go-xmpp"
)
func main() {
gtk.Init(&os.Args)
window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
window.SetTitle("GoTalk")
window.Connect("destroy", func() {
gtk.MainQuit()
})
vbox := gtk.NewVBox(false, 1)
scrolledwin := gtk.NewScrolledWindow(nil, nil)
textview := gtk.NewTextView()
textview.SetEditable(false)
textview.SetCursorVisible(false)
scrolledwin.Add(textview)
vbox.Add(scrolledwin)
buffer := textview.GetBuffer()
entry := gtk.NewEntry()
vbox.PackEnd(entry, false, false, 0)
window.Add(vbox)
window.SetSizeRequest(300, 400)
window.ShowAll()
dialog := gtk.NewDialog()
dialog.SetTitle(window.GetTitle())
sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
hbox := gtk.NewHBox(false, 1)
dialog.GetVBox().Add(hbox)
label := gtk.NewLabel("username:")
sgroup.AddWidget(label)
hbox.Add(label)
username := gtk.NewEntry()
hbox.Add(username)
hbox = gtk.NewHBox(false, 1)
dialog.GetVBox().Add(hbox)
label = gtk.NewLabel("password:")
sgroup.AddWidget(label)
hbox.Add(label)
password := gtk.NewEntry()
password.SetVisibility(false)
hbox.Add(password)
dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
dialog.SetDefaultResponse(gtk.RESPONSE_OK)
dialog.SetTransientFor(window)
dialog.ShowAll()
res := dialog.Run()
username_ := username.GetText()
password_ := password.GetText()
dialog.Destroy()
if res != gtk.RESPONSE_OK {
os.Exit(0)
}
xmpp.DefaultConfig = &tls.Config{
ServerName: "talk.google.com",
InsecureSkipVerify: false,
}
talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
if err != nil {
log.Fatal(err)
}
entry.Connect("activate", func() {
text := entry.GetText()
tokens := strings.SplitN(text, " ", 2)
if len(tokens) == 2 {
func() {
defer recover()
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
entry.SetText("")
}()
}
})
go func() {
for {
func() {
defer recover()
chat, err := talk.Recv()
if err != nil {
log.Fatal(err)
}
var iter gtk.TextIter
buffer.GetStartIter(&iter)
if msg, ok := chat.(xmpp.Chat); ok {
buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
}
}()
}
}()
gtk.Main()
}
go-xmpp-0.3.1/_example/example.go 0000664 0000000 0000000 00000004166 15116612257 0016673 0 ustar 00root root 0000000 0000000 package main
import (
"bufio"
"crypto/tls"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/xmppo/go-xmpp"
)
var (
server = flag.String("server", "talk.google.com:443", "server")
username = flag.String("username", "", "username")
password = flag.String("password", "", "password")
status = flag.String("status", "xa", "status")
statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
notls = flag.Bool("notls", false, "No TLS")
debug = flag.Bool("debug", false, "debug output")
session = flag.Bool("session", false, "use server session")
)
func serverName(host string) string {
return strings.Split(host, ":")[0]
}
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "usage: example [options]\n")
flag.PrintDefaults()
os.Exit(2)
}
flag.Parse()
if *username == "" || *password == "" {
if *debug && *username == "" && *password == "" {
fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n")
} else if *username != "" || *password != "" {
flag.Usage()
}
}
if !*notls {
xmpp.DefaultConfig = &tls.Config{
ServerName: serverName(*server),
InsecureSkipVerify: false,
}
}
var talk *xmpp.Client
var err error
options := xmpp.Options{
Host: *server,
User: *username,
Password: *password,
NoTLS: *notls,
Debug: *debug,
Session: *session,
Status: *status,
StatusMessage: *statusMessage,
}
talk, err = options.NewClient()
if err != nil {
log.Fatal(err)
}
go func() {
for {
chat, err := talk.Recv()
if err != nil {
log.Fatal(err)
}
switch v := chat.(type) {
case xmpp.Chat:
fmt.Println(v.Remote, v.Text)
case xmpp.Presence:
fmt.Println(v.From, v.Show)
}
}
}()
for {
in := bufio.NewReader(os.Stdin)
line, err := in.ReadString('\n')
if err != nil {
continue
}
line = strings.TrimRight(line, "\n")
tokens := strings.SplitN(line, " ", 2)
if len(tokens) == 2 {
talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
}
}
}
go-xmpp-0.3.1/const.go 0000664 0000000 0000000 00000004211 15116612257 0014563 0 ustar 00root root 0000000 0000000 package xmpp
const (
Version = "0.3.1"
// FAST Mechanisms
HT_SHA_256_ENDP = "HT-SHA-256-ENDP"
HT_SHA_256_EXPR = "HT-SHA-256-EXPR"
HT_SHA_256_NONE = "HT-SHA-256-NONE"
HT_SHA_256_UNIQ = "HT-SHA-256-UNIQ"
// IQ Types
IQTypeError = "error"
IQTypeGet = "get"
IQTypeResult = "result"
IQTypeSet = "set"
// SCRAM Mechanisms
SCRAM_SHA_1_PLUS = "SCRAM-SHA-1-PLUS"
SCRAM_SHA_1 = "SCRAM-SHA-1"
SCRAM_SHA_256_PLUS = "SCRAM-SHA-256-PLUS"
SCRAM_SHA_256 = "SCRAM-SHA-256"
SCRAM_SHA_512_PLUS = "SCRAM-SHA-512-PLUS"
SCRAM_SHA_512 = "SCRAM-SHA-512"
UPGR_SCRAM_SHA_256 = "UPGR-SCRAM-SHA-256"
UPGR_SCRAM_SHA_512 = "UPGR-SCRAM-SHA-512"
// XMPP Namespaces
XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data"
XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata"
XMPPNS_BIND_0 = "urn:xmpp:bind:0"
XMPPNS_CLIENT = "jabber:client"
XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
XMPPNS_FAST_0 = "urn:xmpp:fast:0"
XMPPNS_HTTP_UPLOAD_0 = "urn:xmpp:http:upload:0"
XMPPNS_IQ_VERSION = "jabber:iq:version"
XMPPNS_MUC = "http://jabber.org/protocol/muc"
XMPPNS_MUC_USER = "http://jabber.org/protocol/muc#user"
XMPPNS_PING = "urn:xmpp:ping"
XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event"
XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub"
XMPPNS_SASL_2 = "urn:xmpp:sasl:2"
XMPPNS_SASL_CB_0 = "urn:xmpp:sasl-cb:0"
XMPPNS_SASL_UPGRADE_0 = "urn:xmpp:sasl:upgrade:0"
XMPPNS_XMPP_SASL = "urn:ietf:params:xml:ns:xmpp-sasl"
XMPPNS_SCRAM_UPGRADE_0 = "urn:xmpp:scram-upgrade:0"
XMPPNS_SID_0 = "urn:xmpp:sid:0"
XMPPNS_STREAM = "http://etherx.jabber.org/streams"
XMPPNS_STREAM_LIMITS_0 = "urn:xmpp:stream-limits:0"
XMPPNS_TIME = "urn:xmpp:time"
XMPPNS_XMPP_TLS = "urn:ietf:params:xml:ns:xmpp-tls"
XMPPNS_XMPP_BIND = "urn:ietf:params:xml:ns:xmpp-bind"
XMPPNS_XMPP_SESSION = "urn:ietf:params:xml:ns:xmpp-session"
)
go-xmpp-0.3.1/go.mod 0000664 0000000 0000000 00000000205 15116612257 0014213 0 ustar 00root root 0000000 0000000 module github.com/xmppo/go-xmpp
go 1.24.0
toolchain go1.24.4
require (
github.com/google/uuid v1.6.0
golang.org/x/net v0.48.0
)
go-xmpp-0.3.1/go.sum 0000664 0000000 0000000 00000000474 15116612257 0014250 0 ustar 00root root 0000000 0000000 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
go-xmpp-0.3.1/xmpp.go 0000664 0000000 0000000 00000223203 15116612257 0014425 0 ustar 00root root 0000000 0000000 // Copyright 2011 The Go Authors. All rights reserved.
// 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"
"crypto/pbkdf2"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/xml"
"errors"
"fmt"
"hash"
"io"
"log"
"math/big"
"net"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/google/uuid"
"golang.org/x/net/proxy"
)
// Default TLS configuration options
var DefaultConfig = &tls.Config{} //nolint: gosec,G402 // In go1.25 TLS 1.2 used as default. Used by servers for older clients.
// DebugWriter is the writer used to write debugging output to.
type debugWriter struct {
w io.Writer
prefix string
}
func (d debugWriter) Write(p []byte) (int, error) {
nl := []byte("\n")
switch {
case len(p) == 0:
return 0, nil
case string(p) == "":
return len(p), nil
case string(p) == "\n":
return len(p), nil
}
data := append([]byte(d.prefix), p...)
if !strings.HasSuffix(string(p), "\n") {
data = append(data, nl...)
}
n, err := d.w.Write(data)
if err != nil {
return n, err
}
if n != len(data) {
return n, io.ErrShortWrite
}
return len(p), nil
}
// 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[:]))
}
func getUUID() string {
// Use github.com/google/uuid as XEP-0359 requires an UUID according to
// RFC 4122.
id, err := uuid.NewV7()
if err != nil {
log.Fatal(err)
}
return id.String()
}
// 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
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
subIDs []string // IDs of subscription stanzas
unsubIDs []string // IDs of unsubscription stanzas
itemsIDs []string // IDs of item requests
periodicPings bool // Send periodic server pings.
periodicPingTicker *time.Ticker // Ticker for periodic pings.
periodicPingPeriod time.Duration // Period for periodic ping ticker.
periodicPingTimeout time.Duration // Timeout for periodic pings.
periodicPingID string // ID of the current periodic ping request.
periodicPingReply bool // True if a reply for the current ping request was received.
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.
Options *Options // Connection Options, including reported software versions
}
func (c *Client) JID() string {
return c.jid
}
func containsIgnoreCase(s, substr string) bool {
s, substr = strings.ToUpper(s), strings.ToUpper(substr)
return strings.Contains(s, substr)
}
func connect(host string, user string, timeout time.Duration) (net.Conn, error) {
addr := host
if strings.TrimSpace(host) == "" {
a := strings.SplitN(user, "@", 2)
if len(a) == 2 {
addr = a[1]
}
}
a := strings.SplitN(host, ":", 2)
if len(a) == 1 {
addr += ":5222"
}
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 http_proxy != "" {
noproxy := os.Getenv("NO_PROXY")
if noproxy == "" {
noproxy = os.Getenv("no_proxy")
}
if noproxy != "" {
nplist := strings.Split(noproxy, ",")
for _, s := range nplist {
if containsIgnoreCase(addr, s) {
http_proxy = ""
break
}
}
}
}
socks5Target, socks5 := strings.CutPrefix(http_proxy, "socks5://")
if http_proxy != "" && !socks5 {
url, err := url.Parse(http_proxy)
if err == nil {
addr = url.Host
}
}
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 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")
br := bufio.NewReader(c)
req, _ := http.NewRequest("CONNECT", host, nil)
resp, err := http.ReadResponse(br, req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1])
}
}
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
// 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
// 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
// Debug output
Debug bool
// DebugWriter specifies where the debug output is written to
DebugWriter io.Writer
// Use server sessions
Session bool
// Presence Status
Status string
// 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
// NoPLAIN forbids authentication using plain passwords
NoPLAIN bool
// NoSASLUpgrade disables XEP-0480 upgrades.
NoSASLUpgrade bool
// Send periodic XEP-0199 pings to the server.
PeriodicServerPings bool
// Period of inactivity after which the client sends a XEP-0199 ping
// to the server. Specified in milliseconds, defaults to 20.000 (20 seconds).
PeriodicServerPingsPeriod int
// Timeout for ping replies. If no reply is received in this time period, the
// connection is considered broken and gets closed. Specified in milliseconds,
// defaults to 5.000 (5 seconds).
PeriodicServerPingsTimeout int
// ReportSoftwareVersion if set to true iq response will be generated
// according to xep-0092. If set to false all iq version queries will be
// silently ignored. By default set to false.
ReportSoftwareVersion bool
// SoftwareName is client software name (UserAgent in web browsers terms),
// reported in response to information query as described in xep-0092.
// By default it is "go-xmpp" (no quotes), and can be overridden here.
// Responses can be enbled via ReportSoftwareVersion.
SoftwareName string
// SoftwareVersion reported in response to iq version as described in
// xep-0092. If SoftwareName is not overridden in SoftwareName option go-xmpp
// version will be reported. Otherwise set as "undefined" if not overridden
// here.
SoftwareVersion string
// ReportSoftwareOS if set to true information about os go-xmpp being built
// for will be reported. It considered not safe (secure) enough in xep-0092
// for some unknown reasons, so by defult this option set to false.
ReportSoftwareOS bool
}
// NewClient establishes a new Client connection based on a set of Options.
func (o Options) NewClient() (*Client, error) {
host := o.Host
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.DialTimeout)
if err != nil {
return nil, err
}
if strings.LastIndex(host, ":") > 0 {
host = host[:strings.LastIndex(host, ":")]
}
client := new(Client)
client.Options = &o
if o.NoTLS {
client.conn = c
} else {
var tlsconn *tls.Conn
if o.TLSConfig != nil {
tlsconn = tls.Client(c, o.TLSConfig)
host = o.TLSConfig.ServerName
} else {
newconfig := DefaultConfig.Clone()
newconfig.ServerName = host
tlsconn = tls.Client(c, newconfig)
}
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
}
if err := client.init(&o); err != nil {
return nil, err
}
if o.PeriodicServerPings {
client.periodicPings = true
// Set periodic pings period to 20 seconds if not specified.
if o.PeriodicServerPingsPeriod == 0 {
client.periodicPingPeriod = time.Duration(20000 * time.Millisecond)
} else {
client.periodicPingPeriod = time.Duration(o.PeriodicServerPingsPeriod) * time.Millisecond
}
// Set periodic pings timeout to 5 seconds if not specified.
if o.PeriodicServerPingsTimeout == 0 {
client.periodicPingTimeout = time.Duration(5000 * time.Millisecond)
} else {
client.periodicPingTimeout = time.Duration(o.PeriodicServerPingsTimeout) * time.Millisecond
}
client.periodicPingTicker = time.NewTicker(client.periodicPingPeriod)
// Start sending periodic pings
go client.sendPeriodicPings()
}
if client.Options.SoftwareName == "" {
client.Options.SoftwareName = "go-xmpp"
client.Options.SoftwareVersion = Version
} else {
if client.Options.SoftwareVersion == "" {
client.Options.SoftwareVersion = "undefined"
}
}
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.
func NewClient(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
Debug: debug,
Session: false,
SoftwareName: "go-xmpp",
SoftwareVersion: Version,
}
return opts.NewClient()
}
// NewClientNoTLS creates a new client without TLS
func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
opts := Options{
Host: host,
User: user,
Password: passwd,
NoTLS: true,
Debug: debug,
Session: false,
SoftwareName: "go-xmpp",
SoftwareVersion: Version,
}
return opts.NewClient()
}
// Close closes the XMPP connection
func (c *Client) Close() error {
c.shutdown = true
if c.periodicPings {
c.periodicPingTicker.Stop()
}
if c.conn != (*tls.Conn)(nil) {
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.
switch err {
case io.EOF:
return c.conn.Close()
case nil:
if ee.Name.Local == "stream" {
return c.conn.Close()
}
default:
c.conn.Close()
return err
}
}
}
return nil
}
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)
}
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:
switch len(a) {
case 1:
// Allow it to specify the domain as username for ANONYMOUS authentication.
// Otherwise connection fails if the connection target differs from the server
// name
domain = o.User
user = ""
o.User = ""
case 2:
user = a[0]
domain = a[1]
}
default:
domain = o.Host
}
if strings.Contains(domain, ":") {
domain = strings.SplitN(domain, ":", 2)[0]
}
// Declare intent to be a jabber client and gather stream features.
f, err := c.startStream(o, domain)
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 connection is not yet encrypted attempt StartTLS.
if !c.IsEncrypted() {
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, saslUpgrade string
var saslUpgradeMech string
var serverSignature, keyingMaterial, successMsg []byte
var scramPLUS, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool
var cbsSlice, mechSlice, upgrSlice []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
if o.UserAgentSW != "" {
userAgentSW = fmt.Sprintf("%s", o.UserAgentSW)
resource = o.UserAgentSW
} else {
userAgentSW = "go-xmpp"
resource = "go-xmpp"
}
bind2Data = fmt.Sprintf("%s",
XMPPNS_BIND_0, resource)
}
if o.UserAgentDev != "" {
userAgentDev = fmt.Sprintf("%s", o.UserAgentDev)
}
if o.UserAgentID != "" {
userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID)
}
} else {
mechSlice = f.Mechanisms.Mechanism
}
if o.User == "" && o.Password == "" {
foundAnonymous := false
for _, m := range mechSlice {
if m == "ANONYMOUS" {
mechanism = m
if sasl2 {
fmt.Fprintf(c.stanzaWriter,
"%s%s%s%s\n",
XMPPNS_SASL_2, mechanism, userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
} else {
fmt.Fprintf(c.stanzaWriter, "\n", XMPPNS_XMPP_SASL)
}
foundAnonymous = true
break
}
}
if !foundAnonymous {
return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified")
}
} 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")
}
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") && o.OAuthToken != "" && o.OAuthScope != "":
mechanism = "X-OAUTH2"
// Do not use PLAIN auth if NoPlain is set.
case slices.Contains(mechSlice, "PLAIN") && !o.NoPLAIN && (tlsConnOK || o.InsecureAllowUnencryptedAuth):
mechanism = "PLAIN"
}
}
if strings.HasPrefix(mechanism, "SCRAM-SHA") {
if strings.HasSuffix(mechanism, "PLUS") {
scramPLUS = true
}
for _, cbs := range f.ChannelBindings.ChannelBinding {
cbsSlice = append(cbsSlice, cbs.Type)
}
if scramPLUS {
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()
}
if len(keyingMaterial) == 0 {
return errors.New(mechanism + ": no keying material")
}
switch {
case tls13 && !serverEndPoint:
channelBinding = base64.StdEncoding.EncodeToString(slices.Concat([]byte("p=tls-exporter,,"), keyingMaterial))
case serverEndPoint:
channelBinding = base64.StdEncoding.EncodeToString(slices.Concat([]byte("p=tls-server-end-point,,"), keyingMaterial))
default:
channelBinding = base64.StdEncoding.EncodeToString(slices.Concat([]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 !o.NoSASLUpgrade &&
!(o.Fast && f.Authentication.Inline.Fast.Mechanism != nil) {
for _, um := range f.Authentication.Upgrade {
upgrSlice = append(upgrSlice, um.Text)
}
switch {
case slices.Contains(upgrSlice, UPGR_SCRAM_SHA_512):
saslUpgradeMech = UPGR_SCRAM_SHA_512
case slices.Contains(upgrSlice, UPGR_SCRAM_SHA_256):
saslUpgradeMech = UPGR_SCRAM_SHA_256
}
if saslUpgradeMech != "" {
saslUpgrade = fmt.Sprintf("%s",
XMPPNS_SASL_UPGRADE_0, saslUpgradeMech)
}
}
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("", XMPPNS_FAST_0, mech)
} else {
var fastInvalidate string
if o.FastInvalidate {
fastInvalidate = " invalidate='true'"
}
fastAuth = fmt.Sprintf("", XMPPNS_FAST_0, fastInvalidate)
tlsState := tlsConn.ConnectionState()
mechanism = o.FastMechanism
switch mechanism {
case HT_SHA_256_EXPR:
if !tls13 {
return fmt.Errorf("fast: %s can only be used when using TLSv1.3", HT_SHA_256_EXPR)
}
keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32)
if err != nil {
return err
}
case HT_SHA_256_UNIQ:
if tls13 {
return fmt.Errorf("fast: %s can not be used when using TLSv1.3", 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 := slices.Concat([]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,
"%s%s%s%s%s%s\n",
XMPPNS_SASL_2, mechanism, saslUpgrade, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
} else {
fmt.Fprintf(c.stanzaWriter, "%s\n",
XMPPNS_XMPP_SASL, 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)
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 := slices.Concat([]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
c.domain = domain
}
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, XMPPNS_XMPP_SESSION)
}
// 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 string
var dgProtect, dgProtectSep, dgProtectCBSep []byte
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=") || strings.HasPrefix(serverReply, "h=")) && o.SSDP:
dgProtectSep = []byte{0x1e}
dgProtectCBSep = []byte{0x1f}
serverDgProtectHash := strings.SplitN(serverReply, "=", 2)[1]
slices.Sort(f.Mechanisms.Mechanism)
for _, mech := range f.Mechanisms.Mechanism {
if len(dgProtect) == 0 {
dgProtect = []byte(mech)
} else {
dgProtect = append(dgProtect, dgProtectSep...)
dgProtect = append(dgProtect, []byte(mech)...)
}
}
slices.Sort(cbsSlice)
for i, cb := range cbsSlice {
if i == 0 {
dgProtect = append(dgProtect, dgProtectCBSep...)
dgProtect = append(dgProtect, []byte(cb)...)
} else {
dgProtect = append(dgProtect, dgProtectSep...)
dgProtect = append(dgProtect, []byte(cb)...)
}
}
dgh := shaNewFn()
dgh.Write(dgProtect)
dHash := dgh.Sum(nil)
dHashb64 := base64.StdEncoding.EncodeToString(dHash)
if dHashb64 != serverDgProtectHash {
return fmt.Errorf("SCRAM: downgrade protection hash mismatch, expected: %s (hash of %s), received: %s",
dHashb64, dgProtect, serverDgProtectHash)
}
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, err := pbkdf2.Key(shaNewFn, o.Password, salt,
iterations, shaNewFn().Size())
if err != nil {
return err
}
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_SHA_256_PLUS:
storedKey256 := sha256.Sum256(clientKey)
storedKey = storedKey256[:]
case SCRAM_SHA_1, SCRAM_SHA_1_PLUS:
storedKey1 := sha1.Sum(clientKey) //nolint: gosec,G401 // Servers use this because of older clients.
storedKey = storedKey1[:]
}
_, err = h.Write([]byte("Server Key"))
if err != nil {
return err
}
serverFirstMessage, err := base64.StdEncoding.DecodeString(sfm)
if err != nil {
return err
}
split := strings.SplitAfter(clientFirstMessage, ",,")
if len(split) < 2 {
return errors.New("SCRAM: clientFirstMessage didn't contain ',,'")
}
authMessage = split[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", XMPPNS_SASL_2,
clientFinalMessage)
} else {
fmt.Fprintf(c.stanzaWriter, "%s\n", XMPPNS_XMPP_SASL,
clientFinalMessage)
}
}
if mechanism == "X-OAUTH2" {
// 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", XMPPNS_SASL_2, o.OAuthXmlNs, enc)
} else {
fmt.Fprintf(c.stanzaWriter, "%s\n", XMPPNS_XMPP_SASL, 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 o.UserAgentSW != "" {
resource = o.UserAgentSW
} else {
resource = "go-xmpp"
}
bind2Data = fmt.Sprintf("%s",
XMPPNS_BIND_0, resource)
if sasl2 {
fmt.Fprintf(c.conn, "%s%s\n", XMPPNS_SASL_2, enc, bind2Data)
} else {
fmt.Fprintf(c.conn, "%s\n", XMPPNS_XMPP_SASL, enc)
}
}
}
if mechanism == "" {
return fmt.Errorf("no viable authentication method available: %v", f.Mechanisms.Mechanism)
}
var connected bool
for !connected {
// Next message should be either success or failure.
name, val, err := c.next()
if err != nil {
return err
}
switch v := val.(type) {
case *sasl2Continue:
successMsg, err = base64.StdEncoding.DecodeString(v.AdditionalData)
if err != nil {
return err
}
fmt.Fprintf(c.stanzaWriter, "\n",
XMPPNS_SASL_2, saslUpgradeMech)
name, val, err = c.next()
if err != nil {
return err
}
switch v := val.(type) {
case *sasl2TaskData:
var shaNewFn func() hash.Hash
switch saslUpgradeMech {
case UPGR_SCRAM_SHA_512:
shaNewFn = sha512.New
case UPGR_SCRAM_SHA_256:
shaNewFn = sha256.New
}
salt, err := base64.StdEncoding.DecodeString(v.Salt.Text)
if err != nil {
return err
}
saltedPassword, err := pbkdf2.Key(shaNewFn, o.Password, salt,
v.Salt.Iterations, shaNewFn().Size())
if err != nil {
return err
}
saltedPasswordB64 := base64.StdEncoding.EncodeToString(saltedPassword)
fmt.Fprintf(c.stanzaWriter, "%s\n", XMPPNS_SASL_2, XMPPNS_SCRAM_UPGRADE_0, saltedPasswordB64)
continue
default:
return fmt.Errorf("sasl2 upgrade failure: expected *sasl2TaskData, got %s", name.Local)
}
case *sasl2Success:
if strings.HasPrefix(mechanism, "SCRAM-SHA") {
if len(successMsg) == 0 {
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
c.domain = domain
}
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 == "" {
// 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)
default:
return errors.New("expected or , got <" + name.Local + "> in " + name.Space)
}
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
}
}
// 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 !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, XMPPNS_XMPP_BIND)
} else {
fmt.Fprintf(c.stanzaWriter, "%s\n", cookie, XMPPNS_XMPP_BIND, 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 == XMPPNS_XMPP_BIND {
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
cookie := getCookie() // generate new id value for session
fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(domain), cookie, XMPPNS_XMPP_SESSION)
}
// We're connected and can now receive and send messages.
fmt.Fprintf(c.stanzaWriter, "%s%s\n", o.Status, o.StatusMessage)
connected = true
}
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.
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 && o.InsecureAllowUnencryptedAuth && !o.StartTLS:
// the server does not support StartTLS and the user doesn't require it.
return f, nil
case f.StartTLS == nil && o.StartTLS:
// the server does not support StartTLS but the user requires it.
return f, fmt.Errorf("StartTLS is required but the server doesn't support it")
case f.StartTLS == nil:
// the server does not support StartTLS but InsecureAllowUnencryptedAuth is not set.
return f, fmt.Errorf("StartTLS is not supported by the server but InsecureAllowUnencryptedAuth is not set")
case f.StartTLS != nil:
// the server does not require StartTLS and user does not require it.
if f.StartTLS.Required == nil && o.InsecureAllowUnencryptedAuth && !o.StartTLS {
return f, nil
}
}
var err error
fmt.Fprintf(c.stanzaWriter, "\n")
var k tlsProceed
if err = c.p.DecodeElement(&k, nil); err != nil {
return f, errors.New("unmarshal : " + err.Error())
}
tc := o.TLSConfig
if tc == nil {
tc = DefaultConfig.Clone()
// 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
// 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 {
if o.DebugWriter == nil {
o.DebugWriter = os.Stderr
}
debugRecv := &debugWriter{w: o.DebugWriter, prefix: "RECV "}
c.p = xml.NewDecoder(tee{c.conn, debugRecv})
debugSend := &debugWriter{w: o.DebugWriter, prefix: "SEND "}
c.stanzaWriter = io.MultiWriter(c.conn, debugSend)
} else {
c.p = xml.NewDecoder(c.conn)
c.stanzaWriter = c.conn
}
var fromString string
if len(o.User) > 0 {
fromString = fmt.Sprintf("from='%s' ", xmlEscape(o.User))
}
if c.IsEncrypted() {
_, err := fmt.Fprintf(c.stanzaWriter, ""+
"\n",
fromString, xmlEscape(domain), XMPPNS_CLIENT, XMPPNS_STREAM)
if err != nil {
return nil, err
}
} else {
_, err := fmt.Fprintf(c.stanzaWriter, ""+
"\n",
xmlEscape(domain), XMPPNS_CLIENT, XMPPNS_STREAM)
if err != nil {
return nil, err
}
}
// We expect the server to start a .
se, err := c.nextStart()
if err != nil {
return nil, err
}
if se.Name.Space != XMPPNS_STREAM || se.Name.Local != "stream" {
return nil, fmt.Errorf("expected 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 to tell us authentication options.
// See section 4.6 in RFC 3920.
f := new(streamFeatures)
name, val, err := c.next()
if err != nil {
return f, err
}
switch v := val.(type) {
case *streamFeatures:
return v, nil
case *streamError:
if c.IsEncrypted() && v.SeeOtherHost.Text != "" {
c.conn.Close()
c.conn, err = connect(v.SeeOtherHost.Text, o.User, o.DialTimeout)
if err != nil {
return f, err
}
f, err = c.startStream(o, domain)
if err != nil {
return f, errors.New("unmarshal : " + err.Error())
}
return f, nil
}
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 f, errors.New("stream error: " + errorMessage)
default:
return f, errors.New("expected or , got <" + name.Local + "> in " + name.Space)
}
}
// IsEncrypted will return true if the client is connected using a TLS transport, either because it used.
// 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
}
// Chat is an incoming or outgoing XMPP chat message.
type Chat struct {
Remote string
Type string
Text string
Subject string
Thread string
// XEP-0066 Out-Of-Band url/desc
Oob Oob
// Deprecated Oob settings, use Oob.Url and Oob.Desc (above) instead.
Ooburl string
Oobdesc string
Lang string
// Only for incoming messages, ID for outgoing messages will be generated.
OriginID string
// Only for incoming messages, ID for outgoing messages will be generated.
StanzaID StanzaID
Roster Roster
Other []string
OtherElem []XMLElement
Stamp time.Time
}
type Roster []Contact
type Contact struct {
Remote string
Name string
Group []string
}
// Presence is an XMPP presence notification.
type Presence struct {
From string
To string
Type string
Show string
Status string
Priority string
ID string
Affiliation string
Role string
JID string
}
type IQ struct {
ID string
From string
To string
Type string
Query []byte
}
// Recv waits to receive the next XMPP stanza.
func (c *Client) Recv() (stanza interface{}, err error) {
for {
_, val, err := c.next()
if err != nil {
return Chat{}, err
}
// Reset ticker for periodic pings if configured.
if c.periodicPings {
c.periodicPingTicker.Reset(c.periodicPingPeriod)
}
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
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
}
}
stamp, _ := time.Parse(
"2006-01-02T15:04:05Z",
v.Delay.Stamp,
)
chat := Chat{
Remote: v.From,
Type: v.Type,
Text: v.Body,
Subject: v.Subject,
Thread: v.Thread,
Other: v.OtherStrings(),
OtherElem: v.Other,
Stamp: stamp,
Lang: v.Lang,
OriginID: v.OriginID.ID,
StanzaID: v.StanzaID,
Oob: v.Oob,
}
return chat, nil
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
case *clientPresence:
return Presence{
v.From,
v.To,
v.Type,
v.Show,
v.Status,
v.Priority,
v.ID,
v.X.Item.Affiliation,
v.X.Item.Role,
v.X.Item.Jid,
}, nil
case *clientIQ:
switch {
case v.Query.XMLName.Space == XMPPNS_PING && v.Type == "get":
// TODO check more strictly
err := c.SendResultPing(v.ID, v.From)
if err != nil {
return Chat{}, err
}
fallthrough
case v.Query.XMLName.Space == XMPPNS_IQ_VERSION && v.Type == "get":
if c.Options.ReportSoftwareVersion {
var osName string
if c.Options.ReportSoftwareOS {
osName = (strings.SplitN(runtime.GOOS, "/", 2))[0]
}
id, err := c.IqVersionResponse(
IQ{ID: v.ID, From: v.From, To: v.To},
c.Options.SoftwareName,
c.Options.SoftwareVersion,
osName,
)
if err != nil {
err := fmt.Errorf(
"unable to send version info to jabber server: id=%s, err=%w",
id,
err,
)
return Chat{}, err
}
} else {
id, err := c.ErrorServiceUnavailable(
IQ{ID: v.ID, From: v.From, To: v.To},
XMPPNS_IQ_VERSION,
"",
)
if err != nil {
err = fmt.Errorf(
"unable to send service unavailable message stanza to jabber server: id=%s, err=%w",
id,
err,
)
return Chat{}, err
}
}
case v.Type == "error":
switch {
case slices.Contains(c.subIDs, v.ID):
index := slices.Index(c.subIDs, v.ID)
c.subIDs = slices.Delete(c.subIDs, index, index)
// 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
}
return IQ{
ID: v.ID, From: v.From, To: v.To, Type: v.Type,
Query: res,
}, nil
}
case v.Type == "result":
switch {
case c.periodicPings && v.ID == c.periodicPingID:
if v.ID == c.periodicPingID {
c.periodicPingReply = true
}
case 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 v.Query.XMLName.Space == XMPPNS_DISCO_INFO:
var disco clientDiscoQuery
err := xml.Unmarshal(v.InnerXML, &disco)
if err != nil {
return DiscoResult{}, err
}
return DiscoResult{
ID: v.ID,
From: v.From,
To: v.To,
Features: clientFeaturesToReturn(disco.Features),
Identities: clientIdentitiesToReturn(disco.Identities),
X: disco.X,
}, nil
case v.Query.XMLName.Space == XMPPNS_HTTP_UPLOAD_0:
var uploadSlot Slot
err := xml.Unmarshal([]byte(v.InnerXML), &uploadSlot)
uploadSlot.ID = v.ID
// TODO: Validate that the URLs contain HTTPS
return uploadSlot, err
case slices.Contains(c.subIDs, v.ID):
index := slices.Index(c.subIDs, v.ID)
c.subIDs = slices.Delete(c.subIDs, index, index)
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 slices.Contains(c.unsubIDs, v.ID):
index := slices.Index(c.unsubIDs, v.ID)
c.unsubIDs = slices.Delete(c.unsubIDs, index, index)
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 slices.Contains(c.itemsIDs, v.ID):
index := slices.Index(c.itemsIDs, v.ID)
c.itemsIDs = slices.Delete(c.itemsIDs, index, index)
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
}
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
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
}
}
}
}
// Send sends the message wrapped inside an XMPP message stanza body.
func (c *Client) Send(chat Chat) (n int, err error) {
var subtext, thdtext, oobtext string
if chat.Subject != `` {
subtext = `` + xmlEscape(chat.Subject) + ``
}
if chat.Thread != `` {
thdtext = `` + xmlEscape(chat.Thread) + ``
}
if chat.Oob.Url != `` || chat.Ooburl != `` {
if chat.Oob.Url == `` {
chat.Oob.Url = chat.Ooburl
fmt.Println("xmpp: chat.Ooburl is deprecated, use chat.Oob.Url instead.")
}
oobtext = `` + xmlEscape(chat.Oob.Url) + ``
if chat.Oob.Desc != `` || chat.Oobdesc != `` {
if chat.Oob.Desc == `` {
chat.Oob.Desc = chat.Oobdesc
fmt.Println("xmpp: chat.Oobdesc is deprecated, use chat.Oob.Desc instead.")
}
oobtext += `` + xmlEscape(chat.Oob.Desc) + ``
}
oobtext += ``
}
chat.Text = validUTF8(chat.Text)
id := getUUID()
stanza := fmt.Sprintf("%s%s"+
"%s%s\n",
xmlEscape(chat.Remote), xmlEscape(chat.Type), id, subtext, xmlEscape(chat.Text),
XMPPNS_SID_0, id, oobtext, thdtext)
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. Any message body will be discarded
// and replaced by the OOB URL..
func (c *Client) SendOOB(chat Chat) (n int, err error) {
var thdtext, oobtext string
if chat.Thread != `` {
thdtext = `` + xmlEscape(chat.Thread) + ``
}
if chat.Oob.Url == `` && chat.Ooburl == `` {
return 0, fmt.Errorf("SendOOB requires chat.Oob.Url to be set")
}
if chat.Oob.Url == `` {
chat.Oob.Url = chat.Ooburl
fmt.Println("xmpp: chat.Ooburl is deprecated, use chat.Oob.Url instead.")
}
if chat.Oob.Desc == `` && chat.Oobdesc != `` {
chat.Oob.Desc = chat.Oobdesc
fmt.Println("xmpp: chat.Oobdesc is deprecated, use chat.Oob.Desc instead.")
}
oobtext = `` + xmlEscape(chat.Oob.Url) + ``
if chat.Oob.Desc != `` {
oobtext += `` + xmlEscape(chat.Oob.Desc) + ``
}
oobtext += ``
id := getUUID()
stanza := fmt.Sprintf(""+
"%s%s%s\n",
xmlEscape(chat.Remote), xmlEscape(chat.Type), id, XMPPNS_SID_0, id,
oobtext, thdtext, xmlEscape(chat.Oob.Url))
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) {
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) {
// 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("%s\n", buf)
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) {
return fmt.Fprintf(c.conn, " ")
}
// SendHtml sends the message as HTML as defined by XEP-0071
func (c *Client) SendHtml(chat Chat) (n int, err error) {
id := getUUID()
stanza := fmt.Sprintf("%s"+
"%s"+
"\n",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), XMPPNS_SID_0, id, 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.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"`
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 struct {
Text string `xml:",chardata"`
Lang string `xml:"lang,attr"`
Xmlns string `xml:"xmlns,attr"`
} `xml:"text"`
SeeOtherHost struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
} `xml:"see-other-host"`
}
// 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"`
}
type tlsProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
}
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"`
Upgrade []struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
} `xml:"upgrade"`
}
// 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 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 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 sasl2Continue struct {
XMLName xml.Name `xml:"continue"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
AdditionalData string `xml:"additional-data"`
Tasks struct {
Text string `xml:",chardata"`
Task string `xml:"task"`
} `xml:"tasks"`
}
type sasl2TaskData struct {
XMLName xml.Name `xml:"task-data"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Salt struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Iterations int `xml:"iterations,attr"`
} `xml:"salt"`
}
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 {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name `xml:",any"`
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"`
Resource string
Jid string `xml:"jid"`
}
// XEP-0359 Origin ID
type originID struct {
XMLName xml.Name `xml:"origin-id"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
ID string `xml:"id,attr"`
}
// XEP-0359 Stanza ID
type StanzaID struct {
XMLName xml.Name `xml:"stanza-id"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
ID string `xml:"id,attr"`
By string `xml:"by,attr"`
}
// RFC 3921 B.1 jabber:client
type clientMessage struct {
XMLName xml.Name `xml:"jabber:client message"`
From string `xml:"from,attr"`
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"`
Body string `xml:"body"`
Thread string `xml:"thread"`
// XEP-0359
OriginID originID `xml:"origin-id"`
StanzaID StanzaID `xml:"stanza-id"`
// Pubsub
Event clientPubsubEvent `xml:"event"`
// XEP-0060 OOB
Oob Oob
// Any hasn't matched element
Other []XMLElement `xml:",any"`
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()
}
type Delay struct {
Stamp string `xml:"stamp,attr"`
}
type clientPresence struct {
XMLName xml.Name `xml:"jabber:client presence"`
From string `xml:"from,attr"`
ID string `xml:"id,attr"`
To string `xml:"to,attr"`
Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
Lang string `xml:"lang,attr"`
X struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Item struct {
Text string `xml:",chardata"`
Affiliation string `xml:"affiliation,attr"`
Jid string `xml:"jid,attr"`
Role string `xml:"role,attr"`
} `xml:"item"`
} `xml:"x"`
Show string `xml:"show"` // away, chat, dnd, xa
Status string `xml:"status"` // sb []clientText
Priority string `xml:"priority,attr"`
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"`
}
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
}
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
}
// Scan XML token stream to find next StartElement.
func (c *Client) nextStart() (xml.StartElement, error) {
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()
return xml.StartElement{}, err
}
t := xml.CopyToken(to)
switch t := t.(type) {
case xml.StartElement:
c.nextMutex.Unlock()
return t, nil
case xml.EndElement:
if t.Name.Space == XMPPNS_STREAM && t.Name.Local == "stream" {
c.nextMutex.Unlock()
return xml.StartElement{}, fmt.Errorf("server closed stream")
}
}
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.Space == XMPPNS_STREAM && t.Name.Local == "error" {
return t, fmt.Errorf("server closed stream with error")
}
if t.Name.Space == XMPPNS_STREAM && 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 (c *Client) next() (xml.Name, interface{}, error) {
// Read start element to find out what type we want.
se, err := c.nextStart()
if err != nil {
return xml.Name{}, nil, err
}
// Put it in an interface and allocate one.
var nv interface{}
switch se.Name.Space + " " + se.Name.Local {
case XMPPNS_STREAM + " features":
nv = &streamFeatures{}
case XMPPNS_STREAM + " error":
nv = &streamError{}
case XMPPNS_XMPP_TLS + " starttls":
nv = &tlsStartTLS{}
case XMPPNS_XMPP_TLS + " proceed":
nv = &tlsProceed{}
case XMPPNS_XMPP_TLS + " failure":
nv = &tlsFailure{}
case XMPPNS_XMPP_SASL + " mechanisms":
nv = &saslMechanisms{}
case XMPPNS_SASL_2 + " challenge":
nv = &sasl2Challenge{}
case XMPPNS_XMPP_SASL + " challenge":
nv = &saslChallenge{}
case XMPPNS_XMPP_SASL + " response":
nv = ""
case XMPPNS_XMPP_SASL + " abort":
nv = &saslAbort{}
case XMPPNS_SASL_2 + " success":
nv = &sasl2Success{}
case XMPPNS_SASL_2 + " continue":
nv = &sasl2Continue{}
case XMPPNS_SASL_2 + " task-data":
nv = &sasl2TaskData{}
case XMPPNS_XMPP_SASL + " success":
nv = &saslSuccess{}
case XMPPNS_SASL_2 + " failure":
nv = &sasl2Failure{}
case XMPPNS_XMPP_SASL + " failure":
nv = &saslFailure{}
case XMPPNS_SASL_CB_0 + " sasl-channel-binding":
nv = &saslChannelBindings{}
case XMPPNS_XMPP_BIND + " bind":
nv = &bindBind{}
case XMPPNS_CLIENT + " message":
nv = &clientMessage{}
case XMPPNS_CLIENT + " presence":
nv = &clientPresence{}
case XMPPNS_CLIENT + " iq":
nv = &clientIQ{}
case XMPPNS_CLIENT + " error":
nv = &clientError{}
default:
return xml.Name{}, nil, errors.New("unexpected XMPP message " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
// Unmarshal into that storage.
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
}
func xmlEscape(s string) string {
var b bytes.Buffer
xml.Escape(&b, []byte(s))
return b.String()
}
type tee struct {
r io.Reader
w io.Writer
}
func (t tee) Read(p []byte) (n int, err error) {
n, err = t.r.Read(p)
if n > 0 {
_, err = t.w.Write(p[0:n])
if err != nil {
return n, err
}
_, err = t.w.Write([]byte("\n"))
}
return n, err
}
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
}
go-xmpp-0.3.1/xmpp_avatar.go 0000664 0000000 0000000 00000005216 15116612257 0015765 0 ustar 00root root 0000000 0000000 package xmpp
import (
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"errors"
"strconv"
)
type clientAvatarData struct {
XMLName xml.Name `xml:"data"`
Data []byte `xml:",innerxml"`
}
type clientAvatarInfo struct {
XMLName xml.Name `xml:"info"`
Bytes string `xml:"bytes,attr"`
Width string `xml:"width,attr"`
Height string `xml:"height,attr"`
ID string `xml:"id,attr"`
Type string `xml:"type,attr"`
URL string `xml:"url,attr"`
}
type clientAvatarMetadata struct {
XMLName xml.Name `xml:"metadata"`
XMLNS string `xml:"xmlns,attr"`
Info clientAvatarInfo `xml:"info"`
}
type AvatarData struct {
Data []byte
From string
}
type AvatarMetadata struct {
From string
Bytes int
Width int
Height int
ID string
Type string
URL string
}
func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
var data clientAvatarData
err := xml.Unmarshal(itemsBody, &data)
if err != nil {
return AvatarData{}, err
}
// Base64-decode the avatar data to check its SHA1 hash
dataRaw, err := base64.StdEncoding.DecodeString(
string(data.Data))
if err != nil {
return AvatarData{}, err
}
hash := sha1.Sum(dataRaw) //nolint: gosec,G401 // It is not about security.
hashStr := hex.EncodeToString(hash[:])
if hashStr != id {
return AvatarData{}, errors.New("SHA1 hashes do not match")
}
return AvatarData{
Data: dataRaw,
From: from,
}, nil
}
func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
var meta clientAvatarMetadata
err := xml.Unmarshal(body, &meta)
if err != nil {
return AvatarMetadata{}, err
}
return AvatarMetadata{
From: from,
Bytes: atoiw(meta.Info.Bytes),
Width: atoiw(meta.Info.Width),
Height: atoiw(meta.Info.Height),
ID: meta.Info.ID,
Type: meta.Info.Type,
URL: meta.Info.URL,
}, nil
}
// A wrapper for atoi which just returns -1 if an error occurs
func atoiw(str string) int {
i, err := strconv.Atoi(str)
if err != nil {
return -1
}
return i
}
func (c *Client) AvatarSubscribeMetadata(jid string) error {
return c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
}
func (c *Client) AvatarUnsubscribeMetadata(jid string) error {
return c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
}
func (c *Client) AvatarRequestData(jid string) error {
return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid)
}
func (c *Client) AvatarRequestDataByID(jid, id string) error {
return c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id)
}
func (c *Client) AvatarRequestMetadata(jid string) error {
return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid)
}
go-xmpp-0.3.1/xmpp_disco.go 0000664 0000000 0000000 00000004106 15116612257 0015605 0 ustar 00root root 0000000 0000000 package xmpp
import (
"encoding/xml"
)
type clientDiscoFeature struct {
XMLName xml.Name `xml:"feature"`
Var string `xml:"var,attr"`
}
type clientDiscoIdentity struct {
XMLName xml.Name `xml:"identity"`
Category string `xml:"category,attr"`
Type string `xml:"type,attr"`
Name string `xml:"name,attr"`
}
type clientDiscoQuery struct {
XMLName xml.Name `xml:"query"`
Features []clientDiscoFeature `xml:"feature"`
Identities []clientDiscoIdentity `xml:"identity"`
X []DiscoX `xml:"x"`
}
type clientDiscoItem struct {
XMLName xml.Name `xml:"item"`
Jid string `xml:"jid,attr"`
Node string `xml:"node,attr"`
Name string `xml:"name,attr"`
}
type clientDiscoItemsQuery struct {
XMLName xml.Name `xml:"query"`
Items []clientDiscoItem `xml:"item"`
}
type DiscoIdentity struct {
Category string
Type string
Name string
}
type DiscoItem struct {
Jid string
Name string
Node string
}
type DiscoResult struct {
ID string
From string
To string
Features []string
Identities []DiscoIdentity
X []DiscoX
}
type DiscoX struct {
XMLName xml.Name `xml:"x"`
Field []DiscoXField `xml:"field"`
}
type DiscoXField struct {
Type string `xml:"type,attr"`
Var string `xml:"var,attr"`
Value []string `xml:"value"`
}
type DiscoItems struct {
Jid string
Items []DiscoItem
}
func clientFeaturesToReturn(features []clientDiscoFeature) []string {
var ret []string
for _, feature := range features {
ret = append(ret, feature.Var)
}
return ret
}
func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity {
var ret []DiscoIdentity
for _, id := range identities {
ret = append(ret, DiscoIdentity{
Category: id.Category,
Type: id.Type,
Name: id.Name,
})
}
return ret
}
func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem {
var ret []DiscoItem
for _, item := range items {
ret = append(ret, DiscoItem{
Jid: item.Jid,
Name: item.Name,
Node: item.Node,
})
}
return ret
}
go-xmpp-0.3.1/xmpp_error.go 0000664 0000000 0000000 00000003023 15116612257 0015632 0 ustar 00root root 0000000 0000000 package xmpp
import (
"fmt"
)
// ErrorServiceUnavailable implements error response about a feature that is not available. Currently implemented for
// xep-0030.
// QueryXmlns is about incoming xmlns attribute in query tag.
// Node is about incoming node attribute in query tag (looks like it used only in disco#commands).
//
// If queried feature is not here on purpose, standards suggest to answer with this stanza.
func (c *Client) ErrorServiceUnavailable(v IQ, queryXmlns, node string) (string, error) {
query := fmt.Sprintf("", node)
} else {
query += "/>"
}
query += ""
query += ""
query += ""
return c.RawInformation(
v.To,
v.From,
v.ID,
IQTypeError,
query,
)
}
// ErrorNotImplemented implements error response about a feature that is not (yet?) implemented.
// Xmlns is about not implemented feature.
//
// If queried feature is not here because of it under development or for similar reasons, standards suggest to answer with
// this stanza.
func (c *Client) ErrorNotImplemented(v IQ, xmlns, feature string) (string, error) {
query := ""
query += ""
query += fmt.Sprintf(
"",
xmlns,
feature,
)
query += ""
return c.RawInformation(
v.To,
v.From,
v.ID,
IQTypeError,
query,
)
}
go-xmpp-0.3.1/xmpp_http_upload.go 0000664 0000000 0000000 00000001361 15116612257 0017027 0 ustar 00root root 0000000 0000000 package xmpp
import (
"encoding/xml"
)
type Slot struct {
// TODO: Maybe this doesn't belong here
ID string
XMLName xml.Name `xml:"slot"`
Put Put
Get Get
}
type Put struct {
XMLName xml.Name `xml:"put"`
Url string `xml:"url,attr"`
Headers []Header `xml:"header"`
}
type Get struct {
XMLName xml.Name `xml:"get"`
Url string `xml:"url,attr"`
}
type Header struct {
XMLName xml.Name `xml:"header"`
Name string `xml:"name,attr"`
Value string `xml:",innerxml"`
}
// Oob is an out-of-band url/description, used in file uploads.
// See https://xmpp.org/extensions/xep-0066.html
type Oob struct {
XMLName xml.Name `xml:"x,xmlns:jabber:x:oob"`
Url string `xml:"url"`
Desc string `xml:"desc"`
}
go-xmpp-0.3.1/xmpp_information_query.go 0000664 0000000 0000000 00000005606 15116612257 0020264 0 ustar 00root root 0000000 0000000 package xmpp
import (
"fmt"
"time"
)
func (c *Client) Discovery() (string, error) {
// use UUIDv4 for a pseudo random id.
reqID := getUUID()
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "")
}
// Discover information about a node. Empty node queries info about server itself.
func (c *Client) DiscoverNodeInfo(node string) (string, error) {
query := fmt.Sprintf("", XMPPNS_DISCO_INFO, node)
return c.RawInformation(c.jid, c.domain, getUUID(), IQTypeGet, query)
}
// Discover information about given item from given jid.
func (c *Client) DiscoverInfo(to string) (string, error) {
query := fmt.Sprintf("", XMPPNS_DISCO_INFO)
return c.RawInformation(c.jid, to, getUUID(), IQTypeGet, query)
}
// Discover items that the server exposes
func (c *Client) DiscoverServerItems() (string, error) {
return c.DiscoverEntityItems(c.domain)
}
// Discover items that an entity exposes
func (c *Client) DiscoverEntityItems(jid string) (string, error) {
query := fmt.Sprintf("", XMPPNS_DISCO_ITEMS)
return c.RawInformation(c.jid, jid, getUUID(), IQTypeGet, query)
}
// 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\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\n"
_, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
return id, err
}
// UrnXMPPTimeResponse implements response to query entity's current time (xep-0202).
func (c *Client) UrnXMPPTimeResponse(v IQ, timezoneOffset string) (string, error) {
query := fmt.Sprintf(
"",
XMPPNS_TIME,
timezoneOffset,
time.Now().UTC().Format(time.RFC3339),
)
return c.RawInformation(
v.To,
v.From,
v.ID,
IQTypeResult,
query,
)
}
// IqVersionResponse responding with software version, according to xep-0092.
func (c *Client) IqVersionResponse(v IQ, name string, version string, os string) (string, error) {
if name == "" {
name = "go-xmpp"
version = Version
}
if version == "" {
version = "undefined"
}
query := fmt.Sprintf("", XMPPNS_IQ_VERSION)
query += fmt.Sprintf("%s", name)
query += fmt.Sprintf("%s", version)
if os != "" {
query += fmt.Sprintf("%s", os)
}
query += ""
return c.RawInformation(
v.To,
v.From,
v.ID,
IQTypeResult,
query,
)
}
go-xmpp-0.3.1/xmpp_muc.go 0000664 0000000 0000000 00000010162 15116612257 0015267 0 ustar 00root root 0000000 0000000 // Copyright 2013 Flo Lauber . All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(flo):
// - support password protected MUC rooms
// - cleanup signatures of join/leave functions
package xmpp
import (
"errors"
"fmt"
"time"
)
const (
NoHistory = 0
CharHistory = 1
StanzaHistory = 2
SecondsHistory = 3
SinceHistory = 4
)
// 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.stanzaWriter, ""+"%s\n",
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
}
func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
if nick == "" {
nick = c.jid
}
return fmt.Fprintf(c.stanzaWriter, ""+
""+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC)
}
// xep-0045 7.2
func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
if nick == "" {
nick = c.jid
}
switch history_type {
case NoHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC)
case CharHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, history)
case StanzaHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, history)
case SecondsHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, history)
case SinceHistory:
if history_date != nil {
return fmt.Fprintf(c.stanzaWriter, ""+
""+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, history_date.Format(time.RFC3339))
}
}
return 0, errors.New("unknown history option")
}
// xep-0045 7.2.6
func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
if nick == "" {
nick = c.jid
}
switch history_type {
case NoHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
"%s"+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, xmlEscape(password))
case CharHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
"%s"+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, xmlEscape(password), history)
case StanzaHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
"%s"+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, xmlEscape(password), history)
case SecondsHistory:
return fmt.Fprintf(c.stanzaWriter, ""+
""+
"%s"+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, xmlEscape(password), history)
case SinceHistory:
if history_date != nil {
return fmt.Fprintf(c.stanzaWriter, ""+
""+
"%s"+
""+
"\n",
xmlEscape(jid), xmlEscape(nick), XMPPNS_MUC, xmlEscape(password), history_date.Format(time.RFC3339))
}
}
return 0, errors.New("unknown history option")
}
// xep-0045 7.14
func (c *Client) LeaveMUC(jid string) (n int, err error) {
return fmt.Fprintf(c.stanzaWriter, "\n",
c.jid, xmlEscape(jid))
}
go-xmpp-0.3.1/xmpp_ping.go 0000664 0000000 0000000 00000002707 15116612257 0015446 0 ustar 00root root 0000000 0000000 package xmpp
import (
"fmt"
"time"
)
func (c *Client) PingC2S(jid, server string) error {
if jid == "" {
jid = c.jid
}
if server == "" {
server = c.domain
}
_, err := fmt.Fprintf(c.stanzaWriter, ""+
""+
"\n",
xmlEscape(jid), xmlEscape(server), getUUID())
return err
}
func (c *Client) PingS2S(fromServer, toServer string) error {
_, err := fmt.Fprintf(c.stanzaWriter, ""+
""+
"\n",
xmlEscape(fromServer), xmlEscape(toServer), getUUID())
return err
}
func (c *Client) SendResultPing(id, toServer string) error {
_, err := fmt.Fprintf(c.stanzaWriter, "\n",
xmlEscape(toServer), xmlEscape(id))
return err
}
func (c *Client) sendPeriodicPings() {
for range c.periodicPingTicker.C {
// Reset ticker for periodic pings if configured.
if c.periodicPings {
c.periodicPingTicker.Reset(c.periodicPingPeriod)
}
c.periodicPingID = getUUID()
c.periodicPingReply = false
_, err := fmt.Fprintf(c.stanzaWriter, ""+
"\n",
xmlEscape(c.jid), xmlEscape(c.domain), c.periodicPingID)
if err != nil {
c.Close()
}
time.Sleep(c.periodicPingTimeout)
if !c.periodicPingReply {
c.shutdown = true
fmt.Fprintf(c.stanzaWriter, "\n")
c.conn.Close()
}
}
}
go-xmpp-0.3.1/xmpp_pubsub.go 0000664 0000000 0000000 00000005676 15116612257 0016021 0 ustar 00root root 0000000 0000000 package xmpp
import (
"encoding/xml"
"fmt"
)
type clientPubsubItem struct {
XMLName xml.Name `xml:"item"`
ID string `xml:"id,attr"`
Body []byte `xml:",innerxml"`
}
type clientPubsubItems struct {
XMLName xml.Name `xml:"items"`
Node string `xml:"node,attr"`
Items []clientPubsubItem `xml:"item"`
}
type clientPubsubEvent struct {
XMLName xml.Name `xml:"event"`
XMLNS string `xml:"xmlns,attr"`
Items clientPubsubItems `xml:"items"`
}
type clientPubsubError struct {
XMLName xml.Name
}
type clientPubsubSubscription struct {
XMLName xml.Name `xml:"subscription"`
Node string `xml:"node,attr"`
JID string `xml:"jid,attr"`
SubID string `xml:"subid,attr"`
}
type PubsubEvent struct {
Node string
Items []PubsubItem
}
type PubsubSubscription struct {
SubID string
JID string
Node string
Errors []string
}
type PubsubUnsubscription PubsubSubscription
type PubsubItem struct {
ID string
InnerXML []byte
}
type PubsubItems struct {
Node string
Items []PubsubItem
}
// Converts []clientPubsubItem to []PubsubItem
func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem {
var tmp []PubsubItem
for _, i := range items {
tmp = append(tmp, PubsubItem{
ID: i.ID,
InnerXML: i.Body,
})
}
return tmp
}
func pubsubClientToReturn(event clientPubsubEvent) PubsubEvent {
return PubsubEvent{
Node: event.Items.Node,
Items: pubsubItemsToReturn(event.Items.Items),
}
}
func pubsubStanza(body string) string {
return fmt.Sprintf("%s",
XMPPNS_PUBSUB, body)
}
func pubsubSubscriptionStanza(node, jid string) string {
body := fmt.Sprintf("",
xmlEscape(node),
xmlEscape(jid))
return pubsubStanza(body)
}
func pubsubUnsubscriptionStanza(node, jid string) string {
body := fmt.Sprintf("",
xmlEscape(node),
xmlEscape(jid))
return pubsubStanza(body)
}
func (c *Client) PubsubSubscribeNode(node, jid string) error {
id := getUUID()
c.subIDs = append(c.subIDs, id)
_, err := c.RawInformation(c.jid,
jid,
id,
"set",
pubsubSubscriptionStanza(node, c.jid))
return err
}
func (c *Client) PubsubUnsubscribeNode(node, jid string) error {
id := getUUID()
c.unsubIDs = append(c.unsubIDs, id)
_, err := c.RawInformation(c.jid,
jid,
id,
"set",
pubsubUnsubscriptionStanza(node, c.jid))
return err
}
func (c *Client) PubsubRequestLastItems(node, jid string) error {
id := getUUID()
c.itemsIDs = append(c.itemsIDs, id)
body := fmt.Sprintf("", node)
_, err := c.RawInformation(c.jid, jid, id, "get", pubsubStanza(body))
return err
}
func (c *Client) PubsubRequestItem(node, jid, id string) error {
stanzaID := getUUID()
c.itemsIDs = append(c.itemsIDs, stanzaID)
body := fmt.Sprintf(" ", node, id)
_, err := c.RawInformation(c.jid, jid, stanzaID, "get", pubsubStanza(body))
return err
}
go-xmpp-0.3.1/xmpp_subscription.go 0000664 0000000 0000000 00000001331 15116612257 0017225 0 ustar 00root root 0000000 0000000 package xmpp
import (
"fmt"
)
func (c *Client) ApproveSubscription(jid string) {
fmt.Fprintf(c.stanzaWriter, "\n",
xmlEscape(jid))
}
func (c *Client) RevokeSubscription(jid string) {
fmt.Fprintf(c.stanzaWriter, "\n",
xmlEscape(jid))
}
// Deprecated: Use RevertSubscription instead.
func (c *Client) RetrieveSubscription(jid string) {
c.RevertSubscription(jid)
}
func (c *Client) RevertSubscription(jid string) {
fmt.Fprintf(c.conn, "\n",
xmlEscape(jid))
}
func (c *Client) RequestSubscription(jid string) {
fmt.Fprintf(c.stanzaWriter, "\n",
xmlEscape(jid))
}
go-xmpp-0.3.1/xmpp_test.go 0000664 0000000 0000000 00000015632 15116612257 0015471 0 ustar 00root root 0000000 0000000 package xmpp
import (
"bytes"
"encoding/xml"
"io"
"net"
"reflect"
"strings"
"testing"
"time"
)
type localAddr struct{}
func (a *localAddr) Network() string {
return "tcp"
}
func (addr *localAddr) String() string {
return "localhost:5222"
}
type testConn struct {
*bytes.Buffer
}
func tConnect(s string) net.Conn {
var conn testConn
conn.Buffer = bytes.NewBufferString(s)
return &conn
}
func (*testConn) Close() error {
return nil
}
func (*testConn) LocalAddr() net.Addr {
return &localAddr{}
}
func (*testConn) RemoteAddr() net.Addr {
return &localAddr{}
}
func (*testConn) SetDeadline(time.Time) error {
return nil
}
func (*testConn) SetReadDeadline(time.Time) error {
return nil
}
func (*testConn) SetWriteDeadline(time.Time) error {
return nil
}
var text = strings.TrimSpace(`
{"random": "<text>"}
InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
`)
func TestStanzaError(t *testing.T) {
var c Client
c.conn = tConnect(text)
c.p = xml.NewDecoder(c.conn)
v, err := c.Recv()
if err != nil {
t.Fatalf("Recv() = %v", err)
}
chat := Chat{
Type: "error",
Other: []string{
"\n\t\t{\"random\": \"\"}\n\t",
"\n\t\t\n\t\t\n\t",
},
OtherElem: []XMLElement{
{
XMLName: xml.Name{Space: "google:mobile:data", Local: "gcm"},
Attr: []xml.Attr{{Name: xml.Name{Space: "", Local: "xmlns"}, Value: "google:mobile:data"}},
InnerXML: "\n\t\t{\"random\": \"<text>\"}\n\t",
},
{
XMLName: xml.Name{Space: "jabber:client", Local: "error"},
Attr: []xml.Attr{{Name: xml.Name{Space: "", Local: "code"}, Value: "400"}, {Name: xml.Name{Space: "", Local: "type"}, Value: "modify"}},
InnerXML: `
InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
`,
},
},
}
if !reflect.DeepEqual(v, chat) {
t.Errorf("Recv() = %#v; want %#v", v, chat)
}
}
func TestEOFError(t *testing.T) {
var c Client
c.conn = tConnect("")
c.p = xml.NewDecoder(c.conn)
_, err := c.Recv()
if err != io.EOF {
t.Errorf("Recv() did not return io.EOF on end of input stream")
}
}
var emptyPubSub = strings.TrimSpace(`
`)
func TestEmptyPubsub(t *testing.T) {
var c Client
c.itemsIDs = append(c.itemsIDs, "items3")
c.conn = tConnect(emptyPubSub)
c.p = xml.NewDecoder(c.conn)
m, err := c.Recv()
switch m.(type) {
case AvatarData:
if err == nil {
t.Errorf("Expected an error to be returned")
}
default:
t.Errorf("Recv() = %v", m)
t.Errorf("Expected a return value of AvatarData")
}
}
// https://xmpp.org/extensions/xep-0363.html#example-6
var exampleSlot = strings.TrimSpace(`
`)
func TestUploadSlot(t *testing.T) {
var c Client
// c.itemsIDs = append(c.itemsIDs, "step_03")
c.conn = tConnect(exampleSlot)
c.p = xml.NewDecoder(c.conn)
m, err := c.Recv()
if err != nil {
panic(err)
}
t.Logf("Recv() = %v", m)
switch m.(type) {
case Slot:
v, _ := m.(Slot)
if v.ID != "abcdef" {
t.Errorf("Invalid ID: %s", v.ID)
}
if v.Put.Url != "https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg" {
t.Errorf("Invalid PUT URL: %s", v.Put.Url)
}
if v.Get.Url != "https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg" {
t.Errorf("Invalid GET URL: %s", v.Get.Url)
}
foundAuthorization := false
foundCookie := false
for _, header := range v.Put.Headers {
if header.Name == "Authorization" && header.Value == "Basic Base64String==" {
foundAuthorization = true
continue
}
if header.Name == "Cookie" && header.Value == "foo=bar; user=romeo" {
foundCookie = true
continue
}
t.Errorf("Unknown header: %s: %s", header.Name, header.Value)
}
if !foundAuthorization {
t.Errorf("Authorization header not found")
}
if !foundCookie {
t.Errorf("Cookie header not found")
}
default:
t.Errorf("Recv() = %V", m)
t.Errorf("Expected a return value of Slot")
}
}
// https://xmpp.org/extensions/xep-0066.html#example-5
var exampleOOB = strings.TrimSpace(`
Yeah, but do you have a license to Jabber?
http://www.jabber.org/images/psa-license.jpg
`)
func TestChatOOB(t *testing.T) {
var c Client
c.conn = tConnect(exampleOOB)
c.p = xml.NewDecoder(c.conn)
m, err := c.Recv()
if err != nil {
panic(err)
}
t.Logf("Recv() = %v", m)
switch m.(type) {
case Chat:
v, _ := m.(Chat)
if v.Oob.Url != "http://www.jabber.org/images/psa-license.jpg" {
t.Errorf("Wrong URL, found: `%s`", v.Oob.Url)
}
if v.Oob.Desc != "" {
t.Errorf("Should not find Desc: `%s`", v.Oob.Desc)
}
default:
t.Errorf("Recv() = %v", m)
t.Errorf("Expected a return value of AvatarData")
}
}
var exampleNoOOB = strings.TrimSpace(`
Yeah, but do you have a license to Jabber?
`)
func TestChatNoOOB(t *testing.T) {
var c Client
c.conn = tConnect(exampleNoOOB)
c.p = xml.NewDecoder(c.conn)
m, err := c.Recv()
if err != nil {
panic(err)
}
t.Logf("Recv() = %v", m)
switch m.(type) {
case Chat:
v, _ := m.(Chat)
if v.Oob.Url != "" {
t.Errorf("Should not find URL: `%s`", v.Oob.Url)
}
if v.Oob.Desc != "" {
t.Errorf("Should not find Desc: `%s`", v.Oob.Desc)
}
default:
t.Errorf("Recv() = %v", m)
t.Errorf("Expected a return value of AvatarData")
}
}
var rawOob = strings.TrimSpace(`
http://www.jabber.org/images/psa-license.jpg
`)
func TestRawOob(t *testing.T) {
var s Oob
err := xml.Unmarshal([]byte(rawOob), &s)
if err != nil {
t.Errorf("%v", err)
}
if s.Url != "http://www.jabber.org/images/psa-license.jpg" {
t.Errorf("Wrong URL: %s", s.Url)
}
}