├── .travis.yml
├── LICENSE
├── README.md
├── _example
├── example-gui.go
└── example.go
├── go.mod
├── go.sum
├── xmpp.go
├── xmpp_avatar.go
├── xmpp_disco.go
├── xmpp_error.go
├── xmpp_information_query.go
├── xmpp_muc.go
├── xmpp_ping.go
├── xmpp_pubsub.go
├── xmpp_subscription.go
└── xmpp_test.go
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - tip
4 | script:
5 | - go test
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 The Go Authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google Inc. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | go-xmpp
2 | =======
3 |
4 | go xmpp library (original was written by russ cox )
5 |
6 | [Documentation](https://godoc.org/github.com/xmppo/go-xmpp)
7 |
--------------------------------------------------------------------------------
/_example/example-gui.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "log"
6 | "os"
7 | "strings"
8 |
9 | "github.com/mattn/go-gtk/gtk"
10 | "github.com/xmppo/go-xmpp"
11 | )
12 |
13 | func main() {
14 | gtk.Init(&os.Args)
15 |
16 | window := gtk.NewWindow(gtk.WINDOW_TOPLEVEL)
17 | window.SetTitle("GoTalk")
18 | window.Connect("destroy", func() {
19 | gtk.MainQuit()
20 | })
21 | vbox := gtk.NewVBox(false, 1)
22 | scrolledwin := gtk.NewScrolledWindow(nil, nil)
23 | textview := gtk.NewTextView()
24 | textview.SetEditable(false)
25 | textview.SetCursorVisible(false)
26 | scrolledwin.Add(textview)
27 | vbox.Add(scrolledwin)
28 |
29 | buffer := textview.GetBuffer()
30 |
31 | entry := gtk.NewEntry()
32 | vbox.PackEnd(entry, false, false, 0)
33 |
34 | window.Add(vbox)
35 | window.SetSizeRequest(300, 400)
36 | window.ShowAll()
37 |
38 | dialog := gtk.NewDialog()
39 | dialog.SetTitle(window.GetTitle())
40 | sgroup := gtk.NewSizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
41 |
42 | hbox := gtk.NewHBox(false, 1)
43 | dialog.GetVBox().Add(hbox)
44 | label := gtk.NewLabel("username:")
45 | sgroup.AddWidget(label)
46 | hbox.Add(label)
47 | username := gtk.NewEntry()
48 | hbox.Add(username)
49 |
50 | hbox = gtk.NewHBox(false, 1)
51 | dialog.GetVBox().Add(hbox)
52 | label = gtk.NewLabel("password:")
53 | sgroup.AddWidget(label)
54 | hbox.Add(label)
55 | password := gtk.NewEntry()
56 | password.SetVisibility(false)
57 | hbox.Add(password)
58 |
59 | dialog.AddButton(gtk.STOCK_OK, gtk.RESPONSE_OK)
60 | dialog.AddButton(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
61 | dialog.SetDefaultResponse(gtk.RESPONSE_OK)
62 | dialog.SetTransientFor(window)
63 | dialog.ShowAll()
64 | res := dialog.Run()
65 | username_ := username.GetText()
66 | password_ := password.GetText()
67 | dialog.Destroy()
68 | if res != gtk.RESPONSE_OK {
69 | os.Exit(0)
70 | }
71 |
72 | xmpp.DefaultConfig = tls.Config{
73 | ServerName: "talk.google.com",
74 | InsecureSkipVerify: false,
75 | }
76 |
77 | talk, err := xmpp.NewClient("talk.google.com:443", username_, password_, false)
78 | if err != nil {
79 | log.Fatal(err)
80 | }
81 |
82 | entry.Connect("activate", func() {
83 | text := entry.GetText()
84 | tokens := strings.SplitN(text, " ", 2)
85 | if len(tokens) == 2 {
86 | func() {
87 | defer recover()
88 | talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
89 | entry.SetText("")
90 | }()
91 | }
92 | })
93 |
94 | go func() {
95 | for {
96 | func() {
97 | defer recover()
98 | chat, err := talk.Recv()
99 | if err != nil {
100 | log.Fatal(err)
101 | }
102 |
103 | var iter gtk.TextIter
104 | buffer.GetStartIter(&iter)
105 | if msg, ok := chat.(xmpp.Chat); ok {
106 | buffer.Insert(&iter, msg.Remote+": "+msg.Text+"\n")
107 | }
108 | }()
109 | }
110 | }()
111 |
112 | gtk.Main()
113 | }
114 |
--------------------------------------------------------------------------------
/_example/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "flag"
7 | "fmt"
8 | "log"
9 | "os"
10 | "strings"
11 |
12 | "github.com/xmppo/go-xmpp"
13 | )
14 |
15 | var (
16 | server = flag.String("server", "talk.google.com:443", "server")
17 | username = flag.String("username", "", "username")
18 | password = flag.String("password", "", "password")
19 | status = flag.String("status", "xa", "status")
20 | statusMessage = flag.String("status-msg", "I for one welcome our new codebot overlords.", "status message")
21 | notls = flag.Bool("notls", false, "No TLS")
22 | debug = flag.Bool("debug", false, "debug output")
23 | session = flag.Bool("session", false, "use server session")
24 | )
25 |
26 | func serverName(host string) string {
27 | return strings.Split(host, ":")[0]
28 | }
29 |
30 | func main() {
31 | flag.Usage = func() {
32 | fmt.Fprintf(os.Stderr, "usage: example [options]\n")
33 | flag.PrintDefaults()
34 | os.Exit(2)
35 | }
36 | flag.Parse()
37 | if *username == "" || *password == "" {
38 | if *debug && *username == "" && *password == "" {
39 | fmt.Fprintf(os.Stderr, "no username or password were given; attempting ANONYMOUS auth\n")
40 | } else if *username != "" || *password != "" {
41 | flag.Usage()
42 | }
43 | }
44 |
45 | if !*notls {
46 | xmpp.DefaultConfig = tls.Config{
47 | ServerName: serverName(*server),
48 | InsecureSkipVerify: false,
49 | }
50 | }
51 |
52 | var talk *xmpp.Client
53 | var err error
54 | options := xmpp.Options{
55 | Host: *server,
56 | User: *username,
57 | Password: *password,
58 | NoTLS: *notls,
59 | Debug: *debug,
60 | Session: *session,
61 | Status: *status,
62 | StatusMessage: *statusMessage,
63 | }
64 |
65 | talk, err = options.NewClient()
66 | if err != nil {
67 | log.Fatal(err)
68 | }
69 |
70 | go func() {
71 | for {
72 | chat, err := talk.Recv()
73 | if err != nil {
74 | log.Fatal(err)
75 | }
76 | switch v := chat.(type) {
77 | case xmpp.Chat:
78 | fmt.Println(v.Remote, v.Text)
79 | case xmpp.Presence:
80 | fmt.Println(v.From, v.Show)
81 | }
82 | }
83 | }()
84 | for {
85 | in := bufio.NewReader(os.Stdin)
86 | line, err := in.ReadString('\n')
87 | if err != nil {
88 | continue
89 | }
90 | line = strings.TrimRight(line, "\n")
91 |
92 | tokens := strings.SplitN(line, " ", 2)
93 | if len(tokens) == 2 {
94 | talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/xmppo/go-xmpp
2 |
3 | go 1.24
4 |
5 | require golang.org/x/net v0.41.0
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
2 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
3 |
--------------------------------------------------------------------------------
/xmpp.go:
--------------------------------------------------------------------------------
1 | // Copyright 2011 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // TODO(rsc):
6 | // More precise error handling.
7 | // Presence functionality.
8 | // TODO(mattn):
9 | // Add proxy authentication.
10 |
11 | // Package xmpp implements a simple Google Talk client
12 | // using the XMPP protocol described in RFC 3920 and RFC 3921.
13 | package xmpp
14 |
15 | import (
16 | "bufio"
17 | "bytes"
18 | "crypto/hmac"
19 | "crypto/pbkdf2"
20 | "crypto/rand"
21 | "crypto/sha1"
22 | "crypto/sha256"
23 | "crypto/sha512"
24 | "crypto/tls"
25 | "crypto/x509"
26 | "encoding/base64"
27 | "encoding/binary"
28 | "encoding/xml"
29 | "errors"
30 | "fmt"
31 | "hash"
32 | "io"
33 | "math/big"
34 | "net"
35 | "net/http"
36 | "net/url"
37 | "os"
38 | "regexp"
39 | "slices"
40 | "strconv"
41 | "strings"
42 | "sync"
43 | "time"
44 |
45 | "golang.org/x/net/proxy"
46 | )
47 |
48 | const (
49 | Version = "0.2.15-dev"
50 | nsStream = "http://etherx.jabber.org/streams"
51 | nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
52 | nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
53 | nsSASL2 = "urn:xmpp:sasl:2"
54 | nsSASLUpgrade = "urn:xmpp:sasl:upgrade:0"
55 | nsSCRAMUpgrade = "urn:xmpp:scram-upgrade:0"
56 | nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
57 | nsBind2 = "urn:xmpp:bind:0"
58 | nsFast = "urn:xmpp:fast:0"
59 | nsSASLCB = "urn:xmpp:sasl-cb:0"
60 | nsClient = "jabber:client"
61 | nsSession = "urn:ietf:params:xml:ns:xmpp-session"
62 | nsStreamLimits = "urn:xmpp:stream-limits:0"
63 | scramSHA1 = "SCRAM-SHA-1"
64 | scramSHA1Plus = "SCRAM-SHA-1-PLUS"
65 | scramSHA256 = "SCRAM-SHA-256"
66 | scramSHA256Plus = "SCRAM-SHA-256-PLUS"
67 | scramSHA512 = "SCRAM-SHA-512"
68 | scramSHA512Plus = "SCRAM-SHA-512-PLUS"
69 | scramUpSHA256 = "UPGR-SCRAM-SHA-256"
70 | scramUpSHA512 = "UPGR-SCRAM-SHA-512"
71 | htSHA256Expr = "HT-SHA-256-EXPR"
72 | htSHA256Uniq = "HT-SHA-256-UNIQ"
73 | htSHA256Endp = "HT-SHA-256-ENDP"
74 | htSHA256None = "HT-SHA-256-NONE"
75 | )
76 |
77 | // Default TLS configuration options
78 | var DefaultConfig = &tls.Config{}
79 |
80 | // DebugWriter is the writer used to write debugging output to.
81 | type debugWriter struct {
82 | w io.Writer
83 | prefix string
84 | }
85 |
86 | func (d debugWriter) Write(p []byte) (int, error) {
87 | nl := []byte("\n")
88 | switch {
89 | case len(p) == 0:
90 | return 0, nil
91 | case string(p) == "":
92 | return len(p), nil
93 | case string(p) == "\n":
94 | return len(p), nil
95 | }
96 | data := append([]byte(d.prefix), p...)
97 | if !strings.HasSuffix(string(p), "\n") {
98 | data = append(data, nl...)
99 | }
100 | n, err := d.w.Write(data)
101 | if err != nil {
102 | return n, err
103 | }
104 | if n != len(data) {
105 | return n, io.ErrShortWrite
106 | }
107 | return len(p), nil
108 | }
109 |
110 | // Cookie is a unique XMPP session identifier
111 | type Cookie uint64
112 |
113 | func getCookie() Cookie {
114 | var buf [8]byte
115 | if _, err := rand.Reader.Read(buf[:]); err != nil {
116 | panic("Failed to read random bytes: " + err.Error())
117 | }
118 | return Cookie(binary.LittleEndian.Uint64(buf[:]))
119 | }
120 |
121 | func getUUIDv4() string {
122 | return strconv.FormatUint(uint64(getCookie()), 10)
123 | }
124 |
125 | // Fast holds the XEP-0484 fast token, mechanism and expiry date
126 | type Fast struct {
127 | Token string
128 | Mechanism string
129 | Expiry time.Time
130 | }
131 |
132 | // Client holds XMPP connection options
133 | type Client struct {
134 | conn net.Conn // connection to server
135 | jid string // Jabber ID for our connection
136 | domain string
137 | nextMutex sync.Mutex // Mutex to prevent multiple access to xml.Decoder
138 | shutdown bool // Variable signalling that the stream will be closed
139 | p *xml.Decoder
140 | stanzaWriter io.Writer
141 | subIDs []string // IDs of subscription stanzas
142 | unsubIDs []string // IDs of unsubscription stanzas
143 | itemsIDs []string // IDs of item requests
144 | periodicPings bool // Send periodic server pings.
145 | periodicPingTicker *time.Ticker // Ticker for periodic pings.
146 | periodicPingPeriod time.Duration // Period for periodic ping ticker.
147 | LimitMaxBytes int // Maximum stanza size (XEP-0478: Stream Limits Advertisement)
148 | LimitIdleSeconds int // Maximum idle seconds (XEP-0478: Stream Limits Advertisement)
149 | Mechanism string // SCRAM mechanism used.
150 | Fast Fast // XEP-0484 FAST Token, mechanism and expiry.
151 | }
152 |
153 | func (c *Client) JID() string {
154 | return c.jid
155 | }
156 |
157 | func containsIgnoreCase(s, substr string) bool {
158 | s, substr = strings.ToUpper(s), strings.ToUpper(substr)
159 | return strings.Contains(s, substr)
160 | }
161 |
162 | func connect(host string, user string, timeout time.Duration) (net.Conn, error) {
163 | addr := host
164 |
165 | if strings.TrimSpace(host) == "" {
166 | a := strings.SplitN(user, "@", 2)
167 | if len(a) == 2 {
168 | addr = a[1]
169 | }
170 | }
171 | a := strings.SplitN(host, ":", 2)
172 | if len(a) == 1 {
173 | addr += ":5222"
174 | }
175 |
176 | http_proxy := os.Getenv("HTTP_PROXY")
177 | if http_proxy == "" {
178 | http_proxy = os.Getenv("http_proxy")
179 | }
180 | // test for no proxy, takes a comma separated list with substrings to match
181 | if http_proxy != "" {
182 | noproxy := os.Getenv("NO_PROXY")
183 | if noproxy == "" {
184 | noproxy = os.Getenv("no_proxy")
185 | }
186 | if noproxy != "" {
187 | nplist := strings.Split(noproxy, ",")
188 | for _, s := range nplist {
189 | if containsIgnoreCase(addr, s) {
190 | http_proxy = ""
191 | break
192 | }
193 | }
194 | }
195 | }
196 | socks5Target, socks5 := strings.CutPrefix(http_proxy, "socks5://")
197 | if http_proxy != "" && !socks5 {
198 | url, err := url.Parse(http_proxy)
199 | if err == nil {
200 | addr = url.Host
201 | }
202 | }
203 | var c net.Conn
204 | var err error
205 | if socks5 {
206 | dialer, err := proxy.SOCKS5("tcp", socks5Target, nil, nil)
207 | if err != nil {
208 | return nil, err
209 | }
210 | c, err = dialer.Dial("tcp", addr)
211 | if err != nil {
212 | return nil, err
213 | }
214 | } else {
215 | c, err = net.DialTimeout("tcp", addr, timeout)
216 | if err != nil {
217 | return nil, err
218 | }
219 | }
220 |
221 | if http_proxy != "" && !socks5 {
222 | fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\n", host)
223 | fmt.Fprintf(c, "Host: %s\r\n", host)
224 | fmt.Fprintf(c, "\r\n")
225 | br := bufio.NewReader(c)
226 | req, _ := http.NewRequest("CONNECT", host, nil)
227 | resp, err := http.ReadResponse(br, req)
228 | if err != nil {
229 | return nil, err
230 | }
231 | if resp.StatusCode != 200 {
232 | f := strings.SplitN(resp.Status, " ", 2)
233 | return nil, errors.New(f[1])
234 | }
235 | }
236 | return c, nil
237 | }
238 |
239 | // Options are used to specify additional options for new clients, such as a Resource.
240 | type Options struct {
241 | // Host specifies what host to connect to, as either "hostname" or "hostname:port"
242 | // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
243 | // Default the port to 5222.
244 | Host string
245 |
246 | // User specifies what user to authenticate to the remote server.
247 | User string
248 |
249 | // Password supplies the password to use for authentication with the remote server.
250 | Password string
251 |
252 | // DialTimeout is the time limit for establishing a connection. A
253 | // DialTimeout of zero means no timeout.
254 | DialTimeout time.Duration
255 |
256 | // Resource specifies an XMPP client resource, like "bot", instead of accepting one
257 | // from the server. Use "" to let the server generate one for your client.
258 | Resource string
259 |
260 | // OAuthScope provides go-xmpp the required scope for OAuth2 authentication.
261 | OAuthScope string
262 |
263 | // OAuthToken provides go-xmpp with the required OAuth2 token used to authenticate
264 | OAuthToken string
265 |
266 | // OAuthXmlNs provides go-xmpp with the required namespaced used for OAuth2 authentication. This is
267 | // provided to the server as the xmlns:auth attribute of the OAuth2 authentication request.
268 | OAuthXmlNs string
269 |
270 | // TLS Config
271 | TLSConfig *tls.Config
272 |
273 | // InsecureAllowUnencryptedAuth permits authentication over a TCP connection that has not been promoted to
274 | // TLS by STARTTLS; this could leak authentication information over the network, or permit man in the middle
275 | // attacks.
276 | InsecureAllowUnencryptedAuth bool
277 |
278 | // NoTLS directs go-xmpp to not use TLS initially to contact the server; instead, a plain old unencrypted
279 | // TCP connection should be used. (Can be combined with StartTLS to support STARTTLS-based servers.)
280 | NoTLS bool
281 |
282 | // StartTLS directs go-xmpp to STARTTLS if the server supports it; go-xmpp will automatically STARTTLS
283 | // if the server requires it regardless of this option.
284 | StartTLS bool
285 |
286 | // Debug output
287 | Debug bool
288 |
289 | // DebugWriter specifies where the debug output is written to
290 | DebugWriter io.Writer
291 |
292 | // Use server sessions
293 | Session bool
294 |
295 | // Presence Status
296 | Status string
297 |
298 | // Status message
299 | StatusMessage string
300 |
301 | // Auth mechanism to use
302 | Mechanism string
303 |
304 | // XEP-0474: SASL SCRAM Downgrade Protection
305 | SSDP bool
306 |
307 | // XEP-0388: Extensible SASL Profile
308 | // Value for software
309 | UserAgentSW string
310 |
311 | // XEP-0388: XEP-0388: Extensible SASL Profile
312 | // Value for device
313 | UserAgentDev string
314 |
315 | // XEP-0388: Extensible SASL Profile
316 | // Unique stable identifier for the client installation
317 | // MUST be a valid UUIDv4
318 | UserAgentID string
319 |
320 | // Enable XEP-0484: Fast Authentication Streamlining Tokens
321 | Fast bool
322 |
323 | // XEP-0484: Fast Authentication Streamlining Tokens
324 | // Fast Token
325 | FastToken string
326 |
327 | // XEP-0484: Fast Authentication Streamlining Tokens
328 | // Fast Mechanism
329 | FastMechanism string
330 |
331 | // XEP-0484: Fast Authentication Streamlining Tokens
332 | // Invalidate the current token
333 | FastInvalidate bool
334 |
335 | // NoPLAIN forbids authentication using plain passwords
336 | NoPLAIN bool
337 |
338 | // NoSASLUpgrade disables XEP-0480 upgrades.
339 | NoSASLUpgrade bool
340 |
341 | // Send periodic XEP-0199 pings to the server.
342 | PeriodicServerPings bool
343 |
344 | // Period of inactivity after which the client sends a XEP-0199 ping
345 | // to the server. Specified in milliseconds, defaults to 20.000 (20 seconds).
346 | PeriodicServerPingsPeriod int
347 | }
348 |
349 | // NewClient establishes a new Client connection based on a set of Options.
350 | func (o Options) NewClient() (*Client, error) {
351 | host := o.Host
352 | if strings.TrimSpace(host) == "" {
353 | a := strings.SplitN(o.User, "@", 2)
354 | if len(a) == 2 {
355 | if _, addrs, err := net.LookupSRV("xmpp-client", "tcp", a[1]); err == nil {
356 | if len(addrs) > 0 {
357 | // default to first record
358 | host = fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port)
359 | defP := addrs[0].Priority
360 | for _, adr := range addrs {
361 | if adr.Priority < defP {
362 | host = fmt.Sprintf("%s:%d", adr.Target, adr.Port)
363 | defP = adr.Priority
364 | }
365 | }
366 | } else {
367 | host = a[1]
368 | }
369 | } else {
370 | host = a[1]
371 | }
372 | }
373 | }
374 | c, err := connect(host, o.User, o.DialTimeout)
375 | if err != nil {
376 | return nil, err
377 | }
378 |
379 | if strings.LastIndex(host, ":") > 0 {
380 | host = host[:strings.LastIndex(host, ":")]
381 | }
382 |
383 | client := new(Client)
384 | if o.NoTLS {
385 | client.conn = c
386 | } else {
387 | var tlsconn *tls.Conn
388 | if o.TLSConfig != nil {
389 | tlsconn = tls.Client(c, o.TLSConfig)
390 | host = o.TLSConfig.ServerName
391 | } else {
392 | newconfig := DefaultConfig.Clone()
393 | newconfig.ServerName = host
394 | tlsconn = tls.Client(c, newconfig)
395 | }
396 | if err = tlsconn.Handshake(); err != nil {
397 | return nil, err
398 | }
399 | insecureSkipVerify := DefaultConfig.InsecureSkipVerify
400 | if o.TLSConfig != nil {
401 | insecureSkipVerify = o.TLSConfig.InsecureSkipVerify
402 | }
403 | if !insecureSkipVerify {
404 | if err = tlsconn.VerifyHostname(host); err != nil {
405 | return nil, err
406 | }
407 | }
408 | client.conn = tlsconn
409 | }
410 |
411 | if err := client.init(&o); err != nil {
412 | return nil, err
413 | }
414 |
415 | if o.PeriodicServerPings {
416 | client.periodicPings = true
417 | // Set periodic pings period to 20 seconds if not specified.
418 | if o.PeriodicServerPingsPeriod == 0 {
419 | client.periodicPingPeriod = time.Duration(20000 * time.Millisecond)
420 | } else {
421 | client.periodicPingPeriod = time.Duration(o.PeriodicServerPingsPeriod) * time.Millisecond
422 | }
423 | client.periodicPingTicker = time.NewTicker(client.periodicPingPeriod)
424 | // Start sending periodic pings
425 | go client.sendPeriodicPings()
426 | }
427 |
428 | return client, nil
429 | }
430 |
431 | // NewClient creates a new connection to a host given as "hostname" or "hostname:port".
432 | // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID.
433 | // Default the port to 5222.
434 | func NewClient(host, user, passwd string, debug bool) (*Client, error) {
435 | opts := Options{
436 | Host: host,
437 | User: user,
438 | Password: passwd,
439 | Debug: debug,
440 | Session: false,
441 | }
442 | return opts.NewClient()
443 | }
444 |
445 | // NewClientNoTLS creates a new client without TLS
446 | func NewClientNoTLS(host, user, passwd string, debug bool) (*Client, error) {
447 | opts := Options{
448 | Host: host,
449 | User: user,
450 | Password: passwd,
451 | NoTLS: true,
452 | Debug: debug,
453 | Session: false,
454 | }
455 | return opts.NewClient()
456 | }
457 |
458 | // Close closes the XMPP connection
459 | func (c *Client) Close() error {
460 | c.shutdown = true
461 | if c.periodicPings {
462 | c.periodicPingTicker.Stop()
463 | }
464 | if c.conn != (*tls.Conn)(nil) {
465 | fmt.Fprintf(c.stanzaWriter, "\n")
466 | go func() {
467 | <-time.After(10 * time.Second)
468 | c.conn.Close()
469 | }()
470 | // Wait for the server also closing the stream.
471 | for {
472 | ee, err := c.nextEnd()
473 | // If the server already closed the stream it is
474 | // likely to receive an error when trying to parse
475 | // the stream. Therefore the connection is also closed
476 | // if an error is received.
477 | if err != nil {
478 | return c.conn.Close()
479 | }
480 | if ee.Name.Local == "stream" {
481 | return c.conn.Close()
482 | }
483 | }
484 | }
485 | return nil
486 | }
487 |
488 | func cnonce() string {
489 | randSize := big.NewInt(0)
490 | randSize.Lsh(big.NewInt(1), 64)
491 | cn, err := rand.Int(rand.Reader, randSize)
492 | if err != nil {
493 | return ""
494 | }
495 | return fmt.Sprintf("%016x", cn)
496 | }
497 |
498 | func (c *Client) init(o *Options) error {
499 | var domain string
500 | var user string
501 | a := strings.SplitN(o.User, "@", 2)
502 | // Check if User is not empty. Otherwise, we'll be attempting ANONYMOUS with Host domain.
503 | switch {
504 | case len(o.User) > 0:
505 | switch len(a) {
506 | case 1:
507 | // Allow it to specify the domain as username for ANONYMOUS authentication.
508 | // Otherwise connection fails if the connection target differs from the server
509 | // name
510 | domain = o.User
511 | user = ""
512 | o.User = ""
513 | case 2:
514 | user = a[0]
515 | domain = a[1]
516 | }
517 | default:
518 | domain = o.Host
519 | }
520 | if strings.Contains(domain, ":") {
521 | domain = strings.SplitN(domain, ":", 2)[0]
522 | }
523 |
524 | // Declare intent to be a jabber client and gather stream features.
525 | f, err := c.startStream(o, domain)
526 | if err != nil {
527 | return err
528 | }
529 | // Make the max. stanza size limit available.
530 | if f.Limits.MaxBytes != "" {
531 | c.LimitMaxBytes, err = strconv.Atoi(f.Limits.MaxBytes)
532 | if err != nil {
533 | c.LimitMaxBytes = 0
534 | }
535 | }
536 | // Make the servers time limit after which it might consider the stream idle available.
537 | if f.Limits.IdleSeconds != "" {
538 | c.LimitIdleSeconds, err = strconv.Atoi(f.Limits.IdleSeconds)
539 | if err != nil {
540 | c.LimitIdleSeconds = 0
541 | }
542 | }
543 |
544 | // If the server requires STARTTLS, attempt to do so.
545 | if f, err = c.startTLSIfRequired(f, o, domain); err != nil {
546 | return err
547 | }
548 | var mechanism, channelBinding, clientFirstMessage, clientFinalMessageBare, authMessage string
549 | var bind2Data, resource, userAgentSW, userAgentDev, userAgentID, fastAuth, saslUpgrade string
550 | var saslUpgradeMech string
551 | var serverSignature, keyingMaterial, successMsg []byte
552 | var scramPlus, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool
553 | var cbsSlice, mechSlice, upgrSlice []string
554 | var tlsConn *tls.Conn
555 | // Use SASL2 if available
556 | if f.Authentication.Mechanism != nil && c.IsEncrypted() {
557 | sasl2 = true
558 | mechSlice = f.Authentication.Mechanism
559 | // Detect whether bind2 is available
560 | if f.Authentication.Inline.Bind.Xmlns != "" {
561 | bind2 = true
562 | }
563 | } else {
564 | mechSlice = f.Mechanisms.Mechanism
565 | }
566 | if o.User == "" && o.Password == "" {
567 | foundAnonymous := false
568 | for _, m := range mechSlice {
569 | if m == "ANONYMOUS" {
570 | mechanism = m
571 | if sasl2 {
572 | if bind2 {
573 | if o.UserAgentSW != "" {
574 | resource = o.UserAgentSW
575 | } else {
576 | resource = "go-xmpp"
577 | }
578 | bind2Data = fmt.Sprintf("%s",
579 | nsBind2, resource)
580 | }
581 | if o.UserAgentSW != "" {
582 | userAgentSW = fmt.Sprintf("%s", o.UserAgentSW)
583 | } else {
584 | userAgentSW = "go-xmpp"
585 | }
586 | if o.UserAgentDev != "" {
587 | userAgentDev = fmt.Sprintf("%s", o.UserAgentDev)
588 | }
589 | if o.UserAgentID != "" {
590 | userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID)
591 | }
592 | fmt.Fprintf(c.stanzaWriter,
593 | "%s%s%s%s\n",
594 | nsSASL2, mechanism, userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
595 | } else {
596 | fmt.Fprintf(c.stanzaWriter, "\n", nsSASL)
597 | }
598 | foundAnonymous = true
599 | break
600 | }
601 | }
602 | if !foundAnonymous {
603 | return fmt.Errorf("ANONYMOUS authentication is not an option and username and password were not specified")
604 | }
605 | } else {
606 | // Even digest forms of authentication are unsafe if we do not know that the host
607 | // we are talking to is the actual server, and not a man in the middle playing
608 | // proxy.
609 | if !c.IsEncrypted() && !o.InsecureAllowUnencryptedAuth {
610 | return errors.New("refusing to authenticate over unencrypted TCP connection")
611 | }
612 |
613 | tlsConn, ok = c.conn.(*tls.Conn)
614 | if ok {
615 | tlsConnOK = true
616 | }
617 | mechanism = ""
618 | if o.Mechanism != "" {
619 | if slices.Contains(mechSlice, o.Mechanism) {
620 | mechanism = o.Mechanism
621 | }
622 | } else {
623 | switch {
624 | case slices.Contains(mechSlice, scramSHA512Plus) && tlsConnOK:
625 | mechanism = scramSHA512Plus
626 | case slices.Contains(mechSlice, scramSHA256Plus) && tlsConnOK:
627 | mechanism = scramSHA256Plus
628 | case slices.Contains(mechSlice, scramSHA1Plus) && tlsConnOK:
629 | mechanism = scramSHA1Plus
630 | case slices.Contains(mechSlice, scramSHA512):
631 | mechanism = scramSHA512
632 | case slices.Contains(mechSlice, scramSHA256):
633 | mechanism = scramSHA256
634 | case slices.Contains(mechSlice, scramSHA1):
635 | mechanism = scramSHA1
636 | case slices.Contains(mechSlice, "X-OAUTH2"):
637 | mechanism = "X-OAUTH2"
638 | // Do not use PLAIN auth if NoPlain is set.
639 | case slices.Contains(mechSlice, "PLAIN") && tlsConnOK && !o.NoPLAIN:
640 | mechanism = "PLAIN"
641 | }
642 | }
643 | if strings.HasPrefix(mechanism, "SCRAM-SHA") {
644 | if strings.HasSuffix(mechanism, "PLUS") {
645 | scramPlus = true
646 | }
647 | for _, cbs := range f.ChannelBindings.ChannelBinding {
648 | cbsSlice = append(cbsSlice, cbs.Type)
649 | }
650 | if scramPlus {
651 | tlsState := tlsConn.ConnectionState()
652 | switch tlsState.Version {
653 | case tls.VersionTLS13:
654 | tls13 = true
655 | if slices.Contains(cbsSlice, "tls-server-end-point") && !slices.Contains(cbsSlice, "tls-exporter") {
656 | serverEndPoint = true
657 | } else {
658 | keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32)
659 | if err != nil {
660 | return err
661 | }
662 | }
663 | case tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12:
664 | if slices.Contains(cbsSlice, "tls-server-end-point") && !slices.Contains(cbsSlice, "tls-unique") {
665 | serverEndPoint = true
666 | } else {
667 | keyingMaterial = tlsState.TLSUnique
668 | }
669 | default:
670 | return errors.New(mechanism + ": unknown TLS version")
671 | }
672 | if serverEndPoint {
673 | var h hash.Hash
674 | // This material is not necessary for `tls-server-end-point` binding, but it is required to check that
675 | // the TLS connection was not renegotiated. This function will fail if that's the case (see
676 | // https://pkg.go.dev/crypto/tls#ConnectionState.ExportKeyingMaterial
677 | _, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32)
678 | if err != nil {
679 | return err
680 | }
681 | switch tlsState.PeerCertificates[0].SignatureAlgorithm {
682 | case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.ECDSAWithSHA1,
683 | x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS:
684 | h = sha256.New()
685 | case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
686 | h = sha512.New384()
687 | case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS:
688 | h = sha512.New()
689 | }
690 | h.Write(tlsState.PeerCertificates[0].Raw)
691 | keyingMaterial = h.Sum(nil)
692 | h.Reset()
693 | }
694 | if len(keyingMaterial) == 0 {
695 | return errors.New(mechanism + ": no keying material")
696 | }
697 | switch {
698 | case tls13 && !serverEndPoint:
699 | channelBinding = base64.StdEncoding.EncodeToString(slices.Concat([]byte("p=tls-exporter,,"), keyingMaterial))
700 | case serverEndPoint:
701 | channelBinding = base64.StdEncoding.EncodeToString(slices.Concat([]byte("p=tls-server-end-point,,"), keyingMaterial))
702 | default:
703 | channelBinding = base64.StdEncoding.EncodeToString(slices.Concat([]byte("p=tls-unique,,"), keyingMaterial))
704 | }
705 | }
706 | var shaNewFn func() hash.Hash
707 | switch mechanism {
708 | case scramSHA512, scramSHA512Plus:
709 | shaNewFn = sha512.New
710 | case scramSHA256, scramSHA256Plus:
711 | shaNewFn = sha256.New
712 | case scramSHA1, scramSHA1Plus:
713 | shaNewFn = sha1.New
714 | default:
715 | return errors.New("unsupported auth mechanism")
716 | }
717 | clientNonce := cnonce()
718 | if scramPlus {
719 | switch {
720 | case tls13 && !serverEndPoint:
721 | clientFirstMessage = "p=tls-exporter,,n=" + user + ",r=" + clientNonce
722 | case serverEndPoint:
723 | clientFirstMessage = "p=tls-server-end-point,,n=" + user + ",r=" + clientNonce
724 | default:
725 | clientFirstMessage = "p=tls-unique,,n=" + user + ",r=" + clientNonce
726 | }
727 | } else {
728 | clientFirstMessage = "n,,n=" + user + ",r=" + clientNonce
729 | }
730 | if sasl2 {
731 | if !o.NoSASLUpgrade {
732 | for _, um := range f.Authentication.Upgrade {
733 | upgrSlice = append(upgrSlice, um.Text)
734 | }
735 | switch {
736 | case slices.Contains(upgrSlice, scramUpSHA512):
737 | saslUpgradeMech = scramUpSHA512
738 | case slices.Contains(upgrSlice, scramUpSHA256):
739 | saslUpgradeMech = scramUpSHA256
740 | }
741 | if saslUpgradeMech != "" {
742 | saslUpgrade = fmt.Sprintf("%s",
743 | nsSASLUpgrade, saslUpgradeMech)
744 | }
745 | }
746 | if bind2 {
747 | if o.UserAgentSW != "" {
748 | resource = o.UserAgentSW
749 | } else {
750 | resource = "go-xmpp"
751 | }
752 | bind2Data = fmt.Sprintf("%s",
753 | nsBind2, resource)
754 | }
755 | if o.UserAgentSW != "" {
756 | userAgentSW = fmt.Sprintf("%s", o.UserAgentSW)
757 | } else {
758 | userAgentSW = "go-xmpp"
759 | }
760 | if o.UserAgentDev != "" {
761 | userAgentDev = fmt.Sprintf("%s", o.UserAgentDev)
762 | }
763 | if o.UserAgentID != "" {
764 | userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID)
765 | }
766 | if o.Fast && f.Authentication.Inline.Fast.Mechanism != nil && o.UserAgentID != "" && c.IsEncrypted() {
767 | var mech string
768 | if o.FastToken == "" {
769 | m := f.Authentication.Inline.Fast.Mechanism
770 | switch {
771 | case slices.Contains(m, htSHA256Expr) && tls13:
772 | mech = htSHA256Expr
773 | case slices.Contains(m, htSHA256Uniq) && !tls13:
774 | mech = htSHA256Uniq
775 | case slices.Contains(m, htSHA256Endp):
776 | mech = htSHA256Endp
777 | case slices.Contains(m, htSHA256None):
778 | mech = htSHA256None
779 | default:
780 | return fmt.Errorf("fast: unsupported auth mechanism %s", m)
781 | }
782 | fastAuth = fmt.Sprintf("", nsFast, mech)
783 | } else {
784 | var fastInvalidate string
785 | if o.FastInvalidate {
786 | fastInvalidate = " invalidate='true'"
787 | }
788 | fastAuth = fmt.Sprintf("", nsFast, fastInvalidate)
789 | tlsState := tlsConn.ConnectionState()
790 | mechanism = o.FastMechanism
791 | switch mechanism {
792 | case htSHA256Expr:
793 | if !tls13 {
794 | return fmt.Errorf("fast: %s can only be used when using TLSv1.3", htSHA256Expr)
795 | }
796 | keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32)
797 | if err != nil {
798 | return err
799 | }
800 | case htSHA256Uniq:
801 | if tls13 {
802 | return fmt.Errorf("fast: %s can not be used when using TLSv1.3", htSHA256Uniq)
803 | }
804 | keyingMaterial = tlsState.TLSUnique
805 | case htSHA256Endp:
806 | var h hash.Hash
807 | switch tlsState.PeerCertificates[0].SignatureAlgorithm {
808 | case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.ECDSAWithSHA1,
809 | x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS:
810 | h = sha256.New()
811 | case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
812 | h = sha512.New384()
813 | case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS:
814 | h = sha512.New()
815 | }
816 | h.Write(tlsState.PeerCertificates[0].Raw)
817 | keyingMaterial = h.Sum(nil)
818 | h.Reset()
819 | case htSHA256None:
820 | keyingMaterial = []byte("")
821 | default:
822 | return fmt.Errorf("fast: unsupported auth mechanism %s", mechanism)
823 | }
824 | h := hmac.New(sha256.New, []byte(o.FastToken))
825 | initiator := slices.Concat([]byte("Initiator"), keyingMaterial)
826 | _, err = h.Write(initiator)
827 | if err != nil {
828 | return err
829 | }
830 | initiatorHashedToken := h.Sum(nil)
831 | user := strings.Split(o.User, "@")[0]
832 | clientFirstMessage = user + "\x00" + string(initiatorHashedToken)
833 | }
834 | }
835 | fmt.Fprintf(c.stanzaWriter,
836 | "%s%s%s%s%s%s\n",
837 | nsSASL2, mechanism, saslUpgrade, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
838 | } else {
839 | fmt.Fprintf(c.stanzaWriter, "%s\n",
840 | nsSASL, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)))
841 | }
842 | var sfm string
843 | _, val, err := c.next()
844 | if err != nil {
845 | return err
846 | }
847 | switch v := val.(type) {
848 | case *sasl2Failure:
849 | errorMessage := v.Text
850 | if errorMessage == "" {
851 | // v.Any is type of sub-element in failure,
852 | // which gives a description of what failed if there was no text element
853 | errorMessage = v.Any.Local
854 | }
855 | return errors.New("auth failure: " + errorMessage)
856 | case *saslFailure:
857 | errorMessage := v.Text
858 | if errorMessage == "" {
859 | // v.Any is type of sub-element in failure,
860 | // which gives a description of what failed if there was no text element
861 | errorMessage = v.Any.Local
862 | }
863 | return errors.New("auth failure: " + errorMessage)
864 | case *sasl2Success:
865 | if strings.HasPrefix(mechanism, "SCRAM-SHA") {
866 | successMsg, err := base64.StdEncoding.DecodeString(v.AdditionalData)
867 | if err != nil {
868 | return err
869 | }
870 | if !strings.HasPrefix(string(successMsg), "v=") {
871 | return errors.New("server sent unexpected content in SCRAM success message")
872 | }
873 | c.Mechanism = mechanism
874 | }
875 | if strings.HasPrefix(mechanism, "HT-SHA") {
876 | // TODO: Check whether server implementations already support
877 | // https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html#section-3.3
878 | h := hmac.New(sha256.New, []byte(o.FastToken))
879 | responder := slices.Concat([]byte("Responder"), keyingMaterial)
880 | _, err = h.Write(responder)
881 | if err != nil {
882 | return err
883 | }
884 | responderMsgRcv, err := base64.StdEncoding.DecodeString(v.AdditionalData)
885 | if err != nil {
886 | return err
887 | }
888 | responderMsgCalc := h.Sum(nil)
889 | if string(responderMsgCalc) != string(responderMsgRcv) {
890 | return fmt.Errorf("server sent unexpected content in FAST success message")
891 | }
892 | c.Mechanism = mechanism
893 | }
894 | if bind2 {
895 | c.jid = v.AuthorizationIdentifier
896 | c.domain = domain
897 | }
898 | if v.Token.Token != "" && v.Token.Token != o.FastToken {
899 | m := f.Authentication.Inline.Fast.Mechanism
900 | switch {
901 | case slices.Contains(m, htSHA256Expr) && tls13:
902 | c.Fast.Mechanism = htSHA256Expr
903 | case slices.Contains(m, htSHA256Uniq) && !tls13:
904 | c.Fast.Mechanism = htSHA256Uniq
905 | case slices.Contains(m, htSHA256Endp):
906 | c.Fast.Mechanism = htSHA256Endp
907 | case slices.Contains(m, htSHA256None):
908 | c.Fast.Mechanism = htSHA256None
909 | }
910 | c.Fast.Token = v.Token.Token
911 | c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry)
912 | }
913 | if o.Session {
914 | // if server support session, open it
915 | cookie := getCookie() // generate new id value for session
916 | fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(domain), cookie, nsSession)
917 | }
918 |
919 | // We're connected and can now receive and send messages.
920 | fmt.Fprintf(c.stanzaWriter, "%s%s\n", o.Status, o.StatusMessage)
921 | return nil
922 | case *sasl2Challenge:
923 | sfm = v.Text
924 | case *saslChallenge:
925 | sfm = v.Text
926 | }
927 | b, err := base64.StdEncoding.DecodeString(sfm)
928 | if err != nil {
929 | return err
930 | }
931 | var serverNonce string
932 | var dgProtect, dgProtectSep, dgProtectCBSep []byte
933 | var salt []byte
934 | var iterations int
935 | for _, serverReply := range strings.Split(string(b), ",") {
936 | switch {
937 | case strings.HasPrefix(serverReply, "r="):
938 | serverNonce = strings.SplitN(serverReply, "=", 2)[1]
939 | if !strings.HasPrefix(serverNonce, clientNonce) {
940 | return errors.New("SCRAM: server nonce didn't start with client nonce")
941 | }
942 | case strings.HasPrefix(serverReply, "s="):
943 | salt, err = base64.StdEncoding.DecodeString(strings.SplitN(serverReply, "=", 2)[1])
944 | if err != nil {
945 | return err
946 | }
947 | if string(salt) == "" {
948 | return errors.New("SCRAM: server sent empty salt")
949 | }
950 | case strings.HasPrefix(serverReply, "i="):
951 | iterations, err = strconv.Atoi(strings.SplitN(serverReply,
952 | "=", 2)[1])
953 | if err != nil {
954 | return err
955 | }
956 | case (strings.HasPrefix(serverReply, "d=") || strings.HasPrefix(serverReply, "h=")) && o.SSDP:
957 | dgProtectSep = []byte{0x1e}
958 | dgProtectCBSep = []byte{0x1f}
959 | serverDgProtectHash := strings.SplitN(serverReply, "=", 2)[1]
960 | slices.Sort(f.Mechanisms.Mechanism)
961 | for _, mech := range f.Mechanisms.Mechanism {
962 | if len(dgProtect) == 0 {
963 | dgProtect = []byte(mech)
964 | } else {
965 | dgProtect = append(dgProtect, dgProtectSep...)
966 | dgProtect = append(dgProtect, []byte(mech)...)
967 | }
968 | }
969 | slices.Sort(cbsSlice)
970 | for i, cb := range cbsSlice {
971 | if i == 0 {
972 | dgProtect = append(dgProtect, dgProtectCBSep...)
973 | dgProtect = append(dgProtect, []byte(cb)...)
974 | } else {
975 | dgProtect = append(dgProtect, dgProtectSep...)
976 | dgProtect = append(dgProtect, []byte(cb)...)
977 | }
978 | }
979 | dgh := shaNewFn()
980 | dgh.Write(dgProtect)
981 | dHash := dgh.Sum(nil)
982 | dHashb64 := base64.StdEncoding.EncodeToString(dHash)
983 | if dHashb64 != serverDgProtectHash {
984 | return fmt.Errorf("SCRAM: downgrade protection hash mismatch, expected: %s (hash of %s), received: %s",
985 | dHashb64, dgProtect, serverDgProtectHash)
986 | }
987 | dgh.Reset()
988 | case strings.HasPrefix(serverReply, "m="):
989 | return errors.New("scram: server sent reserved 'm' attribute")
990 | }
991 | }
992 | if scramPlus {
993 | clientFinalMessageBare = "c=" + channelBinding + ",r=" + serverNonce
994 | } else {
995 | clientFinalMessageBare = "c=biws,r=" + serverNonce
996 | }
997 | saltedPassword, err := pbkdf2.Key(shaNewFn, o.Password, salt,
998 | iterations, shaNewFn().Size())
999 | if err != nil {
1000 | return err
1001 | }
1002 | h := hmac.New(shaNewFn, saltedPassword)
1003 | _, err = h.Write([]byte("Client Key"))
1004 | if err != nil {
1005 | return err
1006 | }
1007 | clientKey := h.Sum(nil)
1008 | h.Reset()
1009 | var storedKey []byte
1010 | switch mechanism {
1011 | case scramSHA512, scramSHA512Plus:
1012 | storedKey512 := sha512.Sum512(clientKey)
1013 | storedKey = storedKey512[:]
1014 | case scramSHA256, scramSHA256Plus:
1015 | storedKey256 := sha256.Sum256(clientKey)
1016 | storedKey = storedKey256[:]
1017 | case scramSHA1, scramSHA1Plus:
1018 | storedKey1 := sha1.Sum(clientKey)
1019 | storedKey = storedKey1[:]
1020 | }
1021 | _, err = h.Write([]byte("Server Key"))
1022 | if err != nil {
1023 | return err
1024 | }
1025 | serverFirstMessage, err := base64.StdEncoding.DecodeString(sfm)
1026 | if err != nil {
1027 | return err
1028 | }
1029 | authMessage = strings.SplitAfter(clientFirstMessage, ",,")[1] + "," +
1030 | string(serverFirstMessage) + "," + clientFinalMessageBare
1031 | h = hmac.New(shaNewFn, storedKey)
1032 | _, err = h.Write([]byte(authMessage))
1033 | if err != nil {
1034 | return err
1035 | }
1036 | clientSignature := h.Sum(nil)
1037 | h.Reset()
1038 | if len(clientKey) != len(clientSignature) {
1039 | return errors.New("SCRAM: client key and signature length mismatch")
1040 | }
1041 | clientProof := make([]byte, len(clientKey))
1042 | for i := range clientKey {
1043 | clientProof[i] = clientKey[i] ^ clientSignature[i]
1044 | }
1045 | h = hmac.New(shaNewFn, saltedPassword)
1046 | _, err = h.Write([]byte("Server Key"))
1047 | if err != nil {
1048 | return err
1049 | }
1050 | serverKey := h.Sum(nil)
1051 | h.Reset()
1052 | h = hmac.New(shaNewFn, serverKey)
1053 | _, err = h.Write([]byte(authMessage))
1054 | if err != nil {
1055 | return err
1056 | }
1057 | serverSignature = h.Sum(nil)
1058 | if string(serverSignature) == "" {
1059 | return errors.New("SCRAM: calculated an empty server signature")
1060 | }
1061 | clientFinalMessage := base64.StdEncoding.EncodeToString([]byte(clientFinalMessageBare +
1062 | ",p=" + base64.StdEncoding.EncodeToString(clientProof)))
1063 | if sasl2 {
1064 | fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL2,
1065 | clientFinalMessage)
1066 | } else {
1067 | fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL,
1068 | clientFinalMessage)
1069 | }
1070 | }
1071 | if mechanism == "X-OAUTH2" && o.OAuthToken != "" && o.OAuthScope != "" {
1072 | // Oauth authentication: send base64-encoded \x00 user \x00 token.
1073 | raw := "\x00" + user + "\x00" + o.OAuthToken
1074 | enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
1075 | base64.StdEncoding.Encode(enc, []byte(raw))
1076 | if sasl2 {
1077 | fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL2, o.OAuthXmlNs, enc)
1079 | } else {
1080 | fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL, o.OAuthXmlNs, enc)
1082 | }
1083 | }
1084 | if mechanism == "PLAIN" {
1085 | // Plain authentication: send base64-encoded \x00 user \x00 password.
1086 | raw := "\x00" + user + "\x00" + o.Password
1087 | enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
1088 | base64.StdEncoding.Encode(enc, []byte(raw))
1089 | if sasl2 {
1090 | fmt.Fprintf(c.conn, "%s\n", nsSASL2, enc)
1091 | } else {
1092 | fmt.Fprintf(c.conn, "%s\n", nsSASL, enc)
1093 | }
1094 | }
1095 | }
1096 | if mechanism == "" {
1097 | return fmt.Errorf("no viable authentication method available: %v", f.Mechanisms.Mechanism)
1098 | }
1099 | var connected bool
1100 | for !connected {
1101 | // Next message should be either success or failure.
1102 | name, val, err := c.next()
1103 | if err != nil {
1104 | return err
1105 | }
1106 | switch v := val.(type) {
1107 | case *sasl2Continue:
1108 | successMsg, err = base64.StdEncoding.DecodeString(v.AdditionalData)
1109 | if err != nil {
1110 | return err
1111 | }
1112 | fmt.Fprintf(c.stanzaWriter, "\n",
1113 | nsSASL2, saslUpgradeMech)
1114 | name, val, err = c.next()
1115 | if err != nil {
1116 | return err
1117 | }
1118 | switch v := val.(type) {
1119 | case *sasl2TaskData:
1120 | var shaNewFn func() hash.Hash
1121 | switch saslUpgradeMech {
1122 | case scramUpSHA512:
1123 | shaNewFn = sha512.New
1124 | case scramUpSHA256:
1125 | shaNewFn = sha256.New
1126 | }
1127 | salt, err := base64.StdEncoding.DecodeString(v.Salt.Text)
1128 | if err != nil {
1129 | return err
1130 | }
1131 | saltedPassword, err := pbkdf2.Key(shaNewFn, o.Password, salt,
1132 | v.Salt.Iterations, shaNewFn().Size())
1133 | if err != nil {
1134 | return err
1135 | }
1136 | saltedPasswordB64 := base64.StdEncoding.EncodeToString(saltedPassword)
1137 | fmt.Fprintf(c.stanzaWriter, "%s\n", nsSASL2, nsSCRAMUpgrade, saltedPasswordB64)
1138 | continue
1139 | default:
1140 | return fmt.Errorf("sasl2 upgrade failure: expected *sasl2TaskData, got %s", name.Local)
1141 | }
1142 |
1143 | case *sasl2Success:
1144 | if strings.HasPrefix(mechanism, "SCRAM-SHA") {
1145 | if len(successMsg) == 0 {
1146 | successMsg, err = base64.StdEncoding.DecodeString(v.AdditionalData)
1147 | if err != nil {
1148 | return err
1149 | }
1150 | }
1151 | if !strings.HasPrefix(string(successMsg), "v=") {
1152 | return errors.New("server sent unexpected content in SCRAM success message")
1153 | }
1154 | serverSignatureReply := strings.SplitN(string(successMsg), "v=", 2)[1]
1155 | serverSignatureRemote, err := base64.StdEncoding.DecodeString(serverSignatureReply)
1156 | if err != nil {
1157 | return err
1158 | }
1159 | if string(serverSignature) != string(serverSignatureRemote) {
1160 | return errors.New("SCRAM: server signature mismatch")
1161 | }
1162 | c.Mechanism = mechanism
1163 | }
1164 | if bind2 {
1165 | c.jid = v.AuthorizationIdentifier
1166 | c.domain = domain
1167 | }
1168 | if v.Token.Token != "" {
1169 | m := f.Authentication.Inline.Fast.Mechanism
1170 | switch {
1171 | case slices.Contains(m, htSHA256Expr) && tls13:
1172 | c.Fast.Mechanism = htSHA256Expr
1173 | case slices.Contains(m, htSHA256Uniq) && !tls13:
1174 | c.Fast.Mechanism = htSHA256Uniq
1175 | case slices.Contains(m, htSHA256Endp):
1176 | c.Fast.Mechanism = htSHA256Endp
1177 | case slices.Contains(m, htSHA256None):
1178 | c.Fast.Mechanism = htSHA256None
1179 | }
1180 | c.Fast.Token = v.Token.Token
1181 | c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry)
1182 | }
1183 | case *saslSuccess:
1184 | if strings.HasPrefix(mechanism, "SCRAM-SHA") {
1185 | successMsg, err := base64.StdEncoding.DecodeString(v.Text)
1186 | if err != nil {
1187 | return err
1188 | }
1189 | if !strings.HasPrefix(string(successMsg), "v=") {
1190 | return errors.New("server sent unexpected content in SCRAM success message")
1191 | }
1192 | serverSignatureReply := strings.SplitN(string(successMsg), "v=", 2)[1]
1193 | serverSignatureRemote, err := base64.StdEncoding.DecodeString(serverSignatureReply)
1194 | if err != nil {
1195 | return err
1196 | }
1197 | if string(serverSignature) != string(serverSignatureRemote) {
1198 | return errors.New("SCRAM: server signature mismatch")
1199 | }
1200 | c.Mechanism = mechanism
1201 | }
1202 | case *sasl2Failure:
1203 | errorMessage := v.Text
1204 | if errorMessage == "" {
1205 | // v.Any is type of sub-element in failure,
1206 | // which gives a description of what failed if there was no text element
1207 | errorMessage = v.Any.Local
1208 | }
1209 | return errors.New("auth failure: " + errorMessage)
1210 | case *saslFailure:
1211 | errorMessage := v.Text
1212 | if errorMessage == "" {
1213 | // v.Any is type of sub-element in failure,
1214 | // which gives a description of what failed if there was no text element
1215 | errorMessage = v.Any.Local
1216 | }
1217 | return errors.New("auth failure: " + errorMessage)
1218 | default:
1219 | return errors.New("expected or , got <" + name.Local + "> in " + name.Space)
1220 | }
1221 |
1222 | if !sasl2 {
1223 | // Now that we're authenticated, we're supposed to start the stream over again.
1224 | // Declare intent to be a jabber client.
1225 | if f, err = c.startStream(o, domain); err != nil {
1226 | return err
1227 | }
1228 | }
1229 | // Make the max. stanza size limit available.
1230 | if f.Limits.MaxBytes != "" {
1231 | c.LimitMaxBytes, err = strconv.Atoi(f.Limits.MaxBytes)
1232 | if err != nil {
1233 | c.LimitMaxBytes = 0
1234 | }
1235 | }
1236 | // Make the servers time limit after which it might consider the stream idle available.
1237 | if f.Limits.IdleSeconds != "" {
1238 | c.LimitIdleSeconds, err = strconv.Atoi(f.Limits.IdleSeconds)
1239 | if err != nil {
1240 | c.LimitIdleSeconds = 0
1241 | }
1242 | }
1243 |
1244 | if !bind2 {
1245 | // Generate a unique cookie
1246 | cookie := getCookie()
1247 |
1248 | // Send IQ message asking to bind to the local user name.
1249 | if o.Resource == "" {
1250 | fmt.Fprintf(c.stanzaWriter, "\n", cookie, nsBind)
1251 | } else {
1252 | fmt.Fprintf(c.stanzaWriter, "%s\n", cookie, nsBind, o.Resource)
1253 | }
1254 | _, val, err = c.next()
1255 | if err != nil {
1256 | return err
1257 | }
1258 | switch v := val.(type) {
1259 | case *streamError:
1260 | errorMessage := v.Text.Text
1261 | if errorMessage == "" {
1262 | // v.Any is type of sub-element in failure,
1263 | // which gives a description of what failed if there was no text element
1264 | errorMessage = v.Any.Space
1265 | }
1266 | return errors.New("stream error: " + errorMessage)
1267 | case *clientIQ:
1268 | if v.Bind.XMLName.Space == nsBind {
1269 | c.jid = v.Bind.Jid // our local id
1270 | c.domain = domain
1271 | } else {
1272 | return errors.New("bind: unexpected reply to xmpp-bind IQ")
1273 | }
1274 | }
1275 | }
1276 | if o.Session {
1277 | // if server support session, open it
1278 | cookie := getCookie() // generate new id value for session
1279 | fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(domain), cookie, nsSession)
1280 | }
1281 |
1282 | // We're connected and can now receive and send messages.
1283 | fmt.Fprintf(c.stanzaWriter, "%s%s\n", o.Status, o.StatusMessage)
1284 | connected = true
1285 | }
1286 | return nil
1287 | }
1288 |
1289 | // startTlsIfRequired examines the server's stream features and, if STARTTLS is required or supported, performs the TLS handshake.
1290 | // f will be updated if the handshake completes, as the new stream's features are typically different from the original.
1291 | func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string) (*streamFeatures, error) {
1292 | // whether we start tls is a matter of opinion: the server's and the user's.
1293 | switch {
1294 | case f.StartTLS == nil:
1295 | // the server does not support STARTTLS
1296 | return f, nil
1297 | case !o.StartTLS && f.StartTLS.Required == nil:
1298 | return f, nil
1299 | case f.StartTLS.Required != nil:
1300 | // the server requires STARTTLS.
1301 | case !o.StartTLS:
1302 | // the user wants STARTTLS and the server supports it.
1303 | }
1304 | var err error
1305 |
1306 | fmt.Fprintf(c.stanzaWriter, "\n")
1307 | var k tlsProceed
1308 | if err = c.p.DecodeElement(&k, nil); err != nil {
1309 | return f, errors.New("unmarshal : " + err.Error())
1310 | }
1311 |
1312 | tc := o.TLSConfig
1313 | if tc == nil {
1314 | tc = DefaultConfig.Clone()
1315 | // TODO(scott): we should consider using the server's address or reverse lookup
1316 | tc.ServerName = domain
1317 | }
1318 | t := tls.Client(c.conn, tc)
1319 |
1320 | if err = t.Handshake(); err != nil {
1321 | return f, errors.New("starttls handshake: " + err.Error())
1322 | }
1323 | c.conn = t
1324 |
1325 | // restart our declaration of XMPP stream intentions.
1326 | tf, err := c.startStream(o, domain)
1327 | if err != nil {
1328 | return f, err
1329 | }
1330 | return tf, nil
1331 | }
1332 |
1333 | // 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
1334 | // also started the stream; if o.Debug is true, startStream will tee decoded XML data to stderr. The features advertised by the server
1335 | // will be returned.
1336 | func (c *Client) startStream(o *Options, domain string) (*streamFeatures, error) {
1337 | if o.Debug {
1338 | if o.DebugWriter == nil {
1339 | o.DebugWriter = os.Stderr
1340 | }
1341 | debugRecv := &debugWriter{w: o.DebugWriter, prefix: "RECV "}
1342 | c.p = xml.NewDecoder(tee{c.conn, debugRecv})
1343 | debugSend := &debugWriter{w: o.DebugWriter, prefix: "SEND "}
1344 | c.stanzaWriter = io.MultiWriter(c.conn, debugSend)
1345 | } else {
1346 | c.p = xml.NewDecoder(c.conn)
1347 | c.stanzaWriter = c.conn
1348 | }
1349 |
1350 | var fromString string
1351 | if len(o.User) > 0 {
1352 | fromString = fmt.Sprintf("from='%s' ", xmlEscape(o.User))
1353 | }
1354 | if c.IsEncrypted() {
1355 | _, err := fmt.Fprintf(c.stanzaWriter, ""+
1356 | "\n",
1358 | fromString, xmlEscape(domain), nsClient, nsStream)
1359 | if err != nil {
1360 | return nil, err
1361 | }
1362 | } else {
1363 | _, err := fmt.Fprintf(c.stanzaWriter, ""+
1364 | "\n",
1365 | xmlEscape(domain), nsClient, nsStream)
1366 | if err != nil {
1367 | return nil, err
1368 | }
1369 | }
1370 |
1371 | // We expect the server to start a .
1372 | se, err := c.nextStart()
1373 | if err != nil {
1374 | return nil, err
1375 | }
1376 | if se.Name.Space != nsStream || se.Name.Local != "stream" {
1377 | return nil, fmt.Errorf("expected but got <%v> in %v", se.Name.Local, se.Name.Space)
1378 | }
1379 |
1380 | // Now we're in the stream and can use Unmarshal.
1381 | // Next message should be to tell us authentication options.
1382 | // See section 4.6 in RFC 3920.
1383 | f := new(streamFeatures)
1384 | name, val, err := c.next()
1385 | if err != nil {
1386 | return f, err
1387 | }
1388 | switch v := val.(type) {
1389 | case *streamFeatures:
1390 | return v, nil
1391 | case *streamError:
1392 | if c.IsEncrypted() && v.SeeOtherHost.Text != "" {
1393 | c.conn.Close()
1394 | c.conn, err = connect(v.SeeOtherHost.Text, o.User, o.DialTimeout)
1395 | if err != nil {
1396 | return f, err
1397 | }
1398 | f, err = c.startStream(o, domain)
1399 | if err != nil {
1400 | return f, errors.New("unmarshal : " + err.Error())
1401 | }
1402 | return f, nil
1403 | }
1404 | errorMessage := v.Text.Text
1405 | if errorMessage == "" {
1406 | // v.Any is type of sub-element in failure,
1407 | // which gives a description of what failed if there was no text element
1408 | errorMessage = v.Any.Space
1409 | }
1410 | return f, errors.New("stream error: " + errorMessage)
1411 | default:
1412 | return f, errors.New("expected or , got <" + name.Local + "> in " + name.Space)
1413 | }
1414 | }
1415 |
1416 | // IsEncrypted will return true if the client is connected using a TLS transport, either because it used.
1417 | // TLS to connect from the outset, or because it successfully used STARTTLS to promote a TCP connection to TLS.
1418 | func (c *Client) IsEncrypted() bool {
1419 | _, ok := c.conn.(*tls.Conn)
1420 | return ok
1421 | }
1422 |
1423 | // Chat is an incoming or outgoing XMPP chat message.
1424 | type Chat struct {
1425 | Remote string
1426 | Type string
1427 | Text string
1428 | Subject string
1429 | Thread string
1430 | Ooburl string
1431 | Oobdesc string
1432 | Lang string
1433 | Roster Roster
1434 | Other []string
1435 | OtherElem []XMLElement
1436 | Stamp time.Time
1437 | }
1438 |
1439 | type Roster []Contact
1440 |
1441 | type Contact struct {
1442 | Remote string
1443 | Name string
1444 | Group []string
1445 | }
1446 |
1447 | // Presence is an XMPP presence notification.
1448 | type Presence struct {
1449 | From string
1450 | To string
1451 | Type string
1452 | Show string
1453 | Status string
1454 | }
1455 |
1456 | type IQ struct {
1457 | ID string
1458 | From string
1459 | To string
1460 | Type string
1461 | Query []byte
1462 | }
1463 |
1464 | // Recv waits to receive the next XMPP stanza.
1465 | func (c *Client) Recv() (stanza interface{}, err error) {
1466 | for {
1467 | _, val, err := c.next()
1468 | if err != nil {
1469 | return Chat{}, err
1470 | }
1471 | switch v := val.(type) {
1472 | case *streamError:
1473 | errorMessage := v.Text.Text
1474 | if errorMessage == "" {
1475 | // v.Any is type of sub-element in failure,
1476 | // which gives a description of what failed if there was no text element
1477 | errorMessage = v.Any.Space
1478 | }
1479 | return Chat{}, errors.New("stream error: " + errorMessage)
1480 | case *clientMessage:
1481 | if v.Event.XMLNS == XMPPNS_PUBSUB_EVENT {
1482 | // Handle Pubsub notifications
1483 | switch v.Event.Items.Node {
1484 | case XMPPNS_AVATAR_PEP_METADATA:
1485 | if len(v.Event.Items.Items) == 0 {
1486 | return AvatarMetadata{}, errors.New("no avatar metadata items available")
1487 | }
1488 |
1489 | return handleAvatarMetadata(v.Event.Items.Items[0].Body,
1490 | v.From)
1491 | // I am not sure whether this can even happen.
1492 | // XEP-0084 only specifies a subscription to
1493 | // the metadata node.
1494 | /*case XMPPNS_AVATAR_PEP_DATA:
1495 | return handleAvatarData(v.Event.Items.Items[0].Body,
1496 | v.From,
1497 | v.Event.Items.Items[0].ID)*/
1498 | default:
1499 | return pubsubClientToReturn(v.Event), nil
1500 | }
1501 | }
1502 |
1503 | stamp, _ := time.Parse(
1504 | "2006-01-02T15:04:05Z",
1505 | v.Delay.Stamp,
1506 | )
1507 | chat := Chat{
1508 | Remote: v.From,
1509 | Type: v.Type,
1510 | Text: v.Body,
1511 | Subject: v.Subject,
1512 | Thread: v.Thread,
1513 | Other: v.OtherStrings(),
1514 | OtherElem: v.Other,
1515 | Stamp: stamp,
1516 | Lang: v.Lang,
1517 | }
1518 | return chat, nil
1519 | case *clientQuery:
1520 | var r Roster
1521 | for _, item := range v.Item {
1522 | r = append(r, Contact{item.Jid, item.Name, item.Group})
1523 | }
1524 | return Chat{Type: "roster", Roster: r}, nil
1525 | case *clientPresence:
1526 | return Presence{v.From, v.To, v.Type, v.Show, v.Status}, nil
1527 | case *clientIQ:
1528 | switch {
1529 | case v.Query.XMLName.Space == "urn:xmpp:ping":
1530 | // TODO check more strictly
1531 | err := c.SendResultPing(v.ID, v.From)
1532 | if err != nil {
1533 | return Chat{}, err
1534 | }
1535 | fallthrough
1536 | case v.Type == "error":
1537 | switch {
1538 | case slices.Contains(c.subIDs, v.ID):
1539 | index := slices.Index(c.subIDs, v.ID)
1540 | c.subIDs = slices.Delete(c.subIDs, index, index)
1541 | // Pubsub subscription failed
1542 | var errs []clientPubsubError
1543 | err := xml.Unmarshal([]byte(v.Error.InnerXML), &errs)
1544 | if err != nil {
1545 | return PubsubSubscription{}, err
1546 | }
1547 |
1548 | var errsStr []string
1549 | for _, e := range errs {
1550 | errsStr = append(errsStr, e.XMLName.Local)
1551 | }
1552 |
1553 | return PubsubSubscription{
1554 | Errors: errsStr,
1555 | }, nil
1556 | default:
1557 | res, err := xml.Marshal(v.Query)
1558 | if err != nil {
1559 | return Chat{}, err
1560 | }
1561 |
1562 | return IQ{
1563 | ID: v.ID, From: v.From, To: v.To, Type: v.Type,
1564 | Query: res,
1565 | }, nil
1566 | }
1567 | case v.Type == "result":
1568 | switch {
1569 | case v.Query.XMLName.Space == XMPPNS_DISCO_ITEMS:
1570 | var itemsQuery clientDiscoItemsQuery
1571 | err := xml.Unmarshal(v.InnerXML, &itemsQuery)
1572 | if err != nil {
1573 | return []DiscoItem{}, err
1574 | }
1575 |
1576 | return DiscoItems{
1577 | Jid: v.From,
1578 | Items: clientDiscoItemsToReturn(itemsQuery.Items),
1579 | }, nil
1580 | case v.Query.XMLName.Space == XMPPNS_DISCO_INFO:
1581 | var disco clientDiscoQuery
1582 | err := xml.Unmarshal(v.InnerXML, &disco)
1583 | if err != nil {
1584 | return DiscoResult{}, err
1585 | }
1586 |
1587 | return DiscoResult{
1588 | Features: clientFeaturesToReturn(disco.Features),
1589 | Identities: clientIdentitiesToReturn(disco.Identities),
1590 | X: disco.X,
1591 | }, nil
1592 | case slices.Contains(c.subIDs, v.ID):
1593 | index := slices.Index(c.subIDs, v.ID)
1594 | c.subIDs = slices.Delete(c.subIDs, index, index)
1595 | if v.Query.XMLName.Local == "pubsub" {
1596 | // Subscription or unsubscription was successful
1597 | var sub clientPubsubSubscription
1598 | err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub)
1599 | if err != nil {
1600 | return PubsubSubscription{}, err
1601 | }
1602 |
1603 | return PubsubSubscription{
1604 | SubID: sub.SubID,
1605 | JID: sub.JID,
1606 | Node: sub.Node,
1607 | Errors: nil,
1608 | }, nil
1609 | }
1610 | case slices.Contains(c.unsubIDs, v.ID):
1611 | index := slices.Index(c.unsubIDs, v.ID)
1612 | c.unsubIDs = slices.Delete(c.unsubIDs, index, index)
1613 | if v.Query.XMLName.Local == "pubsub" {
1614 | var sub clientPubsubSubscription
1615 | err := xml.Unmarshal([]byte(v.Query.InnerXML), &sub)
1616 | if err != nil {
1617 | return PubsubUnsubscription{}, err
1618 | }
1619 |
1620 | return PubsubUnsubscription{
1621 | SubID: sub.SubID,
1622 | JID: v.From,
1623 | Node: sub.Node,
1624 | Errors: nil,
1625 | }, nil
1626 | } else {
1627 | // Unsubscribing MAY contain a pubsub element. But it does
1628 | // not have to
1629 | return PubsubUnsubscription{
1630 | SubID: "",
1631 | JID: v.From,
1632 | Node: "",
1633 | Errors: nil,
1634 | }, nil
1635 | }
1636 | case slices.Contains(c.itemsIDs, v.ID):
1637 | index := slices.Index(c.itemsIDs, v.ID)
1638 | c.itemsIDs = slices.Delete(c.itemsIDs, index, index)
1639 | if v.Query.XMLName.Local == "pubsub" {
1640 | var p clientPubsubItems
1641 | err := xml.Unmarshal([]byte(v.Query.InnerXML), &p)
1642 | if err != nil {
1643 | return PubsubItems{}, err
1644 | }
1645 |
1646 | switch p.Node {
1647 | case XMPPNS_AVATAR_PEP_DATA:
1648 | if len(p.Items) == 0 {
1649 | return AvatarData{}, errors.New("no avatar data items available")
1650 | }
1651 |
1652 | return handleAvatarData(p.Items[0].Body,
1653 | v.From,
1654 | p.Items[0].ID)
1655 | case XMPPNS_AVATAR_PEP_METADATA:
1656 | if len(p.Items) == 0 {
1657 | return AvatarMetadata{}, errors.New("no avatar metadata items available")
1658 | }
1659 |
1660 | return handleAvatarMetadata(p.Items[0].Body,
1661 | v.From)
1662 | default:
1663 | return PubsubItems{
1664 | p.Node,
1665 | pubsubItemsToReturn(p.Items),
1666 | }, nil
1667 | }
1668 | }
1669 | // Note: XEP-0084 states that metadata and data
1670 | // should be fetched with an id of retrieve1.
1671 | // Since we already have PubSub implemented, we
1672 | // can just use items1 and items3 to do the same
1673 | // as an Avatar node is just a PEP (PubSub) node.
1674 | /*case "retrieve1":
1675 | var p clientPubsubItems
1676 | err := xml.Unmarshal([]byte(v.Query.InnerXML), &p)
1677 | if err != nil {
1678 | return PubsubItems{}, err
1679 | }
1680 |
1681 | switch p.Node {
1682 | case XMPPNS_AVATAR_PEP_DATA:
1683 | return handleAvatarData(p.Items[0].Body,
1684 | v.From,
1685 | p.Items[0].ID)
1686 | case XMPPNS_AVATAR_PEP_METADATA:
1687 | return handleAvatarMetadata(p.Items[0].Body,
1688 | v
1689 | }*/
1690 | default:
1691 | res, err := xml.Marshal(v.Query)
1692 | if err != nil {
1693 | return Chat{}, err
1694 | }
1695 |
1696 | return IQ{
1697 | ID: v.ID, From: v.From, To: v.To, Type: v.Type,
1698 | Query: res,
1699 | }, nil
1700 | }
1701 | case v.Query.XMLName.Local == "":
1702 | return IQ{ID: v.ID, From: v.From, To: v.To, Type: v.Type}, nil
1703 | default:
1704 | res, err := xml.Marshal(v.Query)
1705 | if err != nil {
1706 | return Chat{}, err
1707 | }
1708 |
1709 | return IQ{
1710 | ID: v.ID, From: v.From, To: v.To, Type: v.Type,
1711 | Query: res,
1712 | }, nil
1713 | }
1714 | }
1715 | }
1716 | }
1717 |
1718 | // Send sends the message wrapped inside an XMPP message stanza body.
1719 | func (c *Client) Send(chat Chat) (n int, err error) {
1720 | var subtext, thdtext, oobtext string
1721 | if chat.Subject != `` {
1722 | subtext = `` + xmlEscape(chat.Subject) + ``
1723 | }
1724 | if chat.Thread != `` {
1725 | thdtext = `` + xmlEscape(chat.Thread) + ``
1726 | }
1727 | if chat.Ooburl != `` {
1728 | oobtext = `` + xmlEscape(chat.Ooburl) + ``
1729 | if chat.Oobdesc != `` {
1730 | oobtext += `` + xmlEscape(chat.Oobdesc) + ``
1731 | }
1732 | oobtext += ``
1733 | }
1734 |
1735 | chat.Text = validUTF8(chat.Text)
1736 | stanza := fmt.Sprintf(""+subtext+"%s"+oobtext+thdtext+"\n",
1737 | xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce(), xmlEscape(chat.Text))
1738 | if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes {
1739 | return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)",
1740 | len(stanza), c.LimitMaxBytes)
1741 | }
1742 |
1743 | // Reset ticker for periodic pings if configured.
1744 | if c.periodicPings {
1745 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
1746 | }
1747 | return fmt.Fprint(c.stanzaWriter, stanza)
1748 | }
1749 |
1750 | // SendOOB sends OOB data wrapped inside an XMPP message stanza, without actual body.
1751 | func (c *Client) SendOOB(chat Chat) (n int, err error) {
1752 | var thdtext, oobtext string
1753 | if chat.Thread != `` {
1754 | thdtext = `` + xmlEscape(chat.Thread) + ``
1755 | }
1756 | if chat.Ooburl != `` {
1757 | oobtext = `` + xmlEscape(chat.Ooburl) + ``
1758 | if chat.Oobdesc != `` {
1759 | oobtext += `` + xmlEscape(chat.Oobdesc) + ``
1760 | }
1761 | oobtext += ``
1762 | }
1763 | stanza := fmt.Sprintf(""+oobtext+thdtext+"\n",
1764 | xmlEscape(chat.Remote), xmlEscape(chat.Type), cnonce())
1765 | if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes {
1766 | return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)",
1767 | len(stanza), c.LimitMaxBytes)
1768 | }
1769 | // Reset ticker for periodic pings if configured.
1770 | if c.periodicPings {
1771 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
1772 | }
1773 | return fmt.Fprint(c.stanzaWriter, stanza)
1774 | }
1775 |
1776 | // SendOrg sends the original text without being wrapped in an XMPP message stanza.
1777 | func (c *Client) SendOrg(org string) (n int, err error) {
1778 | stanza := fmt.Sprint(org + "\n")
1779 | if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes {
1780 | return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)",
1781 | len(stanza), c.LimitMaxBytes)
1782 | }
1783 | // Reset ticker for periodic pings if configured.
1784 | if c.periodicPings {
1785 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
1786 | }
1787 | return fmt.Fprint(c.stanzaWriter, stanza)
1788 | }
1789 |
1790 | // SendPresence sends Presence wrapped inside XMPP presence stanza.
1791 | func (c *Client) SendPresence(presence Presence) (n int, err error) {
1792 | // Forge opening presence tag
1793 | var buf string = ""
1813 |
1814 | // TODO: there may be optional tag "priority", but former presence type does not take this into account
1815 | // so either we must follow std, change type xmpp.Presence and break backward compatibility
1816 | // or leave it as-is and potentially break client software
1817 |
1818 | if presence.Show != "" {
1819 | // https://www.ietf.org/rfc/rfc3921.txt 2.2.2.1, show can be only
1820 | // away, chat, dnd, xa
1821 | switch presence.Show {
1822 | case "away", "chat", "dnd", "xa":
1823 | buf = buf + fmt.Sprintf("%s", xmlEscape(presence.Show))
1824 | }
1825 | }
1826 |
1827 | if presence.Status != "" {
1828 | buf = buf + fmt.Sprintf("%s", xmlEscape(presence.Status))
1829 | }
1830 |
1831 | stanza := fmt.Sprintf("%s\n", buf)
1832 | if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes {
1833 | return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)",
1834 | len(stanza), c.LimitMaxBytes)
1835 | }
1836 | // Reset ticker for periodic pings if configured.
1837 | if c.periodicPings {
1838 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
1839 | }
1840 | return fmt.Fprint(c.stanzaWriter, stanza)
1841 | }
1842 |
1843 | // SendKeepAlive sends a "whitespace keepalive" as described in chapter 4.6.1 of RFC6120.
1844 | func (c *Client) SendKeepAlive() (n int, err error) {
1845 | return fmt.Fprintf(c.conn, " ")
1846 | }
1847 |
1848 | // SendHtml sends the message as HTML as defined by XEP-0071
1849 | func (c *Client) SendHtml(chat Chat) (n int, err error) {
1850 | stanza := fmt.Sprintf("%s"+
1851 | "%s\n",
1852 | xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text), chat.Text)
1853 | if c.LimitMaxBytes != 0 && len(stanza) > c.LimitMaxBytes {
1854 | return 0, fmt.Errorf("stanza size (%v bytes) exceeds server limit (%v bytes)",
1855 | len(stanza), c.LimitMaxBytes)
1856 | }
1857 | // Reset ticker for periodic pings if configured.
1858 | if c.periodicPings {
1859 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
1860 | }
1861 | return fmt.Fprint(c.stanzaWriter, stanza)
1862 | }
1863 |
1864 | // Roster asks for the chat roster.
1865 | func (c *Client) Roster() error {
1866 | // Reset ticker for periodic pings if configured.
1867 | if c.periodicPings {
1868 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
1869 | }
1870 | fmt.Fprintf(c.stanzaWriter, "\n", xmlEscape(c.jid))
1871 | return nil
1872 | }
1873 |
1874 | // RFC 3920 C.1 Streams name space
1875 | type streamFeatures struct {
1876 | XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
1877 | Authentication sasl2Authentication
1878 | StartTLS *tlsStartTLS
1879 | Mechanisms saslMechanisms
1880 | ChannelBindings saslChannelBindings
1881 | Bind bindBind
1882 | Session bool
1883 | Limits streamLimits
1884 | }
1885 |
1886 | type streamError struct {
1887 | XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
1888 | Any xml.Name
1889 | Text struct {
1890 | Text string `xml:",chardata"`
1891 | Lang string `xml:"lang,attr"`
1892 | Xmlns string `xml:"xmlns,attr"`
1893 | } `xml:"text"`
1894 | SeeOtherHost struct {
1895 | Text string `xml:",chardata"`
1896 | Xmlns string `xml:"xmlns,attr"`
1897 | } `xml:"see-other-host"`
1898 | }
1899 |
1900 | // RFC 3920 C.3 TLS name space
1901 | type tlsStartTLS struct {
1902 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
1903 | Required *string `xml:"required"`
1904 | }
1905 |
1906 | type tlsProceed struct {
1907 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
1908 | }
1909 |
1910 | type tlsFailure struct {
1911 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
1912 | }
1913 |
1914 | type sasl2Authentication struct {
1915 | XMLName xml.Name `xml:"urn:xmpp:sasl:2 authentication"`
1916 | Mechanism []string `xml:"mechanism"`
1917 | Inline struct {
1918 | Text string `xml:",chardata"`
1919 | Bind struct {
1920 | XMLName xml.Name `xml:"urn:xmpp:bind:0 bind"`
1921 | Xmlns string `xml:"xmlns,attr"`
1922 | Text string `xml:",chardata"`
1923 | } `xml:"bind"`
1924 | Fast struct {
1925 | XMLName xml.Name `xml:"urn:xmpp:fast:0 fast"`
1926 | Text string `xml:",chardata"`
1927 | Tls0rtt string `xml:"tls-0rtt,attr"`
1928 | Mechanism []string `xml:"mechanism"`
1929 | } `xml:"fast"`
1930 | } `xml:"inline"`
1931 | Upgrade []struct {
1932 | Text string `xml:",chardata"`
1933 | Xmlns string `xml:"xmlns,attr"`
1934 | } `xml:"upgrade"`
1935 | }
1936 |
1937 | // RFC 3920 C.4 SASL name space
1938 | type saslMechanisms struct {
1939 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
1940 | Mechanism []string `xml:"mechanism"`
1941 | }
1942 |
1943 | type saslChannelBindings struct {
1944 | XMLName xml.Name `xml:"sasl-channel-binding"`
1945 | Text string `xml:",chardata"`
1946 | Xmlns string `xml:"xmlns,attr"`
1947 | ChannelBinding []struct {
1948 | Text string `xml:",chardata"`
1949 | Type string `xml:"type,attr"`
1950 | } `xml:"channel-binding"`
1951 | }
1952 |
1953 | type saslAbort struct {
1954 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl abort"`
1955 | }
1956 |
1957 | type sasl2Success struct {
1958 | XMLName xml.Name `xml:"urn:xmpp:sasl:2 success"`
1959 | Text string `xml:",chardata"`
1960 | AdditionalData string `xml:"additional-data"`
1961 | AuthorizationIdentifier string `xml:"authorization-identifier"`
1962 | Bound struct {
1963 | Text string `xml:",chardata"`
1964 | Xmlns string `xml:"urn:xmpp:bind:0,attr"`
1965 | } `xml:"bound"`
1966 | Token struct {
1967 | Text string `xml:",chardata"`
1968 | Xmlns string `xml:"urn:xmpp:fast:0,attr"`
1969 | Expiry string `xml:"expiry,attr"`
1970 | Token string `xml:"token,attr"`
1971 | } `xml:"token"`
1972 | }
1973 |
1974 | type sasl2Continue struct {
1975 | XMLName xml.Name `xml:"continue"`
1976 | Text string `xml:",chardata"`
1977 | Xmlns string `xml:"xmlns,attr"`
1978 | AdditionalData string `xml:"additional-data"`
1979 | Tasks struct {
1980 | Text string `xml:",chardata"`
1981 | Task string `xml:"task"`
1982 | } `xml:"tasks"`
1983 | }
1984 |
1985 | type sasl2TaskData struct {
1986 | XMLName xml.Name `xml:"task-data"`
1987 | Text string `xml:",chardata"`
1988 | Xmlns string `xml:"xmlns,attr"`
1989 | Salt struct {
1990 | Text string `xml:",chardata"`
1991 | Xmlns string `xml:"xmlns,attr"`
1992 | Iterations int `xml:"iterations,attr"`
1993 | } `xml:"salt"`
1994 | }
1995 |
1996 | type saslSuccess struct {
1997 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
1998 | Text string `xml:",chardata"`
1999 | }
2000 |
2001 | type sasl2Failure struct {
2002 | XMLName xml.Name `xml:"urn:xmpp:sasl:2 failure"`
2003 | Any xml.Name `xml:",any"`
2004 | Text string `xml:"text"`
2005 | }
2006 |
2007 | type saslFailure struct {
2008 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
2009 | Any xml.Name `xml:",any"`
2010 | Text string `xml:"text"`
2011 | }
2012 |
2013 | type sasl2Challenge struct {
2014 | XMLName xml.Name `xml:"urn:xmpp:sasl:2 challenge"`
2015 | Text string `xml:",chardata"`
2016 | }
2017 |
2018 | type saslChallenge struct {
2019 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl challenge"`
2020 | Text string `xml:",chardata"`
2021 | }
2022 |
2023 | type streamLimits struct {
2024 | XMLName xml.Name `xml:"limits"`
2025 | Text string `xml:",chardata"`
2026 | Xmlns string `xml:"xmlns,attr"`
2027 | MaxBytes string `xml:"max-bytes"`
2028 | IdleSeconds string `xml:"idle-seconds"`
2029 | }
2030 |
2031 | // RFC 3920 C.5 Resource binding name space
2032 | type bindBind struct {
2033 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
2034 | Resource string
2035 | Jid string `xml:"jid"`
2036 | }
2037 |
2038 | // RFC 3921 B.1 jabber:client
2039 | type clientMessage struct {
2040 | XMLName xml.Name `xml:"jabber:client message"`
2041 | From string `xml:"from,attr"`
2042 | ID string `xml:"id,attr"`
2043 | To string `xml:"to,attr"`
2044 | Type string `xml:"type,attr"` // chat, error, groupchat, headline, or normal
2045 | Lang string `xml:"lang,attr"`
2046 |
2047 | // These should technically be []clientText, but string is much more convenient.
2048 | Subject string `xml:"subject"`
2049 | Body string `xml:"body"`
2050 | Thread string `xml:"thread"`
2051 |
2052 | // Pubsub
2053 | Event clientPubsubEvent `xml:"event"`
2054 |
2055 | // Any hasn't matched element
2056 | Other []XMLElement `xml:",any"`
2057 |
2058 | Delay Delay `xml:"delay"`
2059 | }
2060 |
2061 | func (m *clientMessage) OtherStrings() []string {
2062 | a := make([]string, len(m.Other))
2063 | for i, e := range m.Other {
2064 | a[i] = e.String()
2065 | }
2066 | return a
2067 | }
2068 |
2069 | type XMLElement struct {
2070 | XMLName xml.Name
2071 | Attr []xml.Attr `xml:",any,attr"` // Save the attributes of the xml element
2072 | InnerXML string `xml:",innerxml"`
2073 | }
2074 |
2075 | func (e *XMLElement) String() string {
2076 | r := bytes.NewReader([]byte(e.InnerXML))
2077 | d := xml.NewDecoder(r)
2078 | var buf bytes.Buffer
2079 | for {
2080 | tok, err := d.Token()
2081 | if err != nil {
2082 | break
2083 | }
2084 | switch v := tok.(type) {
2085 | case xml.StartElement:
2086 | err = d.Skip()
2087 | case xml.CharData:
2088 | _, err = buf.Write(v)
2089 | }
2090 | if err != nil {
2091 | break
2092 | }
2093 | }
2094 | return buf.String()
2095 | }
2096 |
2097 | type Delay struct {
2098 | Stamp string `xml:"stamp,attr"`
2099 | }
2100 |
2101 | type clientPresence struct {
2102 | XMLName xml.Name `xml:"jabber:client presence"`
2103 | From string `xml:"from,attr"`
2104 | ID string `xml:"id,attr"`
2105 | To string `xml:"to,attr"`
2106 | Type string `xml:"type,attr"` // error, probe, subscribe, subscribed, unavailable, unsubscribe, unsubscribed
2107 | Lang string `xml:"lang,attr"`
2108 |
2109 | Show string `xml:"show"` // away, chat, dnd, xa
2110 | Status string `xml:"status"` // sb []clientText
2111 | Priority string `xml:"priority,attr"`
2112 | Error *clientError
2113 | }
2114 |
2115 | type clientIQ struct {
2116 | // info/query
2117 | XMLName xml.Name `xml:"jabber:client iq"`
2118 | From string `xml:"from,attr"`
2119 | ID string `xml:"id,attr"`
2120 | To string `xml:"to,attr"`
2121 | Type string `xml:"type,attr"` // error, get, result, set
2122 | Query XMLElement `xml:",any"`
2123 | Error clientError
2124 | Bind bindBind
2125 |
2126 | InnerXML []byte `xml:",innerxml"`
2127 | }
2128 |
2129 | type clientError struct {
2130 | XMLName xml.Name `xml:"jabber:client error"`
2131 | Code string `xml:",attr"`
2132 | Type string `xml:"type,attr"`
2133 | Any xml.Name
2134 | InnerXML []byte `xml:",innerxml"`
2135 | Text string
2136 | }
2137 |
2138 | type clientQuery struct {
2139 | Item []rosterItem
2140 | }
2141 |
2142 | type rosterItem struct {
2143 | XMLName xml.Name `xml:"jabber:iq:roster item"`
2144 | Jid string `xml:",attr"`
2145 | Name string `xml:",attr"`
2146 | Subscription string `xml:",attr"`
2147 | Group []string
2148 | }
2149 |
2150 | // Scan XML token stream to find next StartElement.
2151 | func (c *Client) nextStart() (xml.StartElement, error) {
2152 | for {
2153 | // Do not read from the stream if it's
2154 | // going to be closed.
2155 | if c.shutdown {
2156 | return xml.StartElement{}, io.EOF
2157 | }
2158 | c.nextMutex.Lock()
2159 | to, err := c.p.Token()
2160 | if err != nil || to == nil {
2161 | c.nextMutex.Unlock()
2162 | return xml.StartElement{}, err
2163 | }
2164 | t := xml.CopyToken(to)
2165 | switch t := t.(type) {
2166 | case xml.StartElement:
2167 | c.nextMutex.Unlock()
2168 | return t, nil
2169 | }
2170 | c.nextMutex.Unlock()
2171 | }
2172 | }
2173 |
2174 | // Scan XML token stream to find next EndElement
2175 | func (c *Client) nextEnd() (xml.EndElement, error) {
2176 | c.p.Strict = false
2177 | for {
2178 | c.nextMutex.Lock()
2179 | to, err := c.p.Token()
2180 | if err != nil || to == nil {
2181 | c.nextMutex.Unlock()
2182 | return xml.EndElement{}, err
2183 | }
2184 | t := xml.CopyToken(to)
2185 | switch t := t.(type) {
2186 | case xml.EndElement:
2187 | // Do not unlock mutex if the stream is closed to
2188 | // prevent further reading on the stream.
2189 | if t.Name.Local == "stream" {
2190 | return t, nil
2191 | }
2192 | c.nextMutex.Unlock()
2193 | return t, nil
2194 | }
2195 | c.nextMutex.Unlock()
2196 | }
2197 | }
2198 |
2199 | // Scan XML token stream for next element and save into val.
2200 | // If val == nil, allocate new element based on proto map.
2201 | // Either way, return val.
2202 | func (c *Client) next() (xml.Name, interface{}, error) {
2203 | // Read start element to find out what type we want.
2204 | se, err := c.nextStart()
2205 | if err != nil {
2206 | return xml.Name{}, nil, err
2207 | }
2208 |
2209 | // Put it in an interface and allocate one.
2210 | var nv interface{}
2211 | switch se.Name.Space + " " + se.Name.Local {
2212 | case nsStream + " features":
2213 | nv = &streamFeatures{}
2214 | case nsStream + " error":
2215 | nv = &streamError{}
2216 | case nsTLS + " starttls":
2217 | nv = &tlsStartTLS{}
2218 | case nsTLS + " proceed":
2219 | nv = &tlsProceed{}
2220 | case nsTLS + " failure":
2221 | nv = &tlsFailure{}
2222 | case nsSASL + " mechanisms":
2223 | nv = &saslMechanisms{}
2224 | case nsSASL2 + " challenge":
2225 | nv = &sasl2Challenge{}
2226 | case nsSASL + " challenge":
2227 | nv = &saslChallenge{}
2228 | case nsSASL + " response":
2229 | nv = ""
2230 | case nsSASL + " abort":
2231 | nv = &saslAbort{}
2232 | case nsSASL2 + " success":
2233 | nv = &sasl2Success{}
2234 | case nsSASL2 + " continue":
2235 | nv = &sasl2Continue{}
2236 | case nsSASL2 + " task-data":
2237 | nv = &sasl2TaskData{}
2238 | case nsSASL + " success":
2239 | nv = &saslSuccess{}
2240 | case nsSASL2 + " failure":
2241 | nv = &sasl2Failure{}
2242 | case nsSASL + " failure":
2243 | nv = &saslFailure{}
2244 | case nsSASLCB + " sasl-channel-binding":
2245 | nv = &saslChannelBindings{}
2246 | case nsBind + " bind":
2247 | nv = &bindBind{}
2248 | case nsClient + " message":
2249 | nv = &clientMessage{}
2250 | case nsClient + " presence":
2251 | nv = &clientPresence{}
2252 | case nsClient + " iq":
2253 | nv = &clientIQ{}
2254 | case nsClient + " error":
2255 | nv = &clientError{}
2256 | default:
2257 | return xml.Name{}, nil, errors.New("unexpected XMPP message " +
2258 | se.Name.Space + " <" + se.Name.Local + "/>")
2259 | }
2260 |
2261 | // Unmarshal into that storage.
2262 | c.nextMutex.Lock()
2263 | if err = c.p.DecodeElement(nv, &se); err != nil {
2264 | return xml.Name{}, nil, err
2265 | }
2266 | c.nextMutex.Unlock()
2267 |
2268 | return se.Name, nv, err
2269 | }
2270 |
2271 | func xmlEscape(s string) string {
2272 | var b bytes.Buffer
2273 | xml.Escape(&b, []byte(s))
2274 |
2275 | return b.String()
2276 | }
2277 |
2278 | type tee struct {
2279 | r io.Reader
2280 | w io.Writer
2281 | }
2282 |
2283 | func (t tee) Read(p []byte) (n int, err error) {
2284 | n, err = t.r.Read(p)
2285 | if n > 0 {
2286 | _, err = t.w.Write(p[0:n])
2287 | if err != nil {
2288 | return
2289 | }
2290 | _, err = t.w.Write([]byte("\n"))
2291 | }
2292 | return
2293 | }
2294 |
2295 | func validUTF8(s string) string {
2296 | // Remove invalid code points.
2297 | s = strings.ToValidUTF8(s, "�")
2298 | reg := regexp.MustCompile(`[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}]`)
2299 | s = reg.ReplaceAllString(s, "�")
2300 |
2301 | return s
2302 | }
2303 |
--------------------------------------------------------------------------------
/xmpp_avatar.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "crypto/sha1"
5 | "encoding/base64"
6 | "encoding/hex"
7 | "encoding/xml"
8 | "errors"
9 | "strconv"
10 | )
11 |
12 | const (
13 | XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data"
14 | XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata"
15 | )
16 |
17 | type clientAvatarData struct {
18 | XMLName xml.Name `xml:"data"`
19 | Data []byte `xml:",innerxml"`
20 | }
21 |
22 | type clientAvatarInfo struct {
23 | XMLName xml.Name `xml:"info"`
24 | Bytes string `xml:"bytes,attr"`
25 | Width string `xml:"width,attr"`
26 | Height string `xml:"height,attr"`
27 | ID string `xml:"id,attr"`
28 | Type string `xml:"type,attr"`
29 | URL string `xml:"url,attr"`
30 | }
31 |
32 | type clientAvatarMetadata struct {
33 | XMLName xml.Name `xml:"metadata"`
34 | XMLNS string `xml:"xmlns,attr"`
35 | Info clientAvatarInfo `xml:"info"`
36 | }
37 |
38 | type AvatarData struct {
39 | Data []byte
40 | From string
41 | }
42 |
43 | type AvatarMetadata struct {
44 | From string
45 | Bytes int
46 | Width int
47 | Height int
48 | ID string
49 | Type string
50 | URL string
51 | }
52 |
53 | func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
54 | var data clientAvatarData
55 | err := xml.Unmarshal(itemsBody, &data)
56 | if err != nil {
57 | return AvatarData{}, err
58 | }
59 |
60 | // Base64-decode the avatar data to check its SHA1 hash
61 | dataRaw, err := base64.StdEncoding.DecodeString(
62 | string(data.Data))
63 | if err != nil {
64 | return AvatarData{}, err
65 | }
66 |
67 | hash := sha1.Sum(dataRaw)
68 | hashStr := hex.EncodeToString(hash[:])
69 | if hashStr != id {
70 | return AvatarData{}, errors.New("SHA1 hashes do not match")
71 | }
72 |
73 | return AvatarData{
74 | Data: dataRaw,
75 | From: from,
76 | }, nil
77 | }
78 |
79 | func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
80 | var meta clientAvatarMetadata
81 | err := xml.Unmarshal(body, &meta)
82 | if err != nil {
83 | return AvatarMetadata{}, err
84 | }
85 |
86 | return AvatarMetadata{
87 | From: from,
88 | Bytes: atoiw(meta.Info.Bytes),
89 | Width: atoiw(meta.Info.Width),
90 | Height: atoiw(meta.Info.Height),
91 | ID: meta.Info.ID,
92 | Type: meta.Info.Type,
93 | URL: meta.Info.URL,
94 | }, nil
95 | }
96 |
97 | // A wrapper for atoi which just returns -1 if an error occurs
98 | func atoiw(str string) int {
99 | i, err := strconv.Atoi(str)
100 | if err != nil {
101 | return -1
102 | }
103 |
104 | return i
105 | }
106 |
107 | func (c *Client) AvatarSubscribeMetadata(jid string) error {
108 | return c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
109 | }
110 |
111 | func (c *Client) AvatarUnsubscribeMetadata(jid string) error {
112 | return c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
113 | }
114 |
115 | func (c *Client) AvatarRequestData(jid string) error {
116 | return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid)
117 | }
118 |
119 | func (c *Client) AvatarRequestDataByID(jid, id string) error {
120 | return c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id)
121 | }
122 |
123 | func (c *Client) AvatarRequestMetadata(jid string) error {
124 | return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid)
125 | }
126 |
--------------------------------------------------------------------------------
/xmpp_disco.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "encoding/xml"
5 | )
6 |
7 | const (
8 | XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
9 | XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
10 | )
11 |
12 | type clientDiscoFeature struct {
13 | XMLName xml.Name `xml:"feature"`
14 | Var string `xml:"var,attr"`
15 | }
16 |
17 | type clientDiscoIdentity struct {
18 | XMLName xml.Name `xml:"identity"`
19 | Category string `xml:"category,attr"`
20 | Type string `xml:"type,attr"`
21 | Name string `xml:"name,attr"`
22 | }
23 |
24 | type clientDiscoQuery struct {
25 | XMLName xml.Name `xml:"query"`
26 | Features []clientDiscoFeature `xml:"feature"`
27 | Identities []clientDiscoIdentity `xml:"identity"`
28 | X []DiscoX `xml:"x"`
29 | }
30 |
31 | type clientDiscoItem struct {
32 | XMLName xml.Name `xml:"item"`
33 | Jid string `xml:"jid,attr"`
34 | Node string `xml:"node,attr"`
35 | Name string `xml:"name,attr"`
36 | }
37 |
38 | type clientDiscoItemsQuery struct {
39 | XMLName xml.Name `xml:"query"`
40 | Items []clientDiscoItem `xml:"item"`
41 | }
42 |
43 | type DiscoIdentity struct {
44 | Category string
45 | Type string
46 | Name string
47 | }
48 |
49 | type DiscoItem struct {
50 | Jid string
51 | Name string
52 | Node string
53 | }
54 |
55 | type DiscoResult struct {
56 | Features []string
57 | Identities []DiscoIdentity
58 | X []DiscoX
59 | }
60 |
61 | type DiscoX struct {
62 | XMLName xml.Name `xml:"x"`
63 | Field []DiscoXField `xml:"field"`
64 | }
65 |
66 | type DiscoXField struct {
67 | Type string `xml:"type,attr"`
68 | Var string `xml:"var,attr"`
69 | Value []string `xml:"value"`
70 | }
71 |
72 | type DiscoItems struct {
73 | Jid string
74 | Items []DiscoItem
75 | }
76 |
77 | func clientFeaturesToReturn(features []clientDiscoFeature) []string {
78 | var ret []string
79 |
80 | for _, feature := range features {
81 | ret = append(ret, feature.Var)
82 | }
83 |
84 | return ret
85 | }
86 |
87 | func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity {
88 | var ret []DiscoIdentity
89 |
90 | for _, id := range identities {
91 | ret = append(ret, DiscoIdentity{
92 | Category: id.Category,
93 | Type: id.Type,
94 | Name: id.Name,
95 | })
96 | }
97 |
98 | return ret
99 | }
100 |
101 | func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem {
102 | var ret []DiscoItem
103 | for _, item := range items {
104 | ret = append(ret, DiscoItem{
105 | Jid: item.Jid,
106 | Name: item.Name,
107 | Node: item.Node,
108 | })
109 | }
110 |
111 | return ret
112 | }
113 |
--------------------------------------------------------------------------------
/xmpp_error.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // ErrorServiceUnavailable implements error response about a feature that is not available. Currently implemented for
8 | // xep-0030.
9 | // QueryXmlns is about incoming xmlns attribute in query tag.
10 | // Node is about incoming node attribute in query tag (looks like it used only in disco#commands).
11 | //
12 | // If queried feature is not here on purpose, standards suggest to answer with this stanza.
13 | func (c *Client) ErrorServiceUnavailable(v IQ, queryXmlns, node string) (string, error) {
14 | query := fmt.Sprintf("", node)
18 | } else {
19 | query += "/>"
20 | }
21 |
22 | query += ""
23 | query += ""
24 | query += ""
25 |
26 | return c.RawInformation(
27 | v.To,
28 | v.From,
29 | v.ID,
30 | IQTypeError,
31 | query,
32 | )
33 | }
34 |
35 | // ErrorNotImplemented implements error response about a feature that is not (yet?) implemented.
36 | // Xmlns is about not implemented feature.
37 | //
38 | // If queried feature is not here because of it under development or for similar reasons, standards suggest to answer with
39 | // this stanza.
40 | func (c *Client) ErrorNotImplemented(v IQ, xmlns, feature string) (string, error) {
41 | query := ""
42 | query += ""
43 | query += fmt.Sprintf(
44 | "",
45 | xmlns,
46 | feature,
47 | )
48 | query += ""
49 |
50 | return c.RawInformation(
51 | v.To,
52 | v.From,
53 | v.ID,
54 | IQTypeError,
55 | query,
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/xmpp_information_query.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | const (
8 | IQTypeGet = "get"
9 | IQTypeSet = "set"
10 | IQTypeResult = "result"
11 | IQTypeError = "error"
12 | )
13 |
14 | func (c *Client) Discovery() (string, error) {
15 | // use UUIDv4 for a pseudo random id.
16 | reqID := getUUIDv4()
17 | // Reset ticker for periodic pings if configured.
18 | if c.periodicPings {
19 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
20 | }
21 | return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "")
22 | }
23 |
24 | // Discover information about a node. Empty node queries info about server itself.
25 | func (c *Client) DiscoverNodeInfo(node string) (string, error) {
26 | query := fmt.Sprintf("", XMPPNS_DISCO_INFO, node)
27 | // Reset ticker for periodic pings if configured.
28 | if c.periodicPings {
29 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
30 | }
31 | return c.RawInformation(c.jid, c.domain, getUUIDv4(), IQTypeGet, query)
32 | }
33 |
34 | // Discover information about given item from given jid.
35 | func (c *Client) DiscoverInfo(to string) (string, error) {
36 | query := fmt.Sprintf("", XMPPNS_DISCO_INFO)
37 | // Reset ticker for periodic pings if configured.
38 | if c.periodicPings {
39 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
40 | }
41 | return c.RawInformation(c.jid, to, getUUIDv4(), IQTypeGet, query)
42 | }
43 |
44 | // Discover items that the server exposes
45 | func (c *Client) DiscoverServerItems() (string, error) {
46 | // Reset ticker for periodic pings if configured.
47 | if c.periodicPings {
48 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
49 | }
50 | return c.DiscoverEntityItems(c.domain)
51 | }
52 |
53 | // Discover items that an entity exposes
54 | func (c *Client) DiscoverEntityItems(jid string) (string, error) {
55 | query := fmt.Sprintf("", XMPPNS_DISCO_ITEMS)
56 | // Reset ticker for periodic pings if configured.
57 | if c.periodicPings {
58 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
59 | }
60 | return c.RawInformation(c.jid, jid, getUUIDv4(), IQTypeGet, query)
61 | }
62 |
63 | // RawInformationQuery sends an information query request to the server.
64 | func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
65 | const xmlIQ = "%s\n"
66 | // Reset ticker for periodic pings if configured.
67 | if c.periodicPings {
68 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
69 | }
70 | _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
71 | return id, err
72 | }
73 |
74 | // rawInformation send a IQ request with the payload body to the server
75 | func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) {
76 | const xmlIQ = "%s\n"
77 | // Reset ticker for periodic pings if configured.
78 | if c.periodicPings {
79 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
80 | }
81 | _, err := fmt.Fprintf(c.stanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
82 | return id, err
83 | }
84 |
--------------------------------------------------------------------------------
/xmpp_muc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Flo Lauber . All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // TODO(flo):
6 | // - support password protected MUC rooms
7 | // - cleanup signatures of join/leave functions
8 | package xmpp
9 |
10 | import (
11 | "errors"
12 | "fmt"
13 | "time"
14 | )
15 |
16 | const (
17 | nsMUC = "http://jabber.org/protocol/muc"
18 | nsMUCUser = "http://jabber.org/protocol/muc#user"
19 | NoHistory = 0
20 | CharHistory = 1
21 | StanzaHistory = 2
22 | SecondsHistory = 3
23 | SinceHistory = 4
24 | )
25 |
26 | // Send sends room topic wrapped inside an XMPP message stanza body.
27 | func (c *Client) SendTopic(chat Chat) (n int, err error) {
28 | // Reset ticker for periodic pings if configured.
29 | if c.periodicPings {
30 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
31 | }
32 | return fmt.Fprintf(c.stanzaWriter, ""+"%s\n",
33 | xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
34 | }
35 |
36 | func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
37 | if nick == "" {
38 | nick = c.jid
39 | }
40 | // Reset ticker for periodic pings if configured.
41 | if c.periodicPings {
42 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
43 | }
44 | return fmt.Fprintf(c.stanzaWriter, ""+
45 | ""+
46 | ""+
47 | "\n",
48 | xmlEscape(jid), xmlEscape(nick), nsMUC)
49 | }
50 |
51 | // xep-0045 7.2
52 | func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
53 | if nick == "" {
54 | nick = c.jid
55 | }
56 | switch history_type {
57 | case NoHistory:
58 | // Reset ticker for periodic pings if configured.
59 | if c.periodicPings {
60 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
61 | }
62 | return fmt.Fprintf(c.stanzaWriter, ""+
63 | ""+
64 | "\n",
65 | xmlEscape(jid), xmlEscape(nick), nsMUC)
66 | case CharHistory:
67 | // Reset ticker for periodic pings if configured.
68 | if c.periodicPings {
69 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
70 | }
71 | return fmt.Fprintf(c.stanzaWriter, ""+
72 | ""+
73 | ""+
74 | "\n",
75 | xmlEscape(jid), xmlEscape(nick), nsMUC, history)
76 | case StanzaHistory:
77 | // Reset ticker for periodic pings if configured.
78 | if c.periodicPings {
79 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
80 | }
81 | return fmt.Fprintf(c.stanzaWriter, ""+
82 | ""+
83 | ""+
84 | "\n",
85 | xmlEscape(jid), xmlEscape(nick), nsMUC, history)
86 | case SecondsHistory:
87 | // Reset ticker for periodic pings if configured.
88 | if c.periodicPings {
89 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
90 | }
91 | return fmt.Fprintf(c.stanzaWriter, ""+
92 | ""+
93 | ""+
94 | "\n",
95 | xmlEscape(jid), xmlEscape(nick), nsMUC, history)
96 | case SinceHistory:
97 | if history_date != nil {
98 | // Reset ticker for periodic pings if configured.
99 | if c.periodicPings {
100 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
101 | }
102 | return fmt.Fprintf(c.stanzaWriter, ""+
103 | ""+
104 | ""+
105 | "\n",
106 | xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339))
107 | }
108 | }
109 | return 0, errors.New("unknown history option")
110 | }
111 |
112 | // xep-0045 7.2.6
113 | func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
114 | if nick == "" {
115 | nick = c.jid
116 | }
117 | switch history_type {
118 | case NoHistory:
119 | // Reset ticker for periodic pings if configured.
120 | if c.periodicPings {
121 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
122 | }
123 | return fmt.Fprintf(c.stanzaWriter, ""+
124 | ""+
125 | "%s"+
126 | ""+
127 | "\n",
128 | xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
129 | case CharHistory:
130 | // Reset ticker for periodic pings if configured.
131 | if c.periodicPings {
132 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
133 | }
134 | return fmt.Fprintf(c.stanzaWriter, ""+
135 | ""+
136 | "%s"+
137 | ""+
138 | "\n",
139 | xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
140 | case StanzaHistory:
141 | // Reset ticker for periodic pings if configured.
142 | if c.periodicPings {
143 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
144 | }
145 | return fmt.Fprintf(c.stanzaWriter, ""+
146 | ""+
147 | "%s"+
148 | ""+
149 | "\n",
150 | xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
151 | case SecondsHistory:
152 | // Reset ticker for periodic pings if configured.
153 | if c.periodicPings {
154 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
155 | }
156 | return fmt.Fprintf(c.stanzaWriter, ""+
157 | ""+
158 | "%s"+
159 | ""+
160 | "\n",
161 | xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
162 | case SinceHistory:
163 | if history_date != nil {
164 | // Reset ticker for periodic pings if configured.
165 | if c.periodicPings {
166 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
167 | }
168 | return fmt.Fprintf(c.stanzaWriter, ""+
169 | ""+
170 | "%s"+
171 | ""+
172 | "\n",
173 | xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339))
174 | }
175 | }
176 | return 0, errors.New("unknown history option")
177 | }
178 |
179 | // xep-0045 7.14
180 | func (c *Client) LeaveMUC(jid string) (n int, err error) {
181 | // Reset ticker for periodic pings if configured.
182 | if c.periodicPings {
183 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
184 | }
185 | return fmt.Fprintf(c.stanzaWriter, "\n",
186 | c.jid, xmlEscape(jid))
187 | }
188 |
--------------------------------------------------------------------------------
/xmpp_ping.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func (c *Client) PingC2S(jid, server string) error {
8 | if jid == "" {
9 | jid = c.jid
10 | }
11 | if server == "" {
12 | server = c.domain
13 | }
14 | // Reset ticker for periodic pings if configured.
15 | if c.periodicPings {
16 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
17 | }
18 | _, err := fmt.Fprintf(c.stanzaWriter, ""+
19 | ""+
20 | "\n",
21 | xmlEscape(jid), xmlEscape(server), getUUIDv4())
22 | return err
23 | }
24 |
25 | func (c *Client) PingS2S(fromServer, toServer string) error {
26 | // Reset ticker for periodic pings if configured.
27 | if c.periodicPings {
28 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
29 | }
30 | _, err := fmt.Fprintf(c.stanzaWriter, ""+
31 | ""+
32 | "\n",
33 | xmlEscape(fromServer), xmlEscape(toServer), getUUIDv4())
34 | return err
35 | }
36 |
37 | func (c *Client) SendResultPing(id, toServer string) error {
38 | // Reset ticker for periodic pings if configured.
39 | if c.periodicPings {
40 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
41 | }
42 | _, err := fmt.Fprintf(c.stanzaWriter, "\n",
43 | xmlEscape(toServer), xmlEscape(id))
44 | return err
45 | }
46 |
47 | func (c *Client) sendPeriodicPings() {
48 | for range c.periodicPingTicker.C {
49 | _ = c.PingC2S(c.jid, c.domain)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/xmpp_pubsub.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | )
7 |
8 | const (
9 | XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub"
10 | XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event"
11 | )
12 |
13 | type clientPubsubItem struct {
14 | XMLName xml.Name `xml:"item"`
15 | ID string `xml:"id,attr"`
16 | Body []byte `xml:",innerxml"`
17 | }
18 |
19 | type clientPubsubItems struct {
20 | XMLName xml.Name `xml:"items"`
21 | Node string `xml:"node,attr"`
22 | Items []clientPubsubItem `xml:"item"`
23 | }
24 |
25 | type clientPubsubEvent struct {
26 | XMLName xml.Name `xml:"event"`
27 | XMLNS string `xml:"xmlns,attr"`
28 | Items clientPubsubItems `xml:"items"`
29 | }
30 |
31 | type clientPubsubError struct {
32 | XMLName xml.Name
33 | }
34 |
35 | type clientPubsubSubscription struct {
36 | XMLName xml.Name `xml:"subscription"`
37 | Node string `xml:"node,attr"`
38 | JID string `xml:"jid,attr"`
39 | SubID string `xml:"subid,attr"`
40 | }
41 |
42 | type PubsubEvent struct {
43 | Node string
44 | Items []PubsubItem
45 | }
46 |
47 | type PubsubSubscription struct {
48 | SubID string
49 | JID string
50 | Node string
51 | Errors []string
52 | }
53 | type PubsubUnsubscription PubsubSubscription
54 |
55 | type PubsubItem struct {
56 | ID string
57 | InnerXML []byte
58 | }
59 |
60 | type PubsubItems struct {
61 | Node string
62 | Items []PubsubItem
63 | }
64 |
65 | // Converts []clientPubsubItem to []PubsubItem
66 | func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem {
67 | var tmp []PubsubItem
68 | for _, i := range items {
69 | tmp = append(tmp, PubsubItem{
70 | ID: i.ID,
71 | InnerXML: i.Body,
72 | })
73 | }
74 |
75 | return tmp
76 | }
77 |
78 | func pubsubClientToReturn(event clientPubsubEvent) PubsubEvent {
79 | return PubsubEvent{
80 | Node: event.Items.Node,
81 | Items: pubsubItemsToReturn(event.Items.Items),
82 | }
83 | }
84 |
85 | func pubsubStanza(body string) string {
86 | return fmt.Sprintf("%s",
87 | XMPPNS_PUBSUB, body)
88 | }
89 |
90 | func pubsubSubscriptionStanza(node, jid string) string {
91 | body := fmt.Sprintf("",
92 | xmlEscape(node),
93 | xmlEscape(jid))
94 | return pubsubStanza(body)
95 | }
96 |
97 | func pubsubUnsubscriptionStanza(node, jid string) string {
98 | body := fmt.Sprintf("",
99 | xmlEscape(node),
100 | xmlEscape(jid))
101 | return pubsubStanza(body)
102 | }
103 |
104 | func (c *Client) PubsubSubscribeNode(node, jid string) error {
105 | id := getUUIDv4()
106 | c.subIDs = append(c.subIDs, id)
107 | _, err := c.RawInformation(c.jid,
108 | jid,
109 | id,
110 | "set",
111 | pubsubSubscriptionStanza(node, c.jid))
112 | return err
113 | }
114 |
115 | func (c *Client) PubsubUnsubscribeNode(node, jid string) error {
116 | id := getUUIDv4()
117 | c.unsubIDs = append(c.unsubIDs, id)
118 | _, err := c.RawInformation(c.jid,
119 | jid,
120 | id,
121 | "set",
122 | pubsubUnsubscriptionStanza(node, c.jid))
123 | return err
124 | }
125 |
126 | func (c *Client) PubsubRequestLastItems(node, jid string) error {
127 | id := getUUIDv4()
128 | c.itemsIDs = append(c.itemsIDs, id)
129 | body := fmt.Sprintf("", node)
130 | _, err := c.RawInformation(c.jid, jid, id, "get", pubsubStanza(body))
131 | return err
132 | }
133 |
134 | func (c *Client) PubsubRequestItem(node, jid, id string) error {
135 | stanzaID := getUUIDv4()
136 | c.itemsIDs = append(c.itemsIDs, stanzaID)
137 | body := fmt.Sprintf(" ", node, id)
138 | _, err := c.RawInformation(c.jid, jid, stanzaID, "get", pubsubStanza(body))
139 | return err
140 | }
141 |
--------------------------------------------------------------------------------
/xmpp_subscription.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func (c *Client) ApproveSubscription(jid string) {
8 | // Reset ticker for periodic pings if configured.
9 | if c.periodicPings {
10 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
11 | }
12 | fmt.Fprintf(c.stanzaWriter, "\n",
13 | xmlEscape(jid))
14 | }
15 |
16 | func (c *Client) RevokeSubscription(jid string) {
17 | // Reset ticker for periodic pings if configured.
18 | if c.periodicPings {
19 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
20 | }
21 | fmt.Fprintf(c.stanzaWriter, "\n",
22 | xmlEscape(jid))
23 | }
24 |
25 | // DEPRECATED: Use RevertSubscription instead.
26 | func (c *Client) RetrieveSubscription(jid string) {
27 | c.RevertSubscription(jid)
28 | }
29 |
30 | func (c *Client) RevertSubscription(jid string) {
31 | // Reset ticker for periodic pings if configured.
32 | if c.periodicPings {
33 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
34 | }
35 | fmt.Fprintf(c.conn, "\n",
36 | xmlEscape(jid))
37 | }
38 |
39 | func (c *Client) RequestSubscription(jid string) {
40 | // Reset ticker for periodic pings if configured.
41 | if c.periodicPings {
42 | c.periodicPingTicker.Reset(c.periodicPingPeriod)
43 | }
44 | fmt.Fprintf(c.stanzaWriter, "\n",
45 | xmlEscape(jid))
46 | }
47 |
--------------------------------------------------------------------------------
/xmpp_test.go:
--------------------------------------------------------------------------------
1 | package xmpp
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "io"
7 | "net"
8 | "reflect"
9 | "strings"
10 | "testing"
11 | "time"
12 | )
13 |
14 | type localAddr struct{}
15 |
16 | func (a *localAddr) Network() string {
17 | return "tcp"
18 | }
19 |
20 | func (addr *localAddr) String() string {
21 | return "localhost:5222"
22 | }
23 |
24 | type testConn struct {
25 | *bytes.Buffer
26 | }
27 |
28 | func tConnect(s string) net.Conn {
29 | var conn testConn
30 | conn.Buffer = bytes.NewBufferString(s)
31 | return &conn
32 | }
33 |
34 | func (*testConn) Close() error {
35 | return nil
36 | }
37 |
38 | func (*testConn) LocalAddr() net.Addr {
39 | return &localAddr{}
40 | }
41 |
42 | func (*testConn) RemoteAddr() net.Addr {
43 | return &localAddr{}
44 | }
45 |
46 | func (*testConn) SetDeadline(time.Time) error {
47 | return nil
48 | }
49 |
50 | func (*testConn) SetReadDeadline(time.Time) error {
51 | return nil
52 | }
53 |
54 | func (*testConn) SetWriteDeadline(time.Time) error {
55 | return nil
56 | }
57 |
58 | var text = strings.TrimSpace(`
59 |
60 |
61 | {"random": "<text>"}
62 |
63 |
64 |
65 |
66 | InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
67 |
68 |
69 |
70 | `)
71 |
72 | func TestStanzaError(t *testing.T) {
73 | var c Client
74 | c.conn = tConnect(text)
75 | c.p = xml.NewDecoder(c.conn)
76 | v, err := c.Recv()
77 | if err != nil {
78 | t.Fatalf("Recv() = %v", err)
79 | }
80 |
81 | chat := Chat{
82 | Type: "error",
83 | Other: []string{
84 | "\n\t\t{\"random\": \"\"}\n\t",
85 | "\n\t\t\n\t\t\n\t",
86 | },
87 | OtherElem: []XMLElement{
88 | {
89 | XMLName: xml.Name{Space: "google:mobile:data", Local: "gcm"},
90 | Attr: []xml.Attr{{Name: xml.Name{Space: "", Local: "xmlns"}, Value: "google:mobile:data"}},
91 | InnerXML: "\n\t\t{\"random\": \"<text>\"}\n\t",
92 | },
93 | {
94 | XMLName: xml.Name{Space: "jabber:client", Local: "error"},
95 | Attr: []xml.Attr{{Name: xml.Name{Space: "", Local: "code"}, Value: "400"}, {Name: xml.Name{Space: "", Local: "type"}, Value: "modify"}},
96 | InnerXML: `
97 |
98 |
99 | InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
100 |
101 | `,
102 | },
103 | },
104 | }
105 | if !reflect.DeepEqual(v, chat) {
106 | t.Errorf("Recv() = %#v; want %#v", v, chat)
107 | }
108 | }
109 |
110 | func TestEOFError(t *testing.T) {
111 | var c Client
112 | c.conn = tConnect("")
113 | c.p = xml.NewDecoder(c.conn)
114 | _, err := c.Recv()
115 | if err != io.EOF {
116 | t.Errorf("Recv() did not return io.EOF on end of input stream")
117 | }
118 | }
119 |
120 | var emptyPubSub = strings.TrimSpace(`
121 |
122 |
123 |
124 |
125 |
126 | `)
127 |
128 | func TestEmptyPubsub(t *testing.T) {
129 | var c Client
130 | c.itemsIDs = append(c.itemsIDs, "items3")
131 | c.conn = tConnect(emptyPubSub)
132 | c.p = xml.NewDecoder(c.conn)
133 | m, err := c.Recv()
134 |
135 | switch m.(type) {
136 | case AvatarData:
137 | if err == nil {
138 | t.Errorf("Expected an error to be returned")
139 | }
140 | default:
141 | t.Errorf("Recv() = %v", m)
142 | t.Errorf("Expected a return value of AvatarData")
143 | }
144 | }
145 |
--------------------------------------------------------------------------------