├── .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 | --------------------------------------------------------------------------------