mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2025-11-13 13:23:44 -08:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b84cb796e | ||
|
|
1822089db6 | ||
|
|
7d89353156 | ||
|
|
6aa1e668ee | ||
|
|
47976624c9 | ||
|
|
4efde692a2 | ||
|
|
08878ed4a2 | ||
|
|
ce05c3226c | ||
|
|
3e94880916 | ||
|
|
eda5c23c54 | ||
|
|
a0e74051fd | ||
|
|
83bc8581fd | ||
|
|
8088e3fa7e | ||
|
|
070934743f | ||
|
|
6a25856e85 | ||
|
|
8e1dac6ffa | ||
|
|
21f6a549db | ||
|
|
1d7db9ceee | ||
|
|
0227596f90 | ||
|
|
ebb6e845bf | ||
|
|
a16483397d | ||
|
|
ef2c0b465e | ||
|
|
2f8ec7b36f | ||
|
|
6da1962962 | ||
|
|
33446ad0ba | ||
|
|
390f9b065e | ||
|
|
60e2cdd088 | ||
|
|
a6709a1f71 | ||
|
|
38bdcaec36 | ||
|
|
ffadd331dd | ||
|
|
92329b48e6 | ||
|
|
25fd476328 | ||
|
|
36e153f981 | ||
|
|
d0f2b492ac | ||
|
|
87ff01ac68 | ||
|
|
01d78a1e5c | ||
|
|
8fb3e33a1f | ||
|
|
a189748b9c | ||
|
|
06a76160c8 | ||
|
|
8db608ccc1 | ||
|
|
7fa4b06705 | ||
|
|
f8d0e99696 | ||
|
|
e97d290e2b | ||
|
|
96fccbd399 | ||
|
|
66e219844b | ||
|
|
a3c62e515e | ||
|
|
2781563ea7 | ||
|
|
4f68c5eee2 | ||
|
|
9c8353d081 | ||
|
|
3b66e31888 | ||
|
|
005c8823d9 | ||
|
|
2cdda48467 | ||
|
|
8794ea6ed8 | ||
|
|
7e596fc33c | ||
|
|
6f9808fe16 | ||
|
|
7b1f83f6b7 | ||
|
|
6005a964ba | ||
|
|
d3b45b42a5 | ||
|
|
6e65ba2a0b | ||
|
|
76f59be5ed | ||
|
|
80d2e0fa1e | ||
|
|
2e864ff7f6 | ||
|
|
3de99e0e0e | ||
|
|
e531370dc9 | ||
|
|
4e185f4bb6 | ||
|
|
4f1e0ded97 | ||
|
|
176dcdce33 | ||
|
|
61adf7e414 | ||
|
|
014957e029 | ||
|
|
69118a952a | ||
|
|
1c74d102c7 | ||
|
|
7ab6c3a62d | ||
|
|
a3867dd0b3 | ||
|
|
d2a1329dc6 | ||
|
|
6ff7812ac4 | ||
|
|
3453336f27 | ||
|
|
a23194ad96 | ||
|
|
f984a93e63 | ||
|
|
6a5f2750f1 | ||
|
|
e553028754 | ||
|
|
fed23ad7ad | ||
|
|
244acdc02a | ||
|
|
4d6c783619 | ||
|
|
5697d40e5c | ||
|
|
ff5885f29d | ||
|
|
e3e57ac803 | ||
|
|
3daa5c505c | ||
|
|
0fb90abcf7 | ||
|
|
6aa942dd58 | ||
|
|
c41d068c9f | ||
|
|
9f095cb90f | ||
|
|
7deaf59642 | ||
|
|
fe6cea870d | ||
|
|
323de704f6 | ||
|
|
e05f36c69f | ||
|
|
d36428fb2f | ||
|
|
9577036327 |
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Fluux XMPP Changelog
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Update requirements to go1.13
|
||||||
|
- Add a websocket transport
|
||||||
|
- Add Client.SendIQ method
|
||||||
|
- Add IQ result routes to the Router
|
||||||
|
- Fix SIGSEGV in xmpp_component (#126)
|
||||||
|
- Add tests for Component and code style fixes
|
||||||
|
|
||||||
|
## v0.2.0
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- XMPP Over Websocket support
|
||||||
|
- Add support for getting IQ responses to client IQ queries (synchronously or asynchronously, passing an handler
|
||||||
|
function).
|
||||||
|
- Implement X-OAUTH2 authentication method. You can read more details here:
|
||||||
|
[Understanding ejabberd OAuth Support & Roadmap: Step 4](https://blog.process-one.net/understanding-ejabberd-oauth-support-roadmap/)
|
||||||
|
- Fix issues in the stanza builder when trying to add text inside and XMPP node.
|
||||||
|
- Fix issues with unescaped % characters in XMPP payload.
|
||||||
|
|
||||||
|
### Code migration guide
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.12
|
FROM golang:1.13
|
||||||
WORKDIR /xmpp
|
WORKDIR /xmpp
|
||||||
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
|
RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -8,11 +8,37 @@ The goal is to make simple to write simple XMPP clients and components:
|
|||||||
|
|
||||||
- For automation (like for example monitoring of an XMPP service),
|
- For automation (like for example monitoring of an XMPP service),
|
||||||
- For building connected "things" by plugging them on an XMPP server,
|
- For building connected "things" by plugging them on an XMPP server,
|
||||||
- For writing simple chatbot to control a service or a thing.
|
- For writing simple chatbot to control a service or a thing,
|
||||||
- For writing XMPP servers components.
|
- For writing XMPP servers components.
|
||||||
|
|
||||||
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
|
||||||
|
|
||||||
|
## Configuration and connection
|
||||||
|
|
||||||
|
### Allowing Insecure TLS connection during development
|
||||||
|
|
||||||
|
It is not recommended to disable the check for domain name and certificate chain. Doing so would open your client
|
||||||
|
to man-in-the-middle attacks.
|
||||||
|
|
||||||
|
However, in development, XMPP servers often use self-signed certificates. In that situation, it is better to add the
|
||||||
|
root CA that signed the certificate to your trusted list of root CA. It avoids changing the code and limit the risk
|
||||||
|
of shipping an insecure client to production.
|
||||||
|
|
||||||
|
That said, if you really want to allow your client to trust any TLS certificate, you can customize Go standard
|
||||||
|
`tls.Config` and set it in Config struct.
|
||||||
|
|
||||||
|
Here is an example code to configure a client to allow connecting to a server with self-signed certificate. Note the
|
||||||
|
`InsecureSkipVerify` option. When using this `tls.Config` option, all the checks on the certificate are skipped.
|
||||||
|
|
||||||
|
```go
|
||||||
|
config := xmpp.Config{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("Test"),
|
||||||
|
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Supported specifications
|
## Supported specifications
|
||||||
|
|
||||||
### Clients
|
### Clients
|
||||||
@@ -26,7 +52,9 @@ The library is designed to have minimal dependencies. For now, the library does
|
|||||||
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
- [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html)
|
||||||
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
- [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html)
|
||||||
|
|
||||||
## Stanza subpackage
|
## Package overview
|
||||||
|
|
||||||
|
### Stanza subpackage
|
||||||
|
|
||||||
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
|
XMPP stanzas are basic and extensible XML elements. Stanzas (or sometimes special stanzas called 'nonzas') are used to
|
||||||
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
|
leverage the XMPP protocol features. During a session, a client (or a component) and a server will be exchanging stanzas
|
||||||
@@ -47,6 +75,14 @@ implement your own extensions directly in your own application.
|
|||||||
To learn more about the stanza package, you can read more in the
|
To learn more about the stanza package, you can read more in the
|
||||||
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
|
[stanza package documentation](https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md).
|
||||||
|
|
||||||
|
### Router
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Getting IQ response from server
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
|
We have several [examples](https://github.com/FluuxIO/go-xmpp/tree/master/_examples) to help you get started using
|
||||||
@@ -68,9 +104,11 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := xmpp.Config{
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
Address: "localhost:5222",
|
Address: "localhost:5222",
|
||||||
|
},
|
||||||
Jid: "test@localhost",
|
Jid: "test@localhost",
|
||||||
Password: "test",
|
Credential: xmpp.Password("Test"),
|
||||||
StreamLogger: os.Stdout,
|
StreamLogger: os.Stdout,
|
||||||
Insecure: true,
|
Insecure: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
opts := xmpp.ComponentOptions{
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:9999",
|
||||||
|
Domain: "service.localhost",
|
||||||
|
},
|
||||||
Domain: "service.localhost",
|
Domain: "service.localhost",
|
||||||
Secret: "mypass",
|
Secret: "mypass",
|
||||||
Address: "localhost:9999",
|
|
||||||
|
|
||||||
// TODO: Move that part to a component discovery handler
|
// TODO: Move that part to a component discovery handler
|
||||||
Name: "Test Component",
|
Name: "Test Component",
|
||||||
@@ -99,26 +102,9 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
|
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
|
||||||
// Higher level discovery
|
disco := iqResp.DiscoInfo()
|
||||||
identity := stanza.Identity{
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
Name: opts.Name,
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
Category: opts.Category,
|
|
||||||
Type: opts.Type,
|
|
||||||
}
|
|
||||||
payload := stanza.DiscoInfo{
|
|
||||||
XMLName: xml.Name{
|
|
||||||
Space: stanza.NSDiscoInfo,
|
|
||||||
Local: "query",
|
|
||||||
},
|
|
||||||
Identity: []stanza.Identity{identity},
|
|
||||||
Features: []stanza.Feature{
|
|
||||||
{Var: stanza.NSDiscoInfo},
|
|
||||||
{Var: stanza.NSDiscoItems},
|
|
||||||
{Var: "jabber:iq:version"},
|
|
||||||
{Var: "urn:xmpp:delegation:1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
iqResp.Payload = &payload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func discoInfoPubSub(iqResp *stanza.IQ) {
|
func discoInfoPubSub(iqResp *stanza.IQ) {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
module gosrc.io/xmpp/_examples
|
module gosrc.io/xmpp/_examples
|
||||||
|
|
||||||
go 1.12
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/go-cmp v0.3.0 // indirect
|
|
||||||
github.com/processone/mpg123 v1.0.0
|
github.com/processone/mpg123 v1.0.0
|
||||||
github.com/processone/soundcloud v1.0.0
|
github.com/processone/soundcloud v1.0.0
|
||||||
gosrc.io/xmpp v0.1.1
|
gosrc.io/xmpp v0.1.1
|
||||||
|
|||||||
103
_examples/go.sum
103
_examples/go.sum
@@ -1,10 +1,107 @@
|
|||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||||
github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE=
|
|
||||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
|||||||
3
_examples/muc_bot/README.md
Normal file
3
_examples/muc_bot/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# XMPP Multi-User (MUC) chat bot example
|
||||||
|
|
||||||
|
This code shows how to build a simple basic XMPP Multi-User chat bot using Fluux Go XMPP library.
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
@@ -11,9 +10,12 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
opts := xmpp.ComponentOptions{
|
opts := xmpp.ComponentOptions{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:8888",
|
||||||
|
Domain: "service2.localhost",
|
||||||
|
},
|
||||||
Domain: "service2.localhost",
|
Domain: "service2.localhost",
|
||||||
Secret: "mypass",
|
Secret: "mypass",
|
||||||
Address: "localhost:8888",
|
|
||||||
Name: "Test Component",
|
Name: "Test Component",
|
||||||
Category: "gateway",
|
Category: "gateway",
|
||||||
Type: "service",
|
Type: "service",
|
||||||
@@ -56,30 +58,14 @@ func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
|||||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(stanza.IQ)
|
||||||
if !ok || iq.Type != "get" {
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
identity := stanza.Identity{
|
disco := iqResp.DiscoInfo()
|
||||||
Name: opts.Name,
|
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||||
Category: opts.Category,
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
Type: opts.Type,
|
|
||||||
}
|
|
||||||
payload := stanza.DiscoInfo{
|
|
||||||
XMLName: xml.Name{
|
|
||||||
Space: stanza.NSDiscoInfo,
|
|
||||||
Local: "query",
|
|
||||||
},
|
|
||||||
Identity: []stanza.Identity{identity},
|
|
||||||
Features: []stanza.Feature{
|
|
||||||
{Var: stanza.NSDiscoInfo},
|
|
||||||
{Var: stanza.NSDiscoItems},
|
|
||||||
{Var: "jabber:iq:version"},
|
|
||||||
{Var: "urn:xmpp:delegation:1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
iqResp.Payload = &payload
|
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +73,7 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
|||||||
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||||
// Type conversion & sanity checks
|
// Type conversion & sanity checks
|
||||||
iq, ok := p.(stanza.IQ)
|
iq, ok := p.(stanza.IQ)
|
||||||
if !ok || iq.Type != "get" {
|
if !ok || iq.Type != stanza.IQTypeGet {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,16 +83,11 @@ func discoItems(c xmpp.Sender, p stanza.Packet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
|
items := iqResp.DiscoItems()
|
||||||
|
|
||||||
var payload stanza.DiscoItems
|
|
||||||
if discoItems.Node == "" {
|
if discoItems.Node == "" {
|
||||||
payload = stanza.DiscoItems{
|
items.AddItem("service.localhost", "node1", "test node")
|
||||||
Items: []stanza.DiscoItem{
|
|
||||||
{Name: "test node", JID: "service.localhost", Node: "node1"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
iqResp.Payload = &payload
|
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,9 +99,6 @@ func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||||
var payload stanza.Version
|
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
||||||
payload.Name = "Fluux XMPP Component"
|
|
||||||
payload.Version = "0.0.1"
|
|
||||||
iq.Payload = &payload
|
|
||||||
_ = c.Send(iqResp)
|
_ = c.Send(iqResp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := xmpp.Config{
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
Address: "localhost:5222",
|
Address: "localhost:5222",
|
||||||
|
},
|
||||||
Jid: "test@localhost",
|
Jid: "test@localhost",
|
||||||
Password: "test",
|
Credential: xmpp.Password("test"),
|
||||||
StreamLogger: os.Stdout,
|
StreamLogger: os.Stdout,
|
||||||
Insecure: true,
|
Insecure: true,
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
router := xmpp.NewRouter()
|
router := xmpp.NewRouter()
|
||||||
@@ -47,6 +50,3 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
|||||||
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||||
_ = s.Send(reply)
|
_ = s.Send(reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
|
|
||||||
// (using templates ?)
|
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ func main() {
|
|||||||
|
|
||||||
// 2. Prepare XMPP client
|
// 2. Prepare XMPP client
|
||||||
config := xmpp.Config{
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
Address: *address,
|
Address: *address,
|
||||||
|
},
|
||||||
Jid: *jid,
|
Jid: *jid,
|
||||||
Password: *password,
|
Credential: xmpp.Password(*password),
|
||||||
// StreamLogger: os.Stdout,
|
// StreamLogger: os.Stdout,
|
||||||
Insecure: true,
|
Insecure: true,
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,7 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
|||||||
|
|
||||||
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
||||||
tune := stanza.Tune{Artist: artist, Title: title}
|
tune := stanza.Tune{Artist: artist, Title: title}
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
|
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "usertune-1", Lang: "en"})
|
||||||
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
|
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
|
||||||
iq.Payload = &payload
|
iq.Payload = &payload
|
||||||
_ = s.Send(iq)
|
_ = s.Send(iq)
|
||||||
|
|||||||
50
_examples/xmpp_oauth2/xmpp_oauth2.go
Normal file
50
_examples/xmpp_oauth2/xmpp_oauth2.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
xmpp_oauth2 is a demo client that connect on an XMPP server using OAuth2 and prints received messages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "localhost:5222",
|
||||||
|
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
// Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(config, router)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
}
|
||||||
48
_examples/xmpp_websocket/xmpp_websocket.go
Normal file
48
_examples/xmpp_websocket/xmpp_websocket.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
xmpp_websocket is a demo client that connect on an XMPP server using websocket and prints received messages.ß
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: "wss://localhost:5443/ws",
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: xmpp.Password("test"),
|
||||||
|
StreamLogger: os.Stdout,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := xmpp.NewRouter()
|
||||||
|
router.HandleFunc("message", handleMessage)
|
||||||
|
|
||||||
|
client, err := xmpp.NewClient(config, router)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you pass the client to a connection manager, it will handle the reconnect policy
|
||||||
|
// for you automatically.
|
||||||
|
cm := xmpp.NewStreamManager(client, nil)
|
||||||
|
log.Fatal(cm.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||||
|
msg, ok := p.(stanza.Message)
|
||||||
|
if !ok {
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||||
|
}
|
||||||
67
auth.go
67
auth.go
@@ -10,29 +10,60 @@ import (
|
|||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
|
|
||||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
|
// Credential is used to pass the type of secret that will be used to connect to XMPP server.
|
||||||
// TODO: Implement other type of SASL Authentication
|
// It can be either a password or an OAuth 2 bearer token.
|
||||||
havePlain := false
|
type Credential struct {
|
||||||
for _, m := range f.Mechanisms.Mechanism {
|
secret string
|
||||||
if m == "PLAIN" {
|
mechanisms []string
|
||||||
havePlain = true
|
}
|
||||||
|
|
||||||
|
func Password(pwd string) Credential {
|
||||||
|
credential := Credential{
|
||||||
|
secret: pwd,
|
||||||
|
mechanisms: []string{"PLAIN"},
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
func OAuthToken(token string) Credential {
|
||||||
|
credential := Credential{
|
||||||
|
secret: token,
|
||||||
|
mechanisms: []string{"X-OAUTH2"},
|
||||||
|
}
|
||||||
|
return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Authentication flow for SASL mechanisms
|
||||||
|
|
||||||
|
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, credential Credential) (err error) {
|
||||||
|
var matchingMech string
|
||||||
|
for _, mech := range credential.mechanisms {
|
||||||
|
if isSupportedMech(mech, f.Mechanisms.Mechanism) {
|
||||||
|
matchingMech = mech
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !havePlain {
|
|
||||||
err := fmt.Errorf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism)
|
switch matchingMech {
|
||||||
|
case "PLAIN", "X-OAUTH2":
|
||||||
|
// TODO: Implement other type of SASL mechanisms
|
||||||
|
return authPlain(socket, decoder, matchingMech, user, credential.secret)
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("no matching authentication (%v) supported by server: %v", credential.mechanisms, f.Mechanisms.Mechanism)
|
||||||
return NewConnError(err, true)
|
return NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return authPlain(socket, decoder, user, password)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plain authentication: send base64-encoded \x00 user \x00 password
|
// Plain authentication: send base64-encoded \x00 user \x00 password
|
||||||
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
|
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error {
|
||||||
raw := "\x00" + user + "\x00" + password
|
raw := "\x00" + user + "\x00" + secret
|
||||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
|
_, err := fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Next message should be either success or failure.
|
// Next message should be either success or failure.
|
||||||
val, err := stanza.NextPacket(decoder)
|
val, err := stanza.NextPacket(decoder)
|
||||||
@@ -51,3 +82,13 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSupportedMech returns true if the mechanism is supported in the provided list.
|
||||||
|
func isSupportedMech(mech string, mechanisms []string) bool {
|
||||||
|
for _, m := range mechanisms {
|
||||||
|
if mech == m {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (c *ServerCheck) Check() error {
|
|||||||
decoder := xml.NewDecoder(tcpconn)
|
decoder := xml.NewDecoder(tcpconn)
|
||||||
|
|
||||||
// Send stream open tag
|
// Send stream open tag
|
||||||
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil {
|
if _, err = fmt.Fprintf(tcpconn, clientStreamOpen, c.domain); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,15 +79,19 @@ func (c *ServerCheck) Check() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := f.DoesStartTLS(); ok {
|
if _, ok := f.DoesStartTLS(); ok {
|
||||||
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
_, err = fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var k stanza.TLSProceed
|
var k stanza.TLSProceed
|
||||||
if err = decoder.DecodeElement(&k, nil); err != nil {
|
if err = decoder.DecodeElement(&k, nil); err != nil {
|
||||||
return fmt.Errorf("expecting starttls proceed: %s", err)
|
return fmt.Errorf("expecting starttls proceed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stanza.DefaultTlsConfig.ServerName = c.domain
|
var tlsConfig tls.Config
|
||||||
tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
|
tlsConfig.ServerName = c.domain
|
||||||
|
tlsConn := tls.Client(tcpconn, &tlsConfig)
|
||||||
// We convert existing connection to TLS
|
// We convert existing connection to TLS
|
||||||
if err = tlsConn.Handshake(); err != nil {
|
if err = tlsConn.Handshake(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
167
client.go
167
client.go
@@ -1,9 +1,11 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ const (
|
|||||||
StateConnected
|
StateConnected
|
||||||
StateSessionEstablished
|
StateSessionEstablished
|
||||||
StateStreamError
|
StateStreamError
|
||||||
|
StatePermanentError
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event is a structure use to convey event changes related to client state. This
|
// Event is a structure use to convey event changes related to client state. This
|
||||||
@@ -31,11 +34,23 @@ type Event struct {
|
|||||||
State ConnState
|
State ConnState
|
||||||
Description string
|
Description string
|
||||||
StreamError string
|
StreamError string
|
||||||
|
SMState SMState
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMState holds Stream Management information regarding the session that can be
|
||||||
|
// used to resume session after disconnect
|
||||||
|
type SMState struct {
|
||||||
|
// Stream Management ID
|
||||||
|
Id string
|
||||||
|
// Inbound stanza count
|
||||||
|
Inbound uint
|
||||||
|
// TODO Store location for IP affinity
|
||||||
|
// TODO Store max and timestamp, to check if we should retry resumption or not
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventHandler is use to pass events about state of the connection to
|
// EventHandler is use to pass events about state of the connection to
|
||||||
// client implementation.
|
// client implementation.
|
||||||
type EventHandler func(Event)
|
type EventHandler func(Event) error
|
||||||
|
|
||||||
type EventManager struct {
|
type EventManager struct {
|
||||||
// Store current state
|
// Store current state
|
||||||
@@ -52,6 +67,13 @@ func (em EventManager) updateState(state ConnState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (em EventManager) disconnected(state SMState) {
|
||||||
|
em.CurrentState = StateDisconnected
|
||||||
|
if em.Handler != nil {
|
||||||
|
em.Handler(Event{State: em.CurrentState, SMState: state})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (em EventManager) streamError(error, desc string) {
|
func (em EventManager) streamError(error, desc string) {
|
||||||
em.CurrentState = StateStreamError
|
em.CurrentState = StateStreamError
|
||||||
if em.Handler != nil {
|
if em.Handler != nil {
|
||||||
@@ -62,6 +84,8 @@ func (em EventManager) streamError(error, desc string) {
|
|||||||
// Client
|
// Client
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ stanzas")
|
||||||
|
|
||||||
// Client is the main structure used to connect as a client on an XMPP
|
// Client is the main structure used to connect as a client on an XMPP
|
||||||
// server.
|
// server.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -69,8 +93,7 @@ type Client struct {
|
|||||||
config Config
|
config Config
|
||||||
// Session gather data that can be accessed by users of this library
|
// Session gather data that can be accessed by users of this library
|
||||||
Session *Session
|
Session *Session
|
||||||
// TCP level connection / can be replaced by a TLS session after starttls
|
transport Transport
|
||||||
conn net.Conn
|
|
||||||
// Router is used to dispatch packets
|
// Router is used to dispatch packets
|
||||||
router *Router
|
router *Router
|
||||||
// Track and broadcast connection state
|
// Track and broadcast connection state
|
||||||
@@ -91,17 +114,29 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
|||||||
return nil, NewConnError(err, true)
|
return nil, NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Password == "" {
|
if config.Credential.secret == "" {
|
||||||
err = errors.New("missing password")
|
err = errors.New("missing credential")
|
||||||
return nil, NewConnError(err, true)
|
return nil, NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to jid domain
|
// Fallback to jid domain
|
||||||
if config.Address == "" {
|
if config.Address == "" {
|
||||||
config.Address = config.parsedJid.Domain
|
config.Address = config.parsedJid.Domain
|
||||||
}
|
|
||||||
config.Address = ensurePort(config.Address, 5222)
|
|
||||||
|
|
||||||
|
// Fetch SRV DNS-Entries
|
||||||
|
_, srvEntries, err := net.LookupSRV("xmpp-client", "tcp", config.parsedJid.Domain)
|
||||||
|
|
||||||
|
if err == nil && len(srvEntries) > 0 {
|
||||||
|
// If we found matching DNS records, use the entry with highest weight
|
||||||
|
bestSrv := srvEntries[0]
|
||||||
|
for _, srv := range srvEntries {
|
||||||
|
if srv.Priority <= bestSrv.Priority && srv.Weight >= bestSrv.Weight {
|
||||||
|
bestSrv = srv
|
||||||
|
config.Address = ensurePort(srv.Target, int(srv.Port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
c = new(Client)
|
c = new(Client)
|
||||||
c.config = config
|
c.config = config
|
||||||
c.router = r
|
c.router = r
|
||||||
@@ -110,44 +145,68 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
|||||||
c.config.ConnectTimeout = 15 // 15 second as default
|
c.config.ConnectTimeout = 15 // 15 second as default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.TransportConfiguration.Domain == "" {
|
||||||
|
config.TransportConfiguration.Domain = config.parsedJid.Domain
|
||||||
|
}
|
||||||
|
c.transport = NewClientTransport(config.TransportConfiguration)
|
||||||
|
|
||||||
|
if config.StreamLogger != nil {
|
||||||
|
c.transport.LogTraffic(config.StreamLogger)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
// Connect triggers actual TCP connection, based on previously defined parameters.
|
||||||
|
// Connect simply triggers resumption, with an empty session state.
|
||||||
func (c *Client) Connect() error {
|
func (c *Client) Connect() error {
|
||||||
|
var state SMState
|
||||||
|
return c.Resume(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||||
|
// state.
|
||||||
|
func (c *Client) Resume(state SMState) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
c.conn, err = net.DialTimeout("tcp", c.config.Address, time.Duration(c.config.ConnectTimeout)*time.Second)
|
streamId, err := c.transport.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.updateState(StateConnected)
|
c.updateState(StateConnected)
|
||||||
|
|
||||||
// Client is ok, we now open XMPP session
|
// Client is ok, we now open XMPP session
|
||||||
if c.conn, c.Session, err = NewSession(c.conn, c.config); err != nil {
|
if c.Session, err = NewSession(c.transport, c.config, state); err != nil {
|
||||||
|
c.transport.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
c.Session.StreamId = streamId
|
||||||
c.updateState(StateSessionEstablished)
|
c.updateState(StateSessionEstablished)
|
||||||
|
|
||||||
|
// Start the keepalive go routine
|
||||||
|
keepaliveQuit := make(chan struct{})
|
||||||
|
go keepalive(c.transport, keepaliveQuit)
|
||||||
|
// Start the receiver go routine
|
||||||
|
state = c.Session.SMState
|
||||||
|
// Leaving this channel here for later. Not used atm. We should return this instead of an error because right
|
||||||
|
// now the returned error is lost in limbo.
|
||||||
|
errChan := make(chan error)
|
||||||
|
go c.recv(state, keepaliveQuit, errChan)
|
||||||
|
|
||||||
// We're connected and can now receive and send messages.
|
// We're connected and can now receive and send messages.
|
||||||
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
||||||
// TODO: Do we always want to send initial presence automatically ?
|
// TODO: Do we always want to send initial presence automatically ?
|
||||||
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
||||||
fmt.Fprintf(c.Session.streamLogger, "<presence/>")
|
_, err = fmt.Fprintf(c.transport, "<presence/>")
|
||||||
|
|
||||||
// Start the keepalive go routine
|
|
||||||
keepaliveQuit := make(chan struct{})
|
|
||||||
go keepalive(c.conn, keepaliveQuit)
|
|
||||||
// Start the receiver go routine
|
|
||||||
go c.recv(keepaliveQuit)
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Disconnect() {
|
func (c *Client) Disconnect() {
|
||||||
_ = c.SendRaw("</stream:stream>")
|
|
||||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
||||||
_ = c.conn.Close()
|
if c.transport != nil {
|
||||||
|
_ = c.transport.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetHandler(handler EventHandler) {
|
func (c *Client) SetHandler(handler EventHandler) {
|
||||||
@@ -156,7 +215,7 @@ func (c *Client) SetHandler(handler EventHandler) {
|
|||||||
|
|
||||||
// Send marshals XMPP stanza and sends it to the server.
|
// Send marshals XMPP stanza and sends it to the server.
|
||||||
func (c *Client) Send(packet stanza.Packet) error {
|
func (c *Client) Send(packet stanza.Packet) error {
|
||||||
conn := c.conn
|
conn := c.transport
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return errors.New("client is not connected")
|
return errors.New("client is not connected")
|
||||||
}
|
}
|
||||||
@@ -166,7 +225,26 @@ func (c *Client) Send(packet stanza.Packet) error {
|
|||||||
return errors.New("cannot marshal packet " + err.Error())
|
return errors.New("cannot marshal packet " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.sendWithLogger(string(data))
|
return c.sendWithWriter(c.transport, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendIQ sends an IQ set or get stanza to the server. If a result is received
|
||||||
|
// the provided handler function will automatically be called.
|
||||||
|
//
|
||||||
|
// The provided context should have a timeout to prevent the client from waiting
|
||||||
|
// forever for an IQ result. For example:
|
||||||
|
//
|
||||||
|
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||||
|
// result := <- client.SendIQ(ctx, iq)
|
||||||
|
//
|
||||||
|
func (c *Client) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
|
||||||
|
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||||
|
return nil, ErrCanOnlySendGetOrSetIq
|
||||||
|
}
|
||||||
|
if err := c.Send(iq); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendRaw sends an XMPP stanza as a string to the server.
|
// SendRaw sends an XMPP stanza as a string to the server.
|
||||||
@@ -174,17 +252,17 @@ func (c *Client) Send(packet stanza.Packet) error {
|
|||||||
// disconnect the client. It is up to the user of this method to
|
// disconnect the client. It is up to the user of this method to
|
||||||
// carefully craft the XML content to produce valid XMPP.
|
// carefully craft the XML content to produce valid XMPP.
|
||||||
func (c *Client) SendRaw(packet string) error {
|
func (c *Client) SendRaw(packet string) error {
|
||||||
conn := c.conn
|
conn := c.transport
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return errors.New("client is not connected")
|
return errors.New("client is not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.sendWithLogger(packet)
|
return c.sendWithWriter(c.transport, []byte(packet))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) sendWithLogger(packet string) error {
|
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||||
var err error
|
var err error
|
||||||
_, err = fmt.Fprintf(c.Session.streamLogger, packet)
|
_, err = writer.Write(packet)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,13 +270,14 @@ func (c *Client) sendWithLogger(packet string) error {
|
|||||||
// Go routines
|
// Go routines
|
||||||
|
|
||||||
// Loop: Receive data from server
|
// Loop: Receive data from server
|
||||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan<- error) {
|
||||||
for {
|
for {
|
||||||
val, err := stanza.NextPacket(c.Session.decoder)
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
close(keepaliveQuit)
|
close(keepaliveQuit)
|
||||||
c.updateState(StateDisconnected)
|
c.disconnected(state)
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stream errors
|
// Handle stream errors
|
||||||
@@ -207,26 +286,42 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
|||||||
c.router.route(c, val)
|
c.router.route(c, val)
|
||||||
close(keepaliveQuit)
|
close(keepaliveQuit)
|
||||||
c.streamError(packet.Error.Local, packet.Text)
|
c.streamError(packet.Error.Local, packet.Text)
|
||||||
return errors.New("stream error: " + packet.Error.Local)
|
errChan <- errors.New("stream error: " + packet.Error.Local)
|
||||||
|
return
|
||||||
|
// Process Stream management nonzas
|
||||||
|
case stanza.SMRequest:
|
||||||
|
answer := stanza.SMAnswer{XMLName: xml.Name{
|
||||||
|
Space: stanza.NSStreamManagement,
|
||||||
|
Local: "a",
|
||||||
|
}, H: state.Inbound}
|
||||||
|
err = c.Send(answer)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
c.router.route(c, val)
|
state.Inbound++
|
||||||
|
}
|
||||||
|
// Do normal route processing in a go-routine so we can immediately
|
||||||
|
// start receiving other stanzas. This also allows route handlers to
|
||||||
|
// send and receive more stanzas.
|
||||||
|
go c.router.route(c, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop: send whitespace keepalive to server
|
// Loop: send whitespace keepalive to server
|
||||||
// This is use to keep the connection open, but also to detect connection loss
|
// This is use to keep the connection open, but also to detect connection loss
|
||||||
// and trigger proper client connection shutdown.
|
// and trigger proper client connection shutdown.
|
||||||
func keepalive(conn net.Conn, quit <-chan struct{}) {
|
func keepalive(transport Transport, quit <-chan struct{}) {
|
||||||
// TODO: Make keepalive interval configurable
|
// TODO: Make keepalive interval configurable
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if n, err := fmt.Fprintf(conn, "\n"); err != nil || n != 1 {
|
if err := transport.Ping(); err != nil {
|
||||||
// When keep alive fails, we force close the connection. In all cases, the recv will also fail.
|
// When keepalive fails, we force close the transport. In all cases, the recv will also fail.
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
_ = conn.Close()
|
_ = transport.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case <-quit:
|
case <-quit:
|
||||||
|
|||||||
19
client_internal_test.go
Normal file
19
client_internal_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_Send(t *testing.T) {
|
||||||
|
buffer := bytes.NewBufferString("")
|
||||||
|
client := Client{}
|
||||||
|
data := []byte("https://da.wikipedia.org/wiki/J%C3%A6vnd%C3%B8gn")
|
||||||
|
if err := client.sendWithWriter(buffer, data); err != nil {
|
||||||
|
t.Errorf("Writing failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.String() != string(data) {
|
||||||
|
t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,13 @@ func TestClient_Connect(t *testing.T) {
|
|||||||
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
|
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true}
|
||||||
|
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
@@ -47,7 +53,13 @@ func TestClient_NoInsecure(t *testing.T) {
|
|||||||
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
}
|
||||||
|
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
@@ -71,7 +83,13 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
|||||||
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
mock.Start(t, testXMPPAddress, handlerAbortTLS)
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
}
|
||||||
|
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
@@ -94,7 +112,14 @@ func TestClient_RFC3921Session(t *testing.T) {
|
|||||||
mock.Start(t, testXMPPAddress, handlerConnectWithSession)
|
mock.Start(t, testXMPPAddress, handlerConnectWithSession)
|
||||||
|
|
||||||
// Test / Check result
|
// Test / Check result
|
||||||
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
|
config := Config{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testXMPPAddress,
|
||||||
|
},
|
||||||
|
Jid: "test@localhost",
|
||||||
|
Credential: Password("test"),
|
||||||
|
Insecure: true,
|
||||||
|
}
|
||||||
|
|
||||||
var client *Client
|
var client *Client
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
198
cmd/fluuxmpp/README.md
Normal file
198
cmd/fluuxmpp/README.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# fluuxmpp
|
||||||
|
|
||||||
|
fluuxIO's XMPP command-line tool
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install `fluuxmpp` in your Go path:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get -u gosrc.io/xmpp/cmd/fluuxmpp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp --help
|
||||||
|
fluuxIO's xmpp comandline tool
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fluuxmpp [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
|
||||||
|
help Help about any command
|
||||||
|
send is a command-line tool to send to send XMPP messages to users
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for fluuxmpp
|
||||||
|
|
||||||
|
Use "fluuxmpp [command] --help" for more information about a command.
|
||||||
|
```
|
||||||
|
|
||||||
|
### check tls
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp check --help
|
||||||
|
is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fluuxmpp check <host[:port]> [flags]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-d, --domain string domain if host handle multiple domains
|
||||||
|
-h, --help help for check
|
||||||
|
```
|
||||||
|
|
||||||
|
### sending messages
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp send --help
|
||||||
|
is a command-line tool to send to send XMPP messages to users
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fluuxmpp send <recipient,> [message] [flags]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
fluuxmpp send to@chat.sum7.eu "Hello World!"
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--addr string host[:port]
|
||||||
|
--config string config file (default is ~/.config/fluuxmpp.yml)
|
||||||
|
-h, --help help for send
|
||||||
|
--jid string using jid (required)
|
||||||
|
-m, --muc recipient is a muc (join it before sending messages)
|
||||||
|
--password string using password for your jid (required)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### check tls
|
||||||
|
|
||||||
|
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp check chat.sum7.eu
|
||||||
|
info All checks passed
|
||||||
|
⇢ address="chat.sum7.eu" domain=""
|
||||||
|
⇢ main.go:43 main.runCheck
|
||||||
|
⇢ 2019-07-16T22:01:39.765+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass the port and the XMPP domain if different from the server hostname:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de
|
||||||
|
info All checks passed
|
||||||
|
⇢ address="chat.sum7.eu:5222" domain="meckerspace.de"
|
||||||
|
⇢ main.go:43 main.runCheck
|
||||||
|
⇢ 2019-07-16T22:01:33.270+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Error code will be non-zero in case of error. You can thus use it directly with your usual
|
||||||
|
monitoring scripts.
|
||||||
|
|
||||||
|
|
||||||
|
### sending messages
|
||||||
|
|
||||||
|
Message from arguments:
|
||||||
|
```bash
|
||||||
|
$ fluuxmpp send to@example.org "Hello World!"
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:42:43.310+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Hello World!" to="to@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:42:43.310+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Message from STDIN:
|
||||||
|
```bash
|
||||||
|
$ journalctl -f | fluuxmpp send to@example.org -
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:40:03.177+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="to@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:40:03.178+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Jul 17 23:36:46 RECHNERNAME systemd[755]: Started Fetch mails." to="to@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:40:03.178+02:00
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Multiple recipients:
|
||||||
|
```bash
|
||||||
|
$ fluuxmpp send to1@example.org,to2@example.org "Multiple recipient"
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:47:57.650+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Multiple recipient" to="to1@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:47:57.651+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=false text="Multiple recipient" to="to2@example.org"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:47:57.652+02:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Send to MUC:
|
||||||
|
```bash
|
||||||
|
journalctl -f | fluuxmpp send testit@conference.chat.sum7.eu - --muc
|
||||||
|
info client connected
|
||||||
|
⇢ cmd.go:56 main.glob..func1.1
|
||||||
|
⇢ 2019-07-17T23:52:56.269+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=true text="-- Logs begin at Mon 2019-07-08 22:16:54 CEST. --" to="testit@conference.chat.sum7.eu"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:52:56.270+02:00
|
||||||
|
info send message
|
||||||
|
⇢ muc=true text="Jul 17 23:48:58 RECHNERNAME systemd[755]: mail.service: Succeeded." to="testit@conference.chat.sum7.eu"
|
||||||
|
⇢ send.go:31 main.send
|
||||||
|
⇢ 2019-07-17T23:52:56.277+02:00
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentification
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
|
In `/etc/`, `~/.config` and `.` (here).
|
||||||
|
You could create the file name `fluuxmpp` with you favorite file extension (e.g. `toml`, `yml`).
|
||||||
|
|
||||||
|
e.g. ~/.config/fluuxmpp.toml
|
||||||
|
```toml
|
||||||
|
jid = "bot@example.org"
|
||||||
|
password = "secret"
|
||||||
|
|
||||||
|
addr = "example.com:5222"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FLUXXMPP_JID='bot@example.org';
|
||||||
|
export FLUXXMPP_PASSWORD='secret';
|
||||||
|
|
||||||
|
export FLUXXMPP_ADDR='example.com:5222';
|
||||||
|
|
||||||
|
fluuxmpp send to@example.org "Hello Welt";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Warning: This should not be used for production systems, as all users on the system
|
||||||
|
can read the running processes, and their parameters (and thus the password).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fluuxmpp send to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
|
||||||
|
```
|
||||||
21
cmd/fluuxmpp/TODO.md
Normal file
21
cmd/fluuxmpp/TODO.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## check
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Use a config file to define the checks to perform as client on an XMPP server.
|
||||||
|
|
||||||
|
## send
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
- Remove global variable (like mucToleave)
|
||||||
|
- Does not report error when trying to connect to a non open port (for example localhost with no server running).
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- configuration
|
||||||
|
- allow unencrypted
|
||||||
|
- skip tls verification
|
||||||
|
- support muc and single user at same time
|
||||||
|
- send html -> parse console colors to xhtml (is there a easy way or lib for it ?)
|
||||||
41
cmd/fluuxmpp/check.go
Normal file
41
cmd/fluuxmpp/check.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var domain = ""
|
||||||
|
var cmdCheck = &cobra.Command{
|
||||||
|
Use: "check <host[:port]>",
|
||||||
|
Short: "is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires",
|
||||||
|
Example: "fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runCheck(args[0], domain)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdRoot.AddCommand(cmdCheck)
|
||||||
|
cmdCheck.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCheck(address, domain string) {
|
||||||
|
logger := log.WithFields(map[string]interface{}{
|
||||||
|
"address": address,
|
||||||
|
"domain": domain,
|
||||||
|
})
|
||||||
|
client, err := xmpp.NewChecker(address, domain)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Check(); err != nil {
|
||||||
|
logger.Fatal("Failed connection check: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Println("All checks passed")
|
||||||
|
}
|
||||||
5
cmd/fluuxmpp/doc.go
Normal file
5
cmd/fluuxmpp/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
fluuxmpp: fluuxIO's xmpp comandline tool
|
||||||
|
*/
|
||||||
|
package main
|
||||||
34
cmd/fluuxmpp/log.go
Normal file
34
cmd/fluuxmpp/log.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
stdLogger "github.com/bdlm/std/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hook struct{}
|
||||||
|
|
||||||
|
func (h *hook) Fire(entry *log.Entry) error {
|
||||||
|
switch entry.Level {
|
||||||
|
case log.PanicLevel:
|
||||||
|
entry.Logger.Out = os.Stderr
|
||||||
|
case log.FatalLevel:
|
||||||
|
entry.Logger.Out = os.Stderr
|
||||||
|
case log.ErrorLevel:
|
||||||
|
entry.Logger.Out = os.Stderr
|
||||||
|
case log.WarnLevel:
|
||||||
|
entry.Logger.Out = os.Stdout
|
||||||
|
case log.InfoLevel:
|
||||||
|
entry.Logger.Out = os.Stdout
|
||||||
|
case log.DebugLevel:
|
||||||
|
entry.Logger.Out = os.Stdout
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hook) Levels() []stdLogger.Level {
|
||||||
|
return log.AllLevels
|
||||||
|
}
|
||||||
19
cmd/fluuxmpp/main.go
Normal file
19
cmd/fluuxmpp/main.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdRoot represents the base command when called without any subcommands
|
||||||
|
var cmdRoot = &cobra.Command{
|
||||||
|
Use: "fluuxmpp",
|
||||||
|
Short: "fluuxIO's xmpp comandline tool",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.AddHook(&hook{})
|
||||||
|
if err := cmdRoot.Execute(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
136
cmd/fluuxmpp/send.go
Normal file
136
cmd/fluuxmpp/send.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configFile = ""
|
||||||
|
|
||||||
|
// FIXME: Remove global variables
|
||||||
|
var isMUCRecipient = false
|
||||||
|
|
||||||
|
var cmdSend = &cobra.Command{
|
||||||
|
Use: "send <recipient,> [message]",
|
||||||
|
Short: "is a command-line tool to send to send XMPP messages to users",
|
||||||
|
Example: `fluuxmpp send to@chat.sum7.eu "Hello World!"`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: sendxmpp,
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendxmpp(cmd *cobra.Command, args []string) {
|
||||||
|
receiver := strings.Split(args[0], ",")
|
||||||
|
msgText := args[1]
|
||||||
|
|
||||||
|
var err error
|
||||||
|
client, err := xmpp.NewClient(xmpp.Config{
|
||||||
|
TransportConfiguration: xmpp.TransportConfiguration{
|
||||||
|
Address: viper.GetString("addr"),
|
||||||
|
},
|
||||||
|
Jid: viper.GetString("jid"),
|
||||||
|
Credential: xmpp.Password(viper.GetString("password")),
|
||||||
|
}, xmpp.NewRouter())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error when starting xmpp client: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// FIXME: Remove global variables
|
||||||
|
var mucsToLeave []*xmpp.Jid
|
||||||
|
|
||||||
|
cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
log.Info("client connected")
|
||||||
|
|
||||||
|
if isMUCRecipient {
|
||||||
|
for _, muc := range receiver {
|
||||||
|
jid, err := xmpp.NewJid(muc)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
jid.Resource = "sendxmpp"
|
||||||
|
|
||||||
|
if err := joinMUC(c, jid); err != nil {
|
||||||
|
log.WithField("muc", muc).Errorf("error joining muc: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mucsToLeave = append(mucsToLeave, jid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgText != "-" {
|
||||||
|
send(c, receiver, msgText)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
send(c, receiver, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Errorf("error on reading stdin: %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
err := cm.Run()
|
||||||
|
log.Panic("closed connection:", err)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
leaveMUCs(client, mucsToLeave)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdRoot.AddCommand(cmdSend)
|
||||||
|
|
||||||
|
cobra.OnInitialize(initConfigFile)
|
||||||
|
cmdSend.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluuxmpp.yml)")
|
||||||
|
|
||||||
|
cmdSend.Flags().StringP("jid", "", "", "using jid (required)")
|
||||||
|
viper.BindPFlag("jid", cmdSend.Flags().Lookup("jid"))
|
||||||
|
|
||||||
|
cmdSend.Flags().StringP("password", "", "", "using password for your jid (required)")
|
||||||
|
viper.BindPFlag("password", cmdSend.Flags().Lookup("password"))
|
||||||
|
|
||||||
|
cmdSend.Flags().StringP("addr", "", "", "host[:port]")
|
||||||
|
viper.BindPFlag("addr", cmdSend.Flags().Lookup("addr"))
|
||||||
|
|
||||||
|
cmdSend.Flags().BoolVarP(&isMUCRecipient, "muc", "m", false, "recipient is a muc (join it before sending messages)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig reads in config file and ENV variables if set.
|
||||||
|
func initConfigFile() {
|
||||||
|
if configFile != "" {
|
||||||
|
viper.SetConfigFile(configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigName("fluuxmpp")
|
||||||
|
viper.AddConfigPath("/etc/")
|
||||||
|
viper.AddConfigPath("$HOME/.config")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
|
||||||
|
viper.SetEnvPrefix("FLUXXMPP")
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
// If a config file is found, read it in.
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
log.Warnf("no configuration found (somebody could read your password from process argument list): %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
cmd/fluuxmpp/xmppmuc.go
Normal file
28
cmd/fluuxmpp/xmppmuc.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error {
|
||||||
|
return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()},
|
||||||
|
Extensions: []stanza.PresExtension{
|
||||||
|
stanza.MucPresence{
|
||||||
|
History: stanza.History{MaxStanzas: stanza.NewNullableInt(0)},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.Jid) {
|
||||||
|
for _, muc := range mucsToLeave {
|
||||||
|
if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{
|
||||||
|
To: muc.Full(),
|
||||||
|
Type: stanza.PresenceTypeUnavailable,
|
||||||
|
}}); err != nil {
|
||||||
|
log.WithField("muc", muc).Errorf("error on leaving muc: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
cmd/fluuxmpp/xmppsend.go
Normal file
36
cmd/fluuxmpp/xmppsend.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bdlm/log"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
func send(c xmpp.Sender, recipient []string, msgText string) {
|
||||||
|
msg := stanza.Message{
|
||||||
|
Attrs: stanza.Attrs{Type: stanza.MessageTypeChat},
|
||||||
|
Body: msgText,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMUCRecipient {
|
||||||
|
msg.Type = stanza.MessageTypeGroupchat
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, to := range recipient {
|
||||||
|
msg.To = to
|
||||||
|
if err := c.Send(msg); err != nil {
|
||||||
|
log.WithFields(map[string]interface{}{
|
||||||
|
"muc": isMUCRecipient,
|
||||||
|
"to": to,
|
||||||
|
"text": msgText,
|
||||||
|
}).Errorf("error on send message: %s", err)
|
||||||
|
} else {
|
||||||
|
log.WithFields(map[string]interface{}{
|
||||||
|
"muc": isMUCRecipient,
|
||||||
|
"to": to,
|
||||||
|
"text": msgText,
|
||||||
|
}).Info("send message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
cmd/go.mod
Normal file
13
cmd/go.mod
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module gosrc.io/xmpp/cmd
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bdlm/log v0.1.19
|
||||||
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7
|
||||||
|
github.com/spf13/cobra v0.0.5
|
||||||
|
github.com/spf13/viper v1.4.0
|
||||||
|
gosrc.io/xmpp v0.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
replace gosrc.io/xmpp => ./../
|
||||||
232
cmd/go.sum
Normal file
232
cmd/go.sum
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/bdlm/log v0.1.19 h1:GqVFZC+khJCEbtTmkaDL/araNDwxTeLBmdMK8pbRoBE=
|
||||||
|
github.com/bdlm/log v0.1.19/go.mod h1:30V5Zwc5Vt5ePq5rd9KJ6JQ/A5aFUcKzq5fYtO7c9qc=
|
||||||
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 h1:ggZyn+N8eoBh/qLla2kUtqm/ysjnkbzUxTQY+6LMshY=
|
||||||
|
github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7/go.mod h1:E4vIYZDcEPVbE/Dbxc7GpA3YJpXnsF5csRt8LptMGWI=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||||
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# XMPP Check
|
|
||||||
|
|
||||||
XMPP check is a tool to check TLS certificate on a remote server.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To install `xmpp-check` in your Go path:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go get -u gosrc.io/xmpp/cmd/xmpp-check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
If you server is on standard port and XMPP domains matches the hostname you can simply use:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ xmpp-check myhost.net
|
|
||||||
2019/05/16 16:04:36 All checks passed
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also pass the port and the XMPP domain if different from the server hostname:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ xmpp-check myhost.net:5222 xmppdomain.net
|
|
||||||
2019/05/16 16:05:21 All checks passed
|
|
||||||
```
|
|
||||||
|
|
||||||
Error code will be non-zero in case of error. You can thus use it directly with your usual
|
|
||||||
monitoring scripts.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
- Use a config file to define the checks to perform as client on an XMPP server.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
xmpp-check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires.
|
|
||||||
|
|
||||||
*/
|
|
||||||
package main
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
args := os.Args[1:]
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
log.Fatal("usage: xmpp-check host[:port] [domain]")
|
|
||||||
}
|
|
||||||
|
|
||||||
var address string
|
|
||||||
var domain string
|
|
||||||
if len(args) >= 1 {
|
|
||||||
address = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) >= 2 {
|
|
||||||
domain = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
runCheck(address, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCheck(address, domain string) {
|
|
||||||
client, err := xmpp.NewChecker(address, domain)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Check(); err != nil {
|
|
||||||
log.Fatal("Failed connection check: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("All checks passed")
|
|
||||||
}
|
|
||||||
128
component.go
128
component.go
@@ -1,21 +1,19 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
|
|
||||||
|
|
||||||
type ComponentOptions struct {
|
type ComponentOptions struct {
|
||||||
|
TransportConfiguration
|
||||||
|
|
||||||
// =================================
|
// =================================
|
||||||
// Component Connection Info
|
// Component Connection Info
|
||||||
|
|
||||||
@@ -23,9 +21,6 @@ type ComponentOptions struct {
|
|||||||
Domain string
|
Domain string
|
||||||
// Secret is the "password" used by the XMPP server to secure component access
|
// Secret is the "password" used by the XMPP server to secure component access
|
||||||
Secret string
|
Secret string
|
||||||
// Address is the XMPP Host and port to connect to. Host is of
|
|
||||||
// the form 'serverhost:port' i.e "localhost:8888"
|
|
||||||
Address string
|
|
||||||
|
|
||||||
// =================================
|
// =================================
|
||||||
// Component discovery
|
// Component discovery
|
||||||
@@ -50,12 +45,10 @@ type Component struct {
|
|||||||
ComponentOptions
|
ComponentOptions
|
||||||
router *Router
|
router *Router
|
||||||
|
|
||||||
// TCP level connection
|
transport Transport
|
||||||
conn net.Conn
|
|
||||||
|
|
||||||
// read / write
|
// read / write
|
||||||
socketProxy io.ReadWriter // TODO
|
socketProxy io.ReadWriter // TODO
|
||||||
decoder *xml.Decoder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
|
func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
|
||||||
@@ -66,52 +59,68 @@ func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
|
|||||||
// Connect triggers component connection to XMPP server component port.
|
// Connect triggers component connection to XMPP server component port.
|
||||||
// TODO: Failed handshake should be a permanent error
|
// TODO: Failed handshake should be a permanent error
|
||||||
func (c *Component) Connect() error {
|
func (c *Component) Connect() error {
|
||||||
var conn net.Conn
|
var state SMState
|
||||||
|
return c.Resume(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) Resume(sm SMState) error {
|
||||||
var err error
|
var err error
|
||||||
if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
|
var streamId string
|
||||||
return err
|
if c.ComponentOptions.TransportConfiguration.Domain == "" {
|
||||||
|
c.ComponentOptions.TransportConfiguration.Domain = c.ComponentOptions.Domain
|
||||||
}
|
}
|
||||||
c.conn = conn
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
|
||||||
// 1. Send stream open tag
|
|
||||||
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
|
|
||||||
return errors.New("cannot send stream open " + err.Error())
|
|
||||||
}
|
|
||||||
c.decoder = xml.NewDecoder(conn)
|
|
||||||
|
|
||||||
// 2. Initialize xml decoder and extract streamID from reply
|
|
||||||
streamId, err := stanza.InitStream(c.decoder)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("cannot init decoder " + err.Error())
|
c.updateState(StatePermanentError)
|
||||||
|
|
||||||
|
return NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Authentication
|
if streamId, err = c.transport.Connect(); err != nil {
|
||||||
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
|
c.updateState(StatePermanentError)
|
||||||
return errors.New("cannot send handshake " + err.Error())
|
|
||||||
|
return NewConnError(err, true)
|
||||||
|
}
|
||||||
|
c.updateState(StateConnected)
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
if _, err := fmt.Fprintf(c.transport, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
|
||||||
|
c.updateState(StateStreamError)
|
||||||
|
|
||||||
|
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check server response for authentication
|
// Check server response for authentication
|
||||||
val, err := stanza.NextPacket(c.decoder)
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.updateState(StatePermanentError)
|
||||||
|
return NewConnError(err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := val.(type) {
|
switch v := val.(type) {
|
||||||
case stanza.StreamError:
|
case stanza.StreamError:
|
||||||
return errors.New("handshake failed " + v.Error.Local)
|
c.streamError("conflict", "no auth loop")
|
||||||
|
return NewConnError(errors.New("handshake failed "+v.Error.Local), true)
|
||||||
case stanza.Handshake:
|
case stanza.Handshake:
|
||||||
// Start the receiver go routine
|
// Start the receiver go routine
|
||||||
go c.recv()
|
c.updateState(StateSessionEstablished)
|
||||||
return nil
|
// Leaving this channel here for later. Not used atm. We should return this instead of an error because right
|
||||||
|
// now the returned error is lost in limbo.
|
||||||
|
errChan := make(chan error)
|
||||||
|
go c.recv(errChan) // Sends to errChan
|
||||||
|
return err // Should be empty at this point
|
||||||
default:
|
default:
|
||||||
return errors.New("expecting handshake result, got " + v.Name())
|
c.updateState(StatePermanentError)
|
||||||
|
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Component) Disconnect() {
|
func (c *Component) Disconnect() {
|
||||||
_ = c.SendRaw("</stream:stream>")
|
|
||||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
||||||
_ = c.conn.Close()
|
if c.transport != nil {
|
||||||
|
_ = c.transport.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Component) SetHandler(handler EventHandler) {
|
func (c *Component) SetHandler(handler EventHandler) {
|
||||||
@@ -119,20 +128,22 @@ func (c *Component) SetHandler(handler EventHandler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Receiver Go routine receiver
|
// Receiver Go routine receiver
|
||||||
func (c *Component) recv() (err error) {
|
func (c *Component) recv(errChan chan<- error) {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
val, err := stanza.NextPacket(c.decoder)
|
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.updateState(StateDisconnected)
|
c.updateState(StateDisconnected)
|
||||||
return err
|
errChan <- err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle stream errors
|
// Handle stream errors
|
||||||
switch p := val.(type) {
|
switch p := val.(type) {
|
||||||
case stanza.StreamError:
|
case stanza.StreamError:
|
||||||
c.router.route(c, val)
|
c.router.route(c, val)
|
||||||
c.streamError(p.Error.Local, p.Text)
|
c.streamError(p.Error.Local, p.Text)
|
||||||
return errors.New("stream error: " + p.Error.Local)
|
errChan <- errors.New("stream error: " + p.Error.Local)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.router.route(c, val)
|
c.router.route(c, val)
|
||||||
}
|
}
|
||||||
@@ -140,8 +151,8 @@ func (c *Component) recv() (err error) {
|
|||||||
|
|
||||||
// Send marshalls XMPP stanza and sends it to the server.
|
// Send marshalls XMPP stanza and sends it to the server.
|
||||||
func (c *Component) Send(packet stanza.Packet) error {
|
func (c *Component) Send(packet stanza.Packet) error {
|
||||||
conn := c.conn
|
transport := c.transport
|
||||||
if conn == nil {
|
if transport == nil {
|
||||||
return errors.New("component is not connected")
|
return errors.New("component is not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,24 +161,43 @@ func (c *Component) Send(packet stanza.Packet) error {
|
|||||||
return errors.New("cannot marshal packet " + err.Error())
|
return errors.New("cannot marshal packet " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(conn, string(data)); err != nil {
|
if _, err := fmt.Fprintf(transport, string(data)); err != nil {
|
||||||
return errors.New("cannot send packet " + err.Error())
|
return errors.New("cannot send packet " + err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendIQ sends an IQ set or get stanza to the server. If a result is received
|
||||||
|
// the provided handler function will automatically be called.
|
||||||
|
//
|
||||||
|
// The provided context should have a timeout to prevent the client from waiting
|
||||||
|
// forever for an IQ result. For example:
|
||||||
|
//
|
||||||
|
// ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
|
||||||
|
// result := <- client.SendIQ(ctx, iq)
|
||||||
|
//
|
||||||
|
func (c *Component) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
|
||||||
|
if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
|
||||||
|
return nil, ErrCanOnlySendGetOrSetIq
|
||||||
|
}
|
||||||
|
if err := c.Send(iq); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
// SendRaw sends an XMPP stanza as a string to the server.
|
// SendRaw sends an XMPP stanza as a string to the server.
|
||||||
// It can be invalid XML or XMPP content. In that case, the server will
|
// It can be invalid XML or XMPP content. In that case, the server will
|
||||||
// disconnect the component. It is up to the user of this method to
|
// disconnect the component. It is up to the user of this method to
|
||||||
// carefully craft the XML content to produce valid XMPP.
|
// carefully craft the XML content to produce valid XMPP.
|
||||||
func (c *Component) SendRaw(packet string) error {
|
func (c *Component) SendRaw(packet string) error {
|
||||||
conn := c.conn
|
transport := c.transport
|
||||||
if conn == nil {
|
if transport == nil {
|
||||||
return errors.New("component is not connected")
|
return errors.New("component is not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
_, err = fmt.Fprintf(c.conn, packet)
|
_, err = fmt.Fprintf(transport, packet)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,33 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tests are ran in parallel, so each test creating a server must use a different port so we do not get any
|
||||||
|
// conflict. Using iota for this should do the trick.
|
||||||
|
const (
|
||||||
|
testComponentDomain = "localhost"
|
||||||
|
defaultServerName = "testServer"
|
||||||
|
defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545"
|
||||||
|
defaultComponentName = "Test Component"
|
||||||
|
|
||||||
|
// Default port is not standard XMPP port to avoid interfering
|
||||||
|
// with local running XMPP server
|
||||||
|
testHandshakePort = iota + 15222
|
||||||
|
testDecoderPort
|
||||||
|
testSendIqPort
|
||||||
|
testSendRawPort
|
||||||
|
testDisconnectPort
|
||||||
|
testSManDisconnectPort
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHandshake(t *testing.T) {
|
func TestHandshake(t *testing.T) {
|
||||||
@@ -20,6 +46,409 @@ func TestHandshake(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests connection process with a handshake exchange
|
||||||
|
// Tests multiple session IDs. All connections should generate a unique stream ID
|
||||||
func TestGenerateHandshake(t *testing.T) {
|
func TestGenerateHandshake(t *testing.T) {
|
||||||
// TODO
|
// Using this array with a channel to make a queue of values to test
|
||||||
|
// These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate
|
||||||
|
// some handshake value
|
||||||
|
var uuidsArray = [5]string{
|
||||||
|
"cc9b3249-9582-4780-825f-4311b42f9b0e",
|
||||||
|
"bba8be3c-d98e-4e26-b9bb-9ed34578a503",
|
||||||
|
"dae72822-80e8-496b-b763-ab685f53a188",
|
||||||
|
"a45d6c06-de49-4bb0-935b-1a2201b71028",
|
||||||
|
"7dc6924f-0eca-4237-9898-18654b8d891e",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel to pass stream IDs as a queue
|
||||||
|
var uchan = make(chan string, len(uuidsArray))
|
||||||
|
// Populate test channel
|
||||||
|
for _, elt := range uuidsArray {
|
||||||
|
uchan <- elt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It expects to have an ID sent its way through the "uchan"
|
||||||
|
// channel of this file. Otherwise it will hang for ever.
|
||||||
|
h := func(t *testing.T, c net.Conn) {
|
||||||
|
decoder := xml.NewDecoder(c)
|
||||||
|
checkOpenStreamHandshakeID(t, c, decoder, <-uchan)
|
||||||
|
readHandshakeComponent(t, decoder)
|
||||||
|
fmt.Fprintln(c, "<handshake/>") // That's all the server needs to return (see xep-0114)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddess := fmt.Sprintf("%s:%d", testComponentDomain, testHandshakePort)
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testComponentAddess, h)
|
||||||
|
|
||||||
|
// Init component
|
||||||
|
opts := ComponentOptions{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: testComponentAddess,
|
||||||
|
Domain: "localhost",
|
||||||
|
},
|
||||||
|
Domain: testComponentDomain,
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: "Test Component",
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
router := NewRouter()
|
||||||
|
c, err := NewComponent(opts, router)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try connecting, and storing the resulting streamID in a map.
|
||||||
|
m := make(map[string]bool)
|
||||||
|
for _, _ = range uuidsArray {
|
||||||
|
streamId, _ := c.transport.Connect()
|
||||||
|
m[c.handshake(streamId)] = true
|
||||||
|
}
|
||||||
|
if len(uuidsArray) != len(m) {
|
||||||
|
t.Errorf("Handshake does not produce a unique id. Expected: %d unique ids, got: %d", len(uuidsArray), len(m))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that NewStreamManager can accept a Component.
|
||||||
|
//
|
||||||
|
// This validates that Component conforms to StreamClient interface.
|
||||||
|
func TestStreamManager(t *testing.T) {
|
||||||
|
NewStreamManager(&Component{}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the decoder is properly initialized when connecting a component to a server.
|
||||||
|
// The decoder is expected to be built after a valid connection
|
||||||
|
// Based on the xmpp_component example.
|
||||||
|
func TestDecoder(t *testing.T) {
|
||||||
|
c, _ := mockConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID)
|
||||||
|
if c.transport.GetDecoder() == nil {
|
||||||
|
t.Errorf("Failed to initialize decoder. Decoder is nil.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests sending an IQ to the server, and getting the response
|
||||||
|
func TestSendIq(t *testing.T) {
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockConnection(t, testSendIqPort, handlerForComponentIQSend)
|
||||||
|
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"})
|
||||||
|
disco := iqReq.DiscoInfo()
|
||||||
|
iqReq.Payload = disco
|
||||||
|
|
||||||
|
var res chan stanza.IQ
|
||||||
|
res, _ = c.SendIQ(ctx, iqReq)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-res:
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("Failed to receive response, to sent IQ, from mock server")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests sending raw xml to the mock server.
|
||||||
|
// TODO : check the server response client side ?
|
||||||
|
// Right now, the server response is not checked and an err is passed in a channel if the test is supposed to err.
|
||||||
|
// In this test, we use IQs
|
||||||
|
func TestSendRaw(t *testing.T) {
|
||||||
|
// Error channel for the handler
|
||||||
|
errChan := make(chan error)
|
||||||
|
// Handler for the mock server
|
||||||
|
h := func(t *testing.T, c net.Conn) {
|
||||||
|
// Completes the connection by exchanging handshakes
|
||||||
|
handlerForComponentHandshakeDefaultID(t, c)
|
||||||
|
receiveRawIq(t, c, errChan)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
req string
|
||||||
|
shouldErr bool
|
||||||
|
}
|
||||||
|
testRequests := make(map[string]testCase)
|
||||||
|
// Sending a correct IQ of type get. Not supposed to err
|
||||||
|
testRequests["Correct IQ"] = testCase{
|
||||||
|
req: `<iq type="get" id="91bd0bba-012f-4d92-bb17-5fc41e6fe545" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: false,
|
||||||
|
}
|
||||||
|
// Sending an IQ with a missing ID. Should err
|
||||||
|
testRequests["IQ with missing ID"] = testCase{
|
||||||
|
req: `<iq type="get" from="test1@localhost/mremond-mbp" to="testServer" lang="en"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>`,
|
||||||
|
shouldErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for all the IQs
|
||||||
|
for name, tcase := range testRequests {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
//Connecting to a mock server, initialized with given port and handler function
|
||||||
|
c, m := mockConnection(t, testSendRawPort, h)
|
||||||
|
|
||||||
|
// Sending raw xml from test case
|
||||||
|
err := c.SendRaw(tcase.req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error sending Raw string")
|
||||||
|
}
|
||||||
|
// Just wait a little so the message has time to arrive
|
||||||
|
select {
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
case err = <-errChan:
|
||||||
|
if err == nil && tcase.shouldErr {
|
||||||
|
t.Errorf("Failed to get closing stream err")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.transport.Close()
|
||||||
|
m.Stop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests the Disconnect method for Components
|
||||||
|
func TestDisconnect(t *testing.T) {
|
||||||
|
c, m := mockConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID)
|
||||||
|
err := c.transport.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not ping but not disconnected yet")
|
||||||
|
}
|
||||||
|
c.Disconnect()
|
||||||
|
err = c.transport.Ping()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Did not disconnect properly")
|
||||||
|
}
|
||||||
|
m.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that a streamManager successfully disconnects when a handshake fails between the component and the server.
|
||||||
|
func TestStreamManagerDisconnect(t *testing.T) {
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, testSManDisconnectPort)
|
||||||
|
mock := ServerMock{}
|
||||||
|
// Handler fails the handshake, which is currently the only option to disconnect completely when using a streamManager
|
||||||
|
// a failed handshake being a permanent error, except for a "conflict"
|
||||||
|
mock.Start(t, testComponentAddress, handlerComponentFailedHandshakeDefaultID)
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// Create Component to connect to it
|
||||||
|
c := makeBasicComponent(defaultComponentName, testComponentAddress, t)
|
||||||
|
|
||||||
|
//========================================
|
||||||
|
// Connect the new Component to the server
|
||||||
|
cm := NewStreamManager(c, nil)
|
||||||
|
errChan := make(chan error)
|
||||||
|
runSMan := func(errChan chan error) {
|
||||||
|
errChan <- cm.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
go runSMan(errChan)
|
||||||
|
select {
|
||||||
|
case <-errChan:
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("The component and server seem to still be connected while they should not.")
|
||||||
|
}
|
||||||
|
mock.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Basic XMPP Server Mock Handlers.
|
||||||
|
// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant.
|
||||||
|
// Used in the mock server as a Handler
|
||||||
|
func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) {
|
||||||
|
decoder := xml.NewDecoder(c)
|
||||||
|
checkOpenStreamHandshakeDefaultID(t, c, decoder)
|
||||||
|
readHandshakeComponent(t, decoder)
|
||||||
|
fmt.Fprintln(c, "<handshake/>") // That's all the server needs to return (see xep-0114)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant.
|
||||||
|
// This handler is supposed to fail by sending a "message" stanza instead of a <handshake/> stanza to finalize the handshake.
|
||||||
|
func handlerComponentFailedHandshakeDefaultID(t *testing.T, c net.Conn) {
|
||||||
|
decoder := xml.NewDecoder(c)
|
||||||
|
checkOpenStreamHandshakeDefaultID(t, c, decoder)
|
||||||
|
readHandshakeComponent(t, decoder)
|
||||||
|
|
||||||
|
// Send a message, instead of a "<handshake/>" tag, to fail the handshake process dans disconnect the client.
|
||||||
|
me := stanza.Message{
|
||||||
|
Attrs: stanza.Attrs{Type: stanza.MessageTypeChat, From: defaultServerName, To: defaultComponentName, Lang: "en"},
|
||||||
|
Body: "Fail my handshake.",
|
||||||
|
}
|
||||||
|
s, _ := xml.Marshal(me)
|
||||||
|
fmt.Fprintln(c, string(s))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads from the connection with the Component. Expects a handshake request, and returns the <handshake/> tag.
|
||||||
|
func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) {
|
||||||
|
se, err := stanza.NextStart(decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read auth: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nv := &stanza.Handshake{}
|
||||||
|
// Decode element into pointer storage
|
||||||
|
if err = decoder.DecodeElement(nv, &se); err != nil {
|
||||||
|
t.Errorf("cannot decode handshake: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(strings.TrimSpace(nv.Value)) == 0 {
|
||||||
|
t.Errorf("did not receive handshake ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||||
|
checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for ID and handshake related tests
|
||||||
|
func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) {
|
||||||
|
c.SetDeadline(time.Now().Add(defaultTimeout))
|
||||||
|
defer c.SetDeadline(time.Time{})
|
||||||
|
|
||||||
|
for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion.
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read next token: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch elem := token.(type) {
|
||||||
|
// Wait for first startElement
|
||||||
|
case xml.StartElement:
|
||||||
|
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
|
||||||
|
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", streamID, stanza.NSComponent, stanza.NSStream); err != nil {
|
||||||
|
t.Errorf("cannot write server stream open: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Sends IQ response to Component request.
|
||||||
|
// No parsing of the request here. We just check that it's valid, and send the default response.
|
||||||
|
func handlerForComponentIQSend(t *testing.T, c net.Conn) {
|
||||||
|
// Completes the connection by exchanging handshakes
|
||||||
|
handlerForComponentHandshakeDefaultID(t, c)
|
||||||
|
|
||||||
|
// Decoder to parse the request
|
||||||
|
decoder := xml.NewDecoder(c)
|
||||||
|
|
||||||
|
iqReq, err := receiveIq(t, c, decoder)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error receiving the IQ stanza : %v", err)
|
||||||
|
} else if !iqReq.IsValid() {
|
||||||
|
t.Errorf("server received an IQ stanza : %v", iqReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crafting response
|
||||||
|
iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"})
|
||||||
|
disco := iqResp.DiscoInfo()
|
||||||
|
disco.AddFeatures("vcard-temp",
|
||||||
|
`http://jabber.org/protocol/address`)
|
||||||
|
|
||||||
|
disco.AddIdentity("Multicast", "service", "multicast")
|
||||||
|
iqResp.Payload = disco
|
||||||
|
|
||||||
|
// Sending response to the Component
|
||||||
|
mResp, err := xml.Marshal(iqResp)
|
||||||
|
_, err = fmt.Fprintln(c, string(mResp))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not send response stanza : %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads next request coming from the Component. Expecting it to be an IQ request
|
||||||
|
func receiveIq(t *testing.T, c net.Conn, decoder *xml.Decoder) (stanza.IQ, error) {
|
||||||
|
c.SetDeadline(time.Now().Add(defaultTimeout))
|
||||||
|
defer c.SetDeadline(time.Time{})
|
||||||
|
var iqStz stanza.IQ
|
||||||
|
err := decoder.Decode(&iqStz)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot read the received IQ stanza: %s", err)
|
||||||
|
}
|
||||||
|
if !iqStz.IsValid() {
|
||||||
|
t.Errorf("received IQ stanza is invalid : %s", err)
|
||||||
|
}
|
||||||
|
return iqStz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveRawIq(t *testing.T, c net.Conn, errChan chan error) {
|
||||||
|
c.SetDeadline(time.Now().Add(defaultTimeout))
|
||||||
|
defer c.SetDeadline(time.Time{})
|
||||||
|
decoder := xml.NewDecoder(c)
|
||||||
|
var iq stanza.IQ
|
||||||
|
err := decoder.Decode(&iq)
|
||||||
|
if err != nil || !iq.IsValid() {
|
||||||
|
s := stanza.StreamError{
|
||||||
|
XMLName: xml.Name{Local: "stream:error"},
|
||||||
|
Error: xml.Name{Local: "xml-not-well-formed"},
|
||||||
|
Text: `XML was not well-formed`,
|
||||||
|
}
|
||||||
|
raw, _ := xml.Marshal(s)
|
||||||
|
fmt.Fprintln(c, string(raw))
|
||||||
|
fmt.Fprintln(c, `</stream:stream>`) // TODO : check this client side
|
||||||
|
errChan <- fmt.Errorf("invalid xml")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errChan <- nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================
|
||||||
|
// Init mock server and connection
|
||||||
|
// Creating a mock server and connecting a Component to it. Initialized with given port and handler function
|
||||||
|
// The Component and mock are both returned
|
||||||
|
func mockConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) {
|
||||||
|
// Init mock server
|
||||||
|
testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port)
|
||||||
|
mock := ServerMock{}
|
||||||
|
mock.Start(t, testComponentAddress, handler)
|
||||||
|
|
||||||
|
//==================================
|
||||||
|
// Create Component to connect to it
|
||||||
|
c := makeBasicComponent(defaultComponentName, testComponentAddress, t)
|
||||||
|
|
||||||
|
//========================================
|
||||||
|
// Connect the new Component to the server
|
||||||
|
err := c.Connect()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, &mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component {
|
||||||
|
opts := ComponentOptions{
|
||||||
|
TransportConfiguration: TransportConfiguration{
|
||||||
|
Address: mockServerAddr,
|
||||||
|
Domain: "localhost",
|
||||||
|
},
|
||||||
|
Domain: testComponentDomain,
|
||||||
|
Secret: "mypass",
|
||||||
|
Name: name,
|
||||||
|
Category: "gateway",
|
||||||
|
Type: "service",
|
||||||
|
}
|
||||||
|
router := NewRouter()
|
||||||
|
c, err := NewComponent(opts, router)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%+v", err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config & TransportConfiguration must not be modified after having been passed to NewClient. Any
|
||||||
|
// changes made after connecting are ignored.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Address string
|
TransportConfiguration
|
||||||
|
|
||||||
Jid string
|
Jid string
|
||||||
parsedJid *Jid // For easier manipulation
|
parsedJid *Jid // For easier manipulation
|
||||||
Password string
|
Credential Credential
|
||||||
StreamLogger *os.File // Used for debugging
|
StreamLogger *os.File // Used for debugging
|
||||||
Lang string // TODO: should default to 'en'
|
Lang string // TODO: should default to 'en'
|
||||||
ConnectTimeout int // Client timeout in seconds. Default to 15
|
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||||
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
||||||
// is supported on the server, we will still try to use it.
|
// is supported on the server, we will still try to use it.
|
||||||
Insecure bool
|
Insecure bool
|
||||||
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -1,8 +1,10 @@
|
|||||||
module gosrc.io/xmpp
|
module gosrc.io/xmpp
|
||||||
|
|
||||||
go 1.12
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/go-cmp v0.2.0
|
github.com/google/go-cmp v0.3.1
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
github.com/google/uuid v1.1.1
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||||
|
nhooyr.io/websocket v1.6.5
|
||||||
)
|
)
|
||||||
|
|||||||
104
go.sum
104
go.sum
@@ -1,4 +1,106 @@
|
|||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||||
|
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||||
|
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||||
|
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||||
|
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||||
|
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||||
|
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func ensurePort(addr string, port int) string {
|
|||||||
// This is IPV4 without port
|
// This is IPV4 without port
|
||||||
return addr + ":" + strconv.Itoa(port)
|
return addr + ":" + strconv.Itoa(port)
|
||||||
case 1:
|
case 1:
|
||||||
// This is IPV$ with port
|
// This is IPV6 with port
|
||||||
return addr
|
return addr
|
||||||
default:
|
default:
|
||||||
// This is IPV6 without port, as you need to use bracket with port in IPV6
|
// This is IPV6 without port, as you need to use bracket with port in IPV6
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type params struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseAddr(t *testing.T) {
|
func TestParseAddr(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -33,3 +31,36 @@ func TestParseAddr(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsurePort(t *testing.T) {
|
||||||
|
testAddresses := []string{
|
||||||
|
"1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad",
|
||||||
|
"1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad:5252",
|
||||||
|
"[::1]",
|
||||||
|
"127.0.0.1:5555",
|
||||||
|
"127.0.0.1",
|
||||||
|
"[::1]:5555",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, oldAddr := range testAddresses {
|
||||||
|
t.Run(oldAddr, func(st *testing.T) {
|
||||||
|
newAddr := ensurePort(oldAddr, 5222)
|
||||||
|
|
||||||
|
if len(newAddr) < len(oldAddr) {
|
||||||
|
st.Errorf("incorrect Result: transformed address is shorter than input : %v (old) > %v (new)", newAddr, oldAddr)
|
||||||
|
}
|
||||||
|
// If IPv6, the new address needs brackets to specify a port, like so : [2001:db8:85a3:0:0:8a2e:370:7334]:5222
|
||||||
|
if strings.Count(newAddr, "[") < strings.Count(oldAddr, "[") ||
|
||||||
|
strings.Count(newAddr, "]") < strings.Count(oldAddr, "]") {
|
||||||
|
|
||||||
|
st.Errorf("incorrect Result. Transformed address seems to not have correct brakets : %v => %v", oldAddr, newAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we messed up the colons, or didn't properly add a port
|
||||||
|
if strings.Count(newAddr, ":") < strings.Count(oldAddr, ":") {
|
||||||
|
st.Errorf("incorrect Result: transformed address doesn't seem to have a port %v (=> %v, no port ?)", oldAddr, newAddr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
102
router.go
102
router.go
@@ -1,8 +1,10 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
@@ -25,16 +27,35 @@ TODO: Automatically reply to IQ that do not match any route, to comply to XMPP s
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
// Routes to be matched, in order.
|
// Routes to be matched, in order.
|
||||||
routes []*Route
|
routes []*Route
|
||||||
|
|
||||||
|
IQResultRoutes map[string]*IQResultRoute
|
||||||
|
IQResultRouteLock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter returns a new router instance.
|
// NewRouter returns a new router instance.
|
||||||
func NewRouter() *Router {
|
func NewRouter() *Router {
|
||||||
return &Router{}
|
return &Router{
|
||||||
|
IQResultRoutes: make(map[string]*IQResultRoute),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// route is called by the XMPP client to dispatch stanza received using the set up routes.
|
// route is called by the XMPP client to dispatch stanza received using the set up routes.
|
||||||
// It is also used by test, but is not supposed to be used directly by users of the library.
|
// It is also used by test, but is not supposed to be used directly by users of the library.
|
||||||
func (r *Router) route(s Sender, p stanza.Packet) {
|
func (r *Router) route(s Sender, p stanza.Packet) {
|
||||||
|
iq, isIq := p.(stanza.IQ)
|
||||||
|
if isIq {
|
||||||
|
r.IQResultRouteLock.RLock()
|
||||||
|
route, ok := r.IQResultRoutes[iq.Id]
|
||||||
|
r.IQResultRouteLock.RUnlock()
|
||||||
|
if ok {
|
||||||
|
r.IQResultRouteLock.Lock()
|
||||||
|
delete(r.IQResultRoutes, iq.Id)
|
||||||
|
r.IQResultRouteLock.Unlock()
|
||||||
|
route.result <- iq
|
||||||
|
close(route.result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var match RouteMatch
|
var match RouteMatch
|
||||||
if r.Match(p, &match) {
|
if r.Match(p, &match) {
|
||||||
@@ -42,13 +63,12 @@ func (r *Router) route(s Sender, p stanza.Packet) {
|
|||||||
match.Handler.HandlePacket(s, p)
|
match.Handler.HandlePacket(s, p)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no match and we receive an iq set or get, we need to send a reply
|
// If there is no match and we receive an iq set or get, we need to send a reply
|
||||||
if iq, ok := p.(stanza.IQ); ok {
|
if isIq && (iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet) {
|
||||||
if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet {
|
|
||||||
iqNotImplemented(s, iq)
|
iqNotImplemented(s, iq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func iqNotImplemented(s Sender, iq stanza.IQ) {
|
func iqNotImplemented(s Sender, iq stanza.IQ) {
|
||||||
err := stanza.Err{
|
err := stanza.Err{
|
||||||
@@ -68,6 +88,27 @@ func (r *Router) NewRoute() *Route {
|
|||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIQResultRoute register a route that will catch an IQ result stanza with
|
||||||
|
// the given Id. The route will only match ones, after which it will automatically
|
||||||
|
// be unregistered
|
||||||
|
func (r *Router) NewIQResultRoute(ctx context.Context, id string) chan stanza.IQ {
|
||||||
|
route := NewIQResultRoute(ctx)
|
||||||
|
r.IQResultRouteLock.Lock()
|
||||||
|
r.IQResultRoutes[id] = route
|
||||||
|
r.IQResultRouteLock.Unlock()
|
||||||
|
|
||||||
|
// Start a go function to make sure the route is unregistered when the context
|
||||||
|
// is done.
|
||||||
|
go func() {
|
||||||
|
<-route.context.Done()
|
||||||
|
r.IQResultRouteLock.Lock()
|
||||||
|
delete(r.IQResultRoutes, id)
|
||||||
|
r.IQResultRouteLock.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return route.result
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
|
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||||
for _, route := range r.routes {
|
for _, route := range r.routes {
|
||||||
if route.Match(p, match) {
|
if route.Match(p, match) {
|
||||||
@@ -89,8 +130,44 @@ func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Rou
|
|||||||
return r.NewRoute().Packet(name).HandlerFunc(f)
|
return r.NewRoute().Packet(name).HandlerFunc(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// TimeoutHandlerFunc is a function type for handling IQ result timeouts.
|
||||||
|
type TimeoutHandlerFunc func(err error)
|
||||||
|
|
||||||
|
// IQResultRoute is a temporary route to match IQ result stanzas
|
||||||
|
type IQResultRoute struct {
|
||||||
|
context context.Context
|
||||||
|
result chan stanza.IQ
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIQResultRoute creates a new IQResultRoute instance
|
||||||
|
func NewIQResultRoute(ctx context.Context) *IQResultRoute {
|
||||||
|
return &IQResultRoute{
|
||||||
|
context: ctx,
|
||||||
|
result: make(chan stanza.IQ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IQ result handler
|
||||||
|
|
||||||
|
// IQResultHandler is a utility interface for IQ result handlers
|
||||||
|
type IQResultHandler interface {
|
||||||
|
HandleIQ(ctx context.Context, s Sender, iq stanza.IQ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IQResultHandlerFunc is an adapter to allow using functions as IQ result handlers.
|
||||||
|
type IQResultHandlerFunc func(ctx context.Context, s Sender, iq stanza.IQ)
|
||||||
|
|
||||||
|
// HandleIQ is a proxy function to implement IQResultHandler using a function.
|
||||||
|
func (f IQResultHandlerFunc) HandleIQ(ctx context.Context, s Sender, iq stanza.IQ) {
|
||||||
|
f(ctx, s, iq)
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Route
|
// Route
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
HandlePacket(s Sender, p stanza.Packet)
|
HandlePacket(s Sender, p stanza.Packet)
|
||||||
}
|
}
|
||||||
@@ -98,7 +175,7 @@ type Handler interface {
|
|||||||
type Route struct {
|
type Route struct {
|
||||||
handler Handler
|
handler Handler
|
||||||
// Matchers are used to "specialize" routes and focus on specific packet features
|
// Matchers are used to "specialize" routes and focus on specific packet features
|
||||||
matchers []matcher
|
matchers []Matcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Route) Handler(handler Handler) *Route {
|
func (r *Route) Handler(handler Handler) *Route {
|
||||||
@@ -122,8 +199,8 @@ func (r *Route) HandlerFunc(f HandlerFunc) *Route {
|
|||||||
return r.Handler(f)
|
return r.Handler(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addMatcher adds a matcher to the route
|
// AddMatcher adds a matcher to the route
|
||||||
func (r *Route) addMatcher(m matcher) *Route {
|
func (r *Route) AddMatcher(m Matcher) *Route {
|
||||||
r.matchers = append(r.matchers, m)
|
r.matchers = append(r.matchers, m)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -170,7 +247,7 @@ func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
|||||||
// It matches on the Local part of the xml.Name
|
// It matches on the Local part of the xml.Name
|
||||||
func (r *Route) Packet(name string) *Route {
|
func (r *Route) Packet(name string) *Route {
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
return r.addMatcher(nameMatcher(name))
|
return r.AddMatcher(nameMatcher(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@@ -204,7 +281,7 @@ func (r *Route) StanzaType(types ...string) *Route {
|
|||||||
for k, v := range types {
|
for k, v := range types {
|
||||||
types[k] = strings.ToLower(v)
|
types[k] = strings.ToLower(v)
|
||||||
}
|
}
|
||||||
return r.addMatcher(nsTypeMatcher(types))
|
return r.AddMatcher(nsTypeMatcher(types))
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
@@ -229,14 +306,15 @@ func (r *Route) IQNamespaces(namespaces ...string) *Route {
|
|||||||
for k, v := range namespaces {
|
for k, v := range namespaces {
|
||||||
namespaces[k] = strings.ToLower(v)
|
namespaces[k] = strings.ToLower(v)
|
||||||
}
|
}
|
||||||
return r.addMatcher(nsIQMatcher(namespaces))
|
return r.AddMatcher(nsIQMatcher(namespaces))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Matchers
|
// Matchers
|
||||||
|
|
||||||
// Matchers are used to "specialize" routes and focus on specific packet features
|
// Matchers are used to "specialize" routes and focus on specific packet features.
|
||||||
type matcher interface {
|
// You can register attach them to a route via the AddMatcher method.
|
||||||
|
type Matcher interface {
|
||||||
Match(stanza.Packet, *RouteMatch) bool
|
Match(stanza.Packet, *RouteMatch) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package xmpp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
@@ -11,6 +13,47 @@ import (
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Test route & matchers
|
// Test route & matchers
|
||||||
|
|
||||||
|
func TestIQResultRoutes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
router := NewRouter()
|
||||||
|
conn := NewSenderMock()
|
||||||
|
|
||||||
|
if router.IQResultRoutes == nil {
|
||||||
|
t.Fatal("NewRouter does not initialize isResultRoutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the IQ handler was called
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||||
|
defer cancel()
|
||||||
|
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"})
|
||||||
|
res := router.NewIQResultRoute(ctx, "1234")
|
||||||
|
go router.route(conn, iq)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("IQ result was not matched")
|
||||||
|
case <-res:
|
||||||
|
// Success
|
||||||
|
}
|
||||||
|
|
||||||
|
// The match must only happen once, so the id should no longer be in IQResultRoutes
|
||||||
|
if _, ok := router.IQResultRoutes[iq.Attrs.Id]; ok {
|
||||||
|
t.Fatal("IQ ID was not removed from the route map")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check other IQ does not matcah
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), time.Millisecond*100)
|
||||||
|
defer cancel()
|
||||||
|
iq.Attrs.Id = "4321"
|
||||||
|
res = router.NewIQResultRoute(ctx, "1234")
|
||||||
|
go router.route(conn, iq)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Success
|
||||||
|
case <-res:
|
||||||
|
t.Fatal("IQ result with wrong ID was matched")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNameMatcher(t *testing.T) {
|
func TestNameMatcher(t *testing.T) {
|
||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
router.HandleFunc("message", func(s Sender, p stanza.Packet) {
|
router.HandleFunc("message", func(s Sender, p stanza.Packet) {
|
||||||
@@ -103,7 +146,7 @@ func TestTypeMatcher(t *testing.T) {
|
|||||||
|
|
||||||
// We do not match on other types
|
// We do not match on other types
|
||||||
conn = NewSenderMock()
|
conn = NewSenderMock()
|
||||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
iqVersion.Payload = &stanza.DiscoInfo{
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
@@ -120,27 +163,27 @@ func TestCompositeMatcher(t *testing.T) {
|
|||||||
router := NewRouter()
|
router := NewRouter()
|
||||||
router.NewRoute().
|
router.NewRoute().
|
||||||
IQNamespaces("jabber:iq:version").
|
IQNamespaces("jabber:iq:version").
|
||||||
StanzaType("get").
|
StanzaType(string(stanza.IQTypeGet)).
|
||||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||||
_ = s.SendRaw(successFlag)
|
_ = s.SendRaw(successFlag)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Data set
|
// Data set
|
||||||
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
getVersionIq.Payload = &stanza.Version{
|
getVersionIq.Payload = &stanza.Version{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
Local: "query",
|
Local: "query",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
|
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
setVersionIq.Payload = &stanza.Version{
|
setVersionIq.Payload = &stanza.Version{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
Local: "query",
|
Local: "query",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
GetDiscoIq.Payload = &stanza.DiscoInfo{
|
GetDiscoIq.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "http://jabber.org/protocol/disco#info",
|
Space: "http://jabber.org/protocol/disco#info",
|
||||||
@@ -195,7 +238,7 @@ func TestCatchallMatcher(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn = NewSenderMock()
|
conn = NewSenderMock()
|
||||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||||
iqVersion.Payload = &stanza.DiscoInfo{
|
iqVersion.Payload = &stanza.DiscoInfo{
|
||||||
XMLName: xml.Name{
|
XMLName: xml.Name{
|
||||||
Space: "jabber:iq:version",
|
Space: "jabber:iq:version",
|
||||||
@@ -211,7 +254,8 @@ func TestCatchallMatcher(t *testing.T) {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SenderMock
|
// SenderMock
|
||||||
|
|
||||||
var successFlag = "matched"
|
const successFlag = "matched"
|
||||||
|
const cancelledFlag = "cancelled"
|
||||||
|
|
||||||
type SenderMock struct {
|
type SenderMock struct {
|
||||||
buffer *bytes.Buffer
|
buffer *bytes.Buffer
|
||||||
@@ -230,6 +274,15 @@ func (s SenderMock) Send(packet stanza.Packet) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SenderMock) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
|
||||||
|
out, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.buffer.Write(out)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s SenderMock) SendRaw(str string) error {
|
func (s SenderMock) SendRaw(str string) error {
|
||||||
s.buffer.WriteString(str)
|
s.buffer.WriteString(str)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
189
session.go
189
session.go
@@ -1,58 +1,68 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
|
|
||||||
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
// Session info
|
// Session info
|
||||||
BindJid string // Jabber ID as provided by XMPP server
|
BindJid string // Jabber ID as provided by XMPP server
|
||||||
StreamId string
|
StreamId string
|
||||||
|
SMState SMState
|
||||||
Features stanza.StreamFeatures
|
Features stanza.StreamFeatures
|
||||||
TlsEnabled bool
|
TlsEnabled bool
|
||||||
lastPacketId int
|
lastPacketId int
|
||||||
|
|
||||||
// read / write
|
// read / write
|
||||||
streamLogger io.ReadWriter
|
transport Transport
|
||||||
decoder *xml.Decoder
|
|
||||||
|
|
||||||
// error management
|
// error management
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
|
func NewSession(transport Transport, o Config, state SMState) (*Session, error) {
|
||||||
s := new(Session)
|
s := new(Session)
|
||||||
s.init(conn, o)
|
s.transport = transport
|
||||||
|
s.SMState = state
|
||||||
|
s.init(o)
|
||||||
|
|
||||||
// starttls
|
if s.err != nil {
|
||||||
var tlsConn net.Conn
|
return nil, NewConnError(s.err, true)
|
||||||
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain)
|
|
||||||
if s.TlsEnabled {
|
|
||||||
s.reset(conn, tlsConn, o)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.TlsEnabled && !o.Insecure {
|
if !transport.IsSecure() {
|
||||||
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
|
s.startTlsIfSupported(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !transport.IsSecure() && !o.Insecure {
|
||||||
|
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||||
|
return nil, NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TlsEnabled {
|
||||||
|
s.reset(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
s.auth(o)
|
s.auth(o)
|
||||||
s.reset(tlsConn, tlsConn, o)
|
s.reset(o)
|
||||||
|
|
||||||
// bind resource and 'start' XMPP session
|
// attempt resumption
|
||||||
|
if s.resume(o) {
|
||||||
|
return s, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, bind resource and 'start' XMPP session
|
||||||
s.bind(o)
|
s.bind(o)
|
||||||
s.rfc3921Session(o)
|
s.rfc3921Session(o)
|
||||||
|
|
||||||
return tlsConn, s, s.err
|
// Enable stream management if supported
|
||||||
|
s.EnableStreamManagement(o)
|
||||||
|
|
||||||
|
return s, s.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) PacketId() string {
|
func (s *Session) PacketId() string {
|
||||||
@@ -60,77 +70,59 @@ func (s *Session) PacketId() string {
|
|||||||
return fmt.Sprintf("%x", s.lastPacketId)
|
return fmt.Sprintf("%x", s.lastPacketId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) init(conn net.Conn, o Config) {
|
func (s *Session) init(o Config) {
|
||||||
s.setStreamLogger(nil, conn, o)
|
|
||||||
s.Features = s.open(o.parsedJid.Domain)
|
s.Features = s.open(o.parsedJid.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) reset(conn net.Conn, newConn net.Conn, o Config) {
|
func (s *Session) reset(o Config) {
|
||||||
if s.err != nil {
|
if s.StreamId, s.err = s.transport.StartStream(); s.err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.setStreamLogger(conn, newConn, o)
|
|
||||||
s.Features = s.open(o.parsedJid.Domain)
|
s.Features = s.open(o.parsedJid.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) setStreamLogger(conn net.Conn, newConn net.Conn, o Config) {
|
|
||||||
if newConn != conn {
|
|
||||||
s.streamLogger = newStreamLogger(newConn, o.StreamLogger)
|
|
||||||
}
|
|
||||||
s.decoder = xml.NewDecoder(s.streamLogger)
|
|
||||||
s.decoder.CharsetReader = o.CharsetReader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||||
// Send stream open tag
|
|
||||||
if _, s.err = fmt.Fprintf(s.streamLogger, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set xml decoder and extract streamID from reply
|
|
||||||
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
|
|
||||||
if s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract stream features
|
// extract stream features
|
||||||
if s.err = s.decoder.Decode(&f); s.err != nil {
|
if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil {
|
||||||
s.err = errors.New("stream open decode features: " + s.err.Error())
|
s.err = errors.New("stream open decode features: " + s.err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
func (s *Session) startTlsIfSupported(o Config) {
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return conn
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.transport.DoesStartTLS() {
|
||||||
|
if !o.Insecure {
|
||||||
|
s.err = errors.New("Transport does not support starttls")
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := s.Features.DoesStartTLS(); ok {
|
if _, ok := s.Features.DoesStartTLS(); ok {
|
||||||
fmt.Fprintf(s.streamLogger, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
fmt.Fprintf(s.transport, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||||
|
|
||||||
var k stanza.TLSProceed
|
var k stanza.TLSProceed
|
||||||
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
if s.err = s.transport.GetDecoder().DecodeElement(&k, nil); s.err != nil {
|
||||||
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||||
return conn
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.err = s.transport.StartTLS()
|
||||||
|
|
||||||
|
if s.err == nil {
|
||||||
s.TlsEnabled = true
|
s.TlsEnabled = true
|
||||||
|
}
|
||||||
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
return
|
||||||
stanza.DefaultTlsConfig.ServerName = domain
|
|
||||||
tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig)
|
|
||||||
// We convert existing connection to TLS
|
|
||||||
if s.err = tlsConn.Handshake(); s.err != nil {
|
|
||||||
return tlsConn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We check that cert matches hostname
|
// If we do not allow cleartext connections, make it explicit that server do not support starttls
|
||||||
s.err = tlsConn.VerifyHostname(domain)
|
if !o.Insecure {
|
||||||
return tlsConn
|
s.err = errors.New("XMPP server does not advertise support for starttls")
|
||||||
}
|
}
|
||||||
|
|
||||||
// starttls is not supported => we do not upgrade the connection:
|
|
||||||
return conn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) auth(o Config) {
|
func (s *Session) auth(o Config) {
|
||||||
@@ -138,7 +130,39 @@ func (s *Session) auth(o Config) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
|
s.err = authSASL(s.transport, s.transport.GetDecoder(), s.Features, o.parsedJid.Node, o.Credential)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to resume session using stream management
|
||||||
|
func (s *Session) resume(o Config) bool {
|
||||||
|
if !s.Features.DoesStreamManagement() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.SMState.Id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(s.transport, "<resume xmlns='%s' h='%d' previd='%s'/>",
|
||||||
|
stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id)
|
||||||
|
|
||||||
|
var packet stanza.Packet
|
||||||
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||||
|
if s.err == nil {
|
||||||
|
switch p := packet.(type) {
|
||||||
|
case stanza.SMResumed:
|
||||||
|
if p.PrevId != s.SMState.Id {
|
||||||
|
s.err = errors.New("session resumption: mismatched id")
|
||||||
|
s.SMState = SMState{}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case stanza.SMFailed:
|
||||||
|
default:
|
||||||
|
s.err = errors.New("unexpected reply to SM resume")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.SMState = SMState{}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) bind(o Config) {
|
func (s *Session) bind(o Config) {
|
||||||
@@ -149,14 +173,14 @@ func (s *Session) bind(o Config) {
|
|||||||
// Send IQ message asking to bind to the local user name.
|
// Send IQ message asking to bind to the local user name.
|
||||||
var resource = o.parsedJid.Resource
|
var resource = o.parsedJid.Resource
|
||||||
if resource != "" {
|
if resource != "" {
|
||||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
||||||
s.PacketId(), stanza.NSBind, resource)
|
s.PacketId(), stanza.NSBind, resource)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
var iq stanza.IQ
|
var iq stanza.IQ
|
||||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||||
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -181,10 +205,37 @@ func (s *Session) rfc3921Session(o Config) {
|
|||||||
var iq stanza.IQ
|
var iq stanza.IQ
|
||||||
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
// We only negotiate session binding if it is mandatory, we skip it when optional.
|
||||||
if !s.Features.Session.IsOptional() {
|
if !s.Features.Session.IsOptional() {
|
||||||
fmt.Fprintf(s.streamLogger, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
fmt.Fprintf(s.transport, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
||||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil {
|
||||||
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable stream management, with session resumption, if supported.
|
||||||
|
func (s *Session) EnableStreamManagement(o Config) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.Features.DoesStreamManagement() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(s.transport, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement)
|
||||||
|
|
||||||
|
var packet stanza.Packet
|
||||||
|
packet, s.err = stanza.NextPacket(s.transport.GetDecoder())
|
||||||
|
if s.err == nil {
|
||||||
|
switch p := packet.(type) {
|
||||||
|
case stanza.SMEnabled:
|
||||||
|
s.SMState = SMState{Id: p.Id}
|
||||||
|
case stanza.SMFailed:
|
||||||
|
// TODO: Store error in SMState, for later inspection
|
||||||
|
default:
|
||||||
|
s.err = errors.New("unexpected reply to SM enable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
type Handshake struct {
|
type Handshake struct {
|
||||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||||
// TODO Add handshake value with test for proper serialization
|
// TODO Add handshake value with test for proper serialization
|
||||||
// Value string `xml:",innerxml"`
|
Value string `xml:",innerxml"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Handshake) Name() string {
|
func (Handshake) Name() string {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
|
|
||||||
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||||
if elt.XMLName == textName {
|
if elt.XMLName == textName {
|
||||||
x.Text = string(elt.Content)
|
x.Text = elt.Content
|
||||||
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
|
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
|
||||||
x.Reason = elt.XMLName.Local
|
x.Reason = elt.XMLName.Local
|
||||||
}
|
}
|
||||||
@@ -94,16 +94,32 @@ func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
|||||||
// Reason
|
// Reason
|
||||||
if x.Reason != "" {
|
if x.Reason != "" {
|
||||||
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
|
||||||
e.EncodeToken(xml.StartElement{Name: reason})
|
err = e.EncodeToken(xml.StartElement{Name: reason})
|
||||||
e.EncodeToken(xml.EndElement{Name: reason})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.EndElement{Name: reason})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
if x.Text != "" {
|
if x.Text != "" {
|
||||||
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
|
||||||
e.EncodeToken(xml.StartElement{Name: text})
|
err = e.EncodeToken(xml.StartElement{Name: text})
|
||||||
e.EncodeToken(xml.CharData(x.Text))
|
if err != nil {
|
||||||
e.EncodeToken(xml.EndElement{Name: text})
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.CharData(x.Text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeToken(xml.EndElement{Name: text})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||||
|
|||||||
31
stanza/error_test.go
Normal file
31
stanza/error_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErr_UnmarshalXML(t *testing.T) {
|
||||||
|
packet := `
|
||||||
|
<iq from='pubsub.example.com'
|
||||||
|
id='kj4vz31m'
|
||||||
|
to='romeo@example.net/foo'
|
||||||
|
type='error'>
|
||||||
|
<error type='wait'>
|
||||||
|
<resource-constraint
|
||||||
|
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>System overloaded, please retry</text>
|
||||||
|
</error>
|
||||||
|
</iq>`
|
||||||
|
|
||||||
|
parsedIQ := IQ{}
|
||||||
|
data := []byte(packet)
|
||||||
|
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||||
|
t.Errorf("Unmarshal(%s) returned error", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
xmppError := parsedIQ.Error
|
||||||
|
if xmppError.Text != "System overloaded, please retry" {
|
||||||
|
t.Errorf("Could not extract error text: '%s'", xmppError.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,6 @@ func TestControlSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
|
||||||
t.Errorf("Paylod is not an iot control set: %v", cs)
|
t.Errorf("Payload is not an iot control set: %v", cs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
stanza/iq.go
55
stanza/iq.go
@@ -2,7 +2,9 @@ package stanza
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -22,8 +24,8 @@ type IQ struct { // Info/Query
|
|||||||
// child element, which specifies the semantics of the particular
|
// child element, which specifies the semantics of the particular
|
||||||
// request."
|
// request."
|
||||||
Payload IQPayload `xml:",omitempty"`
|
Payload IQPayload `xml:",omitempty"`
|
||||||
Error Err `xml:"error,omitempty"`
|
Error *Err `xml:"error,omitempty"`
|
||||||
// Any is used to decode unknown payload as a generique structure
|
// Any is used to decode unknown payload as a generic structure
|
||||||
Any *Node `xml:",any"`
|
Any *Node `xml:",any"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +34,12 @@ type IQPayload interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewIQ(a Attrs) IQ {
|
func NewIQ(a Attrs) IQ {
|
||||||
// TODO generate IQ ID if not set
|
|
||||||
// TODO ensure that type is set, as it is required
|
// TODO ensure that type is set, as it is required
|
||||||
|
if a.Id == "" {
|
||||||
|
if id, err := uuid.NewRandom(); err == nil {
|
||||||
|
a.Id = id.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
return IQ{
|
return IQ{
|
||||||
XMLName: xml.Name{Local: "iq"},
|
XMLName: xml.Name{Local: "iq"},
|
||||||
Attrs: a,
|
Attrs: a,
|
||||||
@@ -47,7 +53,7 @@ func (iq IQ) MakeError(xerror Err) IQ {
|
|||||||
iq.Type = "error"
|
iq.Type = "error"
|
||||||
iq.From = to
|
iq.From = to
|
||||||
iq.To = from
|
iq.To = from
|
||||||
iq.Error = xerror
|
iq.Error = &xerror
|
||||||
|
|
||||||
return iq
|
return iq
|
||||||
}
|
}
|
||||||
@@ -99,10 +105,9 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
var xmppError Err
|
var xmppError Err
|
||||||
err = d.DecodeElement(&xmppError, &tt)
|
err = d.DecodeElement(&xmppError, &tt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
iq.Error = xmppError
|
iq.Error = &xmppError
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||||
@@ -128,3 +133,39 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Following RFC-3920 for IQs
|
||||||
|
func (iq *IQ) IsValid() bool {
|
||||||
|
// ID is required
|
||||||
|
if len(strings.TrimSpace(iq.Id)) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type is required
|
||||||
|
if iq.Type.IsEmpty() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type get and set must contain one and only one child element that specifies the semantics
|
||||||
|
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
|
||||||
|
if iq.Payload == nil && iq.Any == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A result must include zero or one child element
|
||||||
|
if iq.Type == IQTypeResult {
|
||||||
|
if iq.Payload != nil && iq.Any != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Error type must contain an "error" child element
|
||||||
|
if iq.Type == IQTypeError {
|
||||||
|
if iq.Error == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ func (d *DiscoInfo) AddFeatures(namespace ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscoInfo) SetNode(node string) {
|
func (d *DiscoInfo) SetNode(node string) *DiscoInfo {
|
||||||
d.Node = node
|
d.Node = node
|
||||||
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
||||||
@@ -66,6 +67,7 @@ func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
|
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
|
||||||
|
d.Features = []Feature{}
|
||||||
for _, ns := range namespace {
|
for _, ns := range namespace {
|
||||||
d.Features = append(d.Features, Feature{Var: ns})
|
d.Features = append(d.Features, Feature{Var: ns})
|
||||||
}
|
}
|
||||||
@@ -104,11 +106,38 @@ func (d *DiscoItems) Namespace() string {
|
|||||||
return d.XMLName.Space
|
return d.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// DiscoItems builds a default DiscoItems payload
|
||||||
|
func (iq *IQ) DiscoItems() *DiscoItems {
|
||||||
|
d := DiscoItems{
|
||||||
|
XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"},
|
||||||
|
}
|
||||||
|
iq.Payload = &d
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) SetNode(node string) *DiscoItems {
|
||||||
|
d.Node = node
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoItems) AddItem(jid, node, name string) *DiscoItems {
|
||||||
|
item := DiscoItem{
|
||||||
|
JID: jid,
|
||||||
|
Node: node,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
d.Items = append(d.Items, item)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
type DiscoItem struct {
|
type DiscoItem struct {
|
||||||
XMLName xml.Name `xml:"item"`
|
XMLName xml.Name `xml:"item"`
|
||||||
Name string `xml:"name,attr,omitempty"`
|
|
||||||
JID string `xml:"jid,attr,omitempty"`
|
JID string `xml:"jid,attr,omitempty"`
|
||||||
Node string `xml:"node,attr,omitempty"`
|
Node string `xml:"node,attr,omitempty"`
|
||||||
|
Name string `xml:"name,attr,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -7,29 +7,22 @@ import (
|
|||||||
"gosrc.io/xmpp/stanza"
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiscoInfoBuilder(t *testing.T) {
|
// Test DiscoInfo Builder with several features
|
||||||
|
func TestDiscoInfo_Builder(t *testing.T) {
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||||
disco := iq.DiscoInfo()
|
disco := iq.DiscoInfo()
|
||||||
disco.AddIdentity("Test Component", "gateway", "service")
|
disco.AddIdentity("Test Component", "gateway", "service")
|
||||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||||
|
|
||||||
// Marshall
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
data, err := xml.Marshal(iq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("cannot marshal xml structure: %s", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshall
|
|
||||||
var parsedIQ stanza.IQ
|
|
||||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
|
||||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check result
|
// Check result
|
||||||
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Parsed stanza does not contain an IQ payload")
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check features
|
// Check features
|
||||||
@@ -53,3 +46,45 @@ func TestDiscoInfoBuilder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements XEP-0030 example 17
|
||||||
|
// https://xmpp.org/extensions/xep-0030.html#example-17
|
||||||
|
func TestDiscoItems_Builder(t *testing.T) {
|
||||||
|
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit",
|
||||||
|
To: "romeo@montague.net/orchard", Id: "items-2"})
|
||||||
|
iq.DiscoItems().
|
||||||
|
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
|
||||||
|
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
|
||||||
|
AddItem("catalog.shakespeare.lit", "music", "Music from the time of Shakespeare")
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*stanza.DiscoItems)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check items
|
||||||
|
items := []stanza.DiscoItem{{xml.Name{}, "catalog.shakespeare.lit", "books", "Books by and about Shakespeare"},
|
||||||
|
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
|
||||||
|
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
|
||||||
|
if len(pp.Items) != len(items) {
|
||||||
|
t.Errorf("Items length mismatch: %#v", pp.Items)
|
||||||
|
} else {
|
||||||
|
for i, item := range pp.Items {
|
||||||
|
if item.JID != items[i].JID {
|
||||||
|
t.Errorf("JID Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
}
|
||||||
|
if item.Node != items[i].Node {
|
||||||
|
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
}
|
||||||
|
if item.Name != items[i].Name {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", items[i].JID, item.JID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ func TestUnmarshalIqs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateIqId(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
iq := stanza.NewIQ(stanza.Attrs{Id: "1"})
|
||||||
|
if iq.Id != "1" {
|
||||||
|
t.Errorf("NewIQ replaced id with %s", iq.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
iq = stanza.NewIQ(stanza.Attrs{})
|
||||||
|
if iq.Id == "" {
|
||||||
|
t.Error("NewIQ did not generate an Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
otherIq := stanza.NewIQ(stanza.Attrs{})
|
||||||
|
if iq.Id == otherIq.Id {
|
||||||
|
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateIq(t *testing.T) {
|
func TestGenerateIq(t *testing.T) {
|
||||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||||
payload := stanza.DiscoInfo{
|
payload := stanza.DiscoInfo{
|
||||||
@@ -169,3 +187,38 @@ func TestUnknownPayload(t *testing.T) {
|
|||||||
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
|
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsValid(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
iq string
|
||||||
|
shouldErr bool
|
||||||
|
}
|
||||||
|
testIQs := make(map[string]testCase)
|
||||||
|
testIQs["Valid IQ"] = testCase{
|
||||||
|
`<iq type="get" to="service.localhost" id="1" >
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
testIQs["Invalid IQ"] = testCase{
|
||||||
|
`<iq type="get" to="service.localhost">
|
||||||
|
<query xmlns="unknown:ns"/>
|
||||||
|
</iq>`,
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tcase := range testIQs {
|
||||||
|
t.Run(name, func(st *testing.T) {
|
||||||
|
parsedIQ := stanza.IQ{}
|
||||||
|
err := xml.Unmarshal([]byte(tcase.iq), &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !parsedIQ.IsValid() && !tcase.shouldErr {
|
||||||
|
t.Errorf("failed iq validation for : %s", tcase.iq)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,26 @@ func (v *Version) Namespace() string {
|
|||||||
return v.XMLName.Space
|
return v.XMLName.Space
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Builder helpers
|
||||||
|
|
||||||
|
// Version builds a default software version payload
|
||||||
|
func (iq *IQ) Version() *Version {
|
||||||
|
d := Version{
|
||||||
|
XMLName: xml.Name{Space: "jabber:iq:version", Local: "query"},
|
||||||
|
}
|
||||||
|
iq.Payload = &d
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set all software version info
|
||||||
|
func (v *Version) SetInfo(name, version, os string) *Version {
|
||||||
|
v.Name = name
|
||||||
|
v.Version = version
|
||||||
|
v.OS = os
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Registry init
|
// Registry init
|
||||||
|
|
||||||
|
|||||||
40
stanza/iq_version_test.go
Normal file
40
stanza/iq_version_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package stanza_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build a Software Version reply
|
||||||
|
// https://xmpp.org/extensions/xep-0092.html#example-2
|
||||||
|
func TestVersion_Builder(t *testing.T) {
|
||||||
|
name := "Exodus"
|
||||||
|
version := "0.7.0.4"
|
||||||
|
os := "Windows-XP 5.01.2600"
|
||||||
|
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
|
||||||
|
To: "juliet@capulet.com/balcony", Id: "version_1"})
|
||||||
|
iq.Version().SetInfo(name, version, os)
|
||||||
|
|
||||||
|
parsedIQ, err := checkMarshalling(t, iq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
pp, ok := parsedIQ.Payload.(*stanza.Version)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version info
|
||||||
|
if pp.Name != name {
|
||||||
|
t.Errorf("Name Mismatch (expected: %s): %s", name, pp.Name)
|
||||||
|
}
|
||||||
|
if pp.Version != version {
|
||||||
|
t.Errorf("Version Mismatch (expected: %s): %s", version, pp.Version)
|
||||||
|
}
|
||||||
|
if pp.OS != os {
|
||||||
|
t.Errorf("OS Mismatch (expected: %s): %s", os, pp.OS)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import "encoding/xml"
|
|||||||
type Node struct {
|
type Node struct {
|
||||||
XMLName xml.Name
|
XMLName xml.Name
|
||||||
Attrs []xml.Attr `xml:"-"`
|
Attrs []xml.Attr `xml:"-"`
|
||||||
Content string `xml:",innerxml"`
|
Content string `xml:",cdata"`
|
||||||
Nodes []Node `xml:",any"`
|
Nodes []Node `xml:",any"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +46,18 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
|||||||
start.Name = n.XMLName
|
start.Name = n.XMLName
|
||||||
|
|
||||||
err = e.EncodeToken(start)
|
err = e.EncodeToken(start)
|
||||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n.Content != "" {
|
||||||
|
err = e.EncodeToken(xml.CharData(n.Content))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||||
}
|
}
|
||||||
|
|||||||
30
stanza/node_test.go
Normal file
30
stanza/node_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNode_Marshal(t *testing.T) {
|
||||||
|
jsonData := []byte("{\"key\":\"value\"}")
|
||||||
|
|
||||||
|
iqResp := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||||
|
iqResp.Any = &Node{
|
||||||
|
XMLName: xml.Name{Space: "myNS", Local: "space"},
|
||||||
|
Content: string(jsonData),
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := xml.Marshal(iqResp)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not marshal XML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIQ := IQ{}
|
||||||
|
if err := xml.Unmarshal(bytes, &parsedIQ); err != nil {
|
||||||
|
t.Errorf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedIQ.Any.Content != string(jsonData) {
|
||||||
|
t.Errorf("Cannot find generic any payload in parsedIQ: '%s'", parsedIQ.Any.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ const (
|
|||||||
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||||
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||||
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
|
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||||
|
NSFraming = "urn:ietf:params:xml:ns:xmpp-framing"
|
||||||
NSClient = "jabber:client"
|
NSClient = "jabber:client"
|
||||||
NSComponent = "jabber:component:accept"
|
NSComponent = "jabber:component:accept"
|
||||||
)
|
)
|
||||||
|
|||||||
13
stanza/open.go
Normal file
13
stanza/open.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// Open Packet
|
||||||
|
// Reference: WebSocket connections must start with this element
|
||||||
|
// https://tools.ietf.org/html/rfc7395#section-3.4
|
||||||
|
type WebsocketOpen struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-framing open"`
|
||||||
|
From string `xml:"from,attr"`
|
||||||
|
Id string `xml:"id,attr"`
|
||||||
|
Version string `xml:"version,attr"`
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package stanza
|
package stanza
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
type StanzaType string
|
type StanzaType string
|
||||||
|
|
||||||
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
|
||||||
@@ -23,3 +25,7 @@ const (
|
|||||||
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
|
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
|
||||||
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
|
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s StanzaType) IsEmpty() bool {
|
||||||
|
return len(strings.TrimSpace(string(s))) == 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ func InitStream(p *xml.Decoder) (sessionID string, err error) {
|
|||||||
|
|
||||||
switch elem := t.(type) {
|
switch elem := t.(type) {
|
||||||
case xml.StartElement:
|
case xml.StartElement:
|
||||||
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
|
isStreamOpen := elem.Name.Space == NSStream && elem.Name.Local == "stream"
|
||||||
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
isFrameOpen := elem.Name.Space == NSFraming && elem.Name.Local == "open"
|
||||||
|
if !isStreamOpen && !isFrameOpen {
|
||||||
|
err = errors.New("xmpp: expected <stream> or <open> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||||
return sessionID, err
|
return sessionID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +65,8 @@ func NextPacket(p *xml.Decoder) (Packet, error) {
|
|||||||
return decodeClient(p, se)
|
return decodeClient(p, se)
|
||||||
case NSComponent:
|
case NSComponent:
|
||||||
return decodeComponent(p, se)
|
return decodeComponent(p, se)
|
||||||
|
case NSStreamManagement:
|
||||||
|
return sm.decode(p, se)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown namespace " +
|
return nil, errors.New("unknown namespace " +
|
||||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||||
@@ -133,7 +137,7 @@ func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeClient decodes all known packets in the component namespace.
|
// decodeComponent decodes all known packets in the component namespace.
|
||||||
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||||
switch se.Name.Local {
|
switch se.Name.Local {
|
||||||
case "handshake": // handshake is used to authenticate components
|
case "handshake": // handshake is used to authenticate components
|
||||||
|
|||||||
@@ -107,6 +107,6 @@ func (s *StreamSession) IsOptional() bool {
|
|||||||
// Registry init
|
// Registry init
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, Bind{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-bind", Local: "bind"}, Bind{})
|
||||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{})
|
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-session", Local: "session"}, StreamSession{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package stanza
|
package stanza
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultTlsConfig tls.Config
|
|
||||||
|
|
||||||
// Used during stream initiation / session establishment
|
// Used during stream initiation / session establishment
|
||||||
type TLSProceed struct {
|
type TLSProceed struct {
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||||
|
|||||||
173
stanza/stream.go
173
stanza/stream.go
@@ -1,167 +1,14 @@
|
|||||||
package stanza
|
package stanza
|
||||||
|
|
||||||
import (
|
import "encoding/xml"
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
// Start of stream
|
||||||
// StreamFeatures Packet
|
// Reference: XMPP Core stream open
|
||||||
// Reference: The active stream features are published on
|
// https://tools.ietf.org/html/rfc6120#section-4.2
|
||||||
// https://xmpp.org/registrar/stream-features.html
|
type Stream struct {
|
||||||
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
|
XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"`
|
||||||
|
From string `xml:"from,attr"`
|
||||||
type StreamFeatures struct {
|
To string `xml:"to,attr"`
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
Id string `xml:"id,attr"`
|
||||||
// Server capabilities hash
|
Version string `xml:"version,attr"`
|
||||||
Caps Caps
|
|
||||||
// Stream features
|
|
||||||
StartTLS tlsStartTLS
|
|
||||||
Mechanisms saslMechanisms
|
|
||||||
Bind Bind
|
|
||||||
StreamManagement streamManagement
|
|
||||||
// Obsolete
|
|
||||||
Session StreamSession
|
|
||||||
// ProcessOne Stream Features
|
|
||||||
P1Push p1Push
|
|
||||||
P1Rebind p1Rebind
|
|
||||||
p1Ack p1Ack
|
|
||||||
Any []xml.Name `xml:",any"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (StreamFeatures) Name() string {
|
|
||||||
return "stream:features"
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamFeatureDecoder struct{}
|
|
||||||
|
|
||||||
var streamFeatures streamFeatureDecoder
|
|
||||||
|
|
||||||
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
|
|
||||||
var packet StreamFeatures
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capabilities
|
|
||||||
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
|
|
||||||
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
|
|
||||||
// and peer servers do not need to send service discovery requests each time they connect."
|
|
||||||
// This is not a stream feature but a way to let client cache server disco info.
|
|
||||||
type Caps struct {
|
|
||||||
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
|
||||||
Hash string `xml:"hash,attr"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
Ver string `xml:"ver,attr"`
|
|
||||||
Ext string `xml:"ext,attr,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Supported Stream Features
|
|
||||||
|
|
||||||
// StartTLS feature
|
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
|
||||||
type tlsStartTLS struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML implements custom parsing startTLS required flag
|
|
||||||
func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
stls.XMLName = start.Name
|
|
||||||
|
|
||||||
// Check subelements to extract required field as boolean
|
|
||||||
for {
|
|
||||||
t, err := d.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tt := t.(type) {
|
|
||||||
|
|
||||||
case xml.StartElement:
|
|
||||||
elt := new(Node)
|
|
||||||
|
|
||||||
err = d.DecodeElement(elt, &tt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if elt.XMLName.Local == "required" {
|
|
||||||
stls.Required = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case xml.EndElement:
|
|
||||||
if tt == start.End() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) {
|
|
||||||
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
|
||||||
return sf.StartTLS, true
|
|
||||||
}
|
|
||||||
return feature, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mechanisms
|
|
||||||
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
|
|
||||||
type saslMechanisms struct {
|
|
||||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
|
||||||
Mechanism []string `xml:"mechanism"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamManagement
|
|
||||||
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
|
|
||||||
type streamManagement struct {
|
|
||||||
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
|
||||||
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// P1 extensions
|
|
||||||
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
|
|
||||||
|
|
||||||
// p1:push support
|
|
||||||
type p1Push struct {
|
|
||||||
XMLName xml.Name `xml:"p1:push push"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1:rebind suppor
|
|
||||||
type p1Rebind struct {
|
|
||||||
XMLName xml.Name `xml:"p1:rebind rebind"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// p1:ack support
|
|
||||||
type p1Ack struct {
|
|
||||||
XMLName xml.Name `xml:"p1:ack ack"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// StreamError Packet
|
|
||||||
|
|
||||||
type StreamError struct {
|
|
||||||
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
|
|
||||||
Error xml.Name `xml:",any"`
|
|
||||||
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (StreamError) Name() string {
|
|
||||||
return "stream:error"
|
|
||||||
}
|
|
||||||
|
|
||||||
type streamErrorDecoder struct{}
|
|
||||||
|
|
||||||
var streamError streamErrorDecoder
|
|
||||||
|
|
||||||
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
|
|
||||||
var packet StreamError
|
|
||||||
err := p.DecodeElement(&packet, &se)
|
|
||||||
return packet, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
167
stanza/stream_features.go
Normal file
167
stanza/stream_features.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// StreamFeatures Packet
|
||||||
|
// Reference: The active stream features are published on
|
||||||
|
// https://xmpp.org/registrar/stream-features.html
|
||||||
|
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
|
||||||
|
|
||||||
|
type StreamFeatures struct {
|
||||||
|
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
|
||||||
|
// Server capabilities hash
|
||||||
|
Caps Caps
|
||||||
|
// Stream features
|
||||||
|
StartTLS TlsStartTLS
|
||||||
|
Mechanisms saslMechanisms
|
||||||
|
Bind Bind
|
||||||
|
StreamManagement streamManagement
|
||||||
|
// Obsolete
|
||||||
|
Session StreamSession
|
||||||
|
// ProcessOne Stream Features
|
||||||
|
P1Push p1Push
|
||||||
|
P1Rebind p1Rebind
|
||||||
|
p1Ack p1Ack
|
||||||
|
Any []xml.Name `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StreamFeatures) Name() string {
|
||||||
|
return "stream:features"
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamFeatureDecoder struct{}
|
||||||
|
|
||||||
|
var streamFeatures streamFeatureDecoder
|
||||||
|
|
||||||
|
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
|
||||||
|
var packet StreamFeatures
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capabilities
|
||||||
|
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
|
||||||
|
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
|
||||||
|
// and peer servers do not need to send service discovery requests each time they connect."
|
||||||
|
// This is not a stream feature but a way to let client cache server disco info.
|
||||||
|
type Caps struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
|
||||||
|
Hash string `xml:"hash,attr"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Ver string `xml:"ver,attr"`
|
||||||
|
Ext string `xml:"ext,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Supported Stream Features
|
||||||
|
|
||||||
|
// StartTLS feature
|
||||||
|
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
|
||||||
|
type TlsStartTLS struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
|
||||||
|
Required bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements custom parsing startTLS required flag
|
||||||
|
func (stls *TlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
stls.XMLName = start.Name
|
||||||
|
|
||||||
|
// Check subelements to extract required field as boolean
|
||||||
|
for {
|
||||||
|
t, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tt := t.(type) {
|
||||||
|
|
||||||
|
case xml.StartElement:
|
||||||
|
elt := new(Node)
|
||||||
|
|
||||||
|
err = d.DecodeElement(elt, &tt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if elt.XMLName.Local == "required" {
|
||||||
|
stls.Required = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case xml.EndElement:
|
||||||
|
if tt == start.End() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *StreamFeatures) DoesStartTLS() (feature TlsStartTLS, isSupported bool) {
|
||||||
|
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
|
||||||
|
return sf.StartTLS, true
|
||||||
|
}
|
||||||
|
return feature, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mechanisms
|
||||||
|
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
|
||||||
|
type saslMechanisms struct {
|
||||||
|
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
|
||||||
|
Mechanism []string `xml:"mechanism"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamManagement
|
||||||
|
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
|
||||||
|
type streamManagement struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
|
||||||
|
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// P1 extensions
|
||||||
|
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
|
||||||
|
|
||||||
|
// p1:push support
|
||||||
|
type p1Push struct {
|
||||||
|
XMLName xml.Name `xml:"p1:push push"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// p1:rebind suppor
|
||||||
|
type p1Rebind struct {
|
||||||
|
XMLName xml.Name `xml:"p1:rebind rebind"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// p1:ack support
|
||||||
|
type p1Ack struct {
|
||||||
|
XMLName xml.Name `xml:"p1:ack ack"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// StreamError Packet
|
||||||
|
|
||||||
|
type StreamError struct {
|
||||||
|
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
|
||||||
|
Error xml.Name `xml:",any"`
|
||||||
|
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StreamError) Name() string {
|
||||||
|
return "stream:error"
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamErrorDecoder struct{}
|
||||||
|
|
||||||
|
var streamError streamErrorDecoder
|
||||||
|
|
||||||
|
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
|
||||||
|
var packet StreamError
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
121
stanza/stream_management.go
Normal file
121
stanza/stream_management.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package stanza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NSStreamManagement = "urn:xmpp:sm:3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enabled as defined in Stream Management spec
|
||||||
|
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
|
||||||
|
type SMEnabled struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 enabled"`
|
||||||
|
Id string `xml:"id,attr,omitempty"`
|
||||||
|
Location string `xml:"location,attr,omitempty"`
|
||||||
|
Resume string `xml:"resume,attr,omitempty"`
|
||||||
|
Max uint `xml:"max,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SMEnabled) Name() string {
|
||||||
|
return "Stream Management: enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request as defined in Stream Management spec
|
||||||
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
|
type SMRequest struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 r"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SMRequest) Name() string {
|
||||||
|
return "Stream Management: request"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Answer as defined in Stream Management spec
|
||||||
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
|
type SMAnswer struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 a"`
|
||||||
|
H uint `xml:"h,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SMAnswer) Name() string {
|
||||||
|
return "Stream Management: answer"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resumed as defined in Stream Management spec
|
||||||
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
|
type SMResumed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
|
||||||
|
PrevId string `xml:"previd,attr,omitempty"`
|
||||||
|
H uint `xml:"h,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SMResumed) Name() string {
|
||||||
|
return "Stream Management: resumed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed as defined in Stream Management spec
|
||||||
|
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
|
||||||
|
type SMFailed struct {
|
||||||
|
XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"`
|
||||||
|
// TODO: Handle decoding error cause (need custom parsing).
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SMFailed) Name() string {
|
||||||
|
return "Stream Management: failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
type smDecoder struct{}
|
||||||
|
|
||||||
|
var sm smDecoder
|
||||||
|
|
||||||
|
// decode decodes all known nonza in the stream management namespace.
|
||||||
|
func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) {
|
||||||
|
switch se.Name.Local {
|
||||||
|
case "enabled":
|
||||||
|
return s.decodeEnabled(p, se)
|
||||||
|
case "resumed":
|
||||||
|
return s.decodeResumed(p, se)
|
||||||
|
case "r":
|
||||||
|
return s.decodeRequest(p, se)
|
||||||
|
case "h":
|
||||||
|
return s.decodeAnswer(p, se)
|
||||||
|
case "failed":
|
||||||
|
return s.decodeFailed(p, se)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unexpected XMPP packet " +
|
||||||
|
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smDecoder) decodeEnabled(p *xml.Decoder, se xml.StartElement) (SMEnabled, error) {
|
||||||
|
var packet SMEnabled
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed, error) {
|
||||||
|
var packet SMResumed
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) {
|
||||||
|
var packet SMRequest
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smDecoder) decodeAnswer(p *xml.Decoder, se xml.StartElement) (SMAnswer, error) {
|
||||||
|
var packet SMAnswer
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (smDecoder) decodeFailed(p *xml.Decoder, se xml.StartElement) (SMFailed, error) {
|
||||||
|
var packet SMFailed
|
||||||
|
err := p.DecodeElement(&packet, &se)
|
||||||
|
return packet, err
|
||||||
|
}
|
||||||
@@ -2,10 +2,35 @@ package stanza_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Marshaller / unmarshaller test
|
||||||
|
|
||||||
|
func checkMarshalling(t *testing.T, iq stanza.IQ) (*stanza.IQ, error) {
|
||||||
|
// Marshall
|
||||||
|
data, err := xml.Marshal(iq)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshall
|
||||||
|
var parsedIQ stanza.IQ
|
||||||
|
err = xml.Unmarshal(data, &parsedIQ)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
|
||||||
|
}
|
||||||
|
return &parsedIQ, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// XML structs comparison
|
||||||
|
|
||||||
// Compare iq structure but ignore empty namespace as they are set properly on
|
// Compare iq structure but ignore empty namespace as they are set properly on
|
||||||
// marshal / unmarshal. There is no need to manage them on the manually
|
// marshal / unmarshal. There is no need to manage them on the manually
|
||||||
// crafted structure.
|
// crafted structure.
|
||||||
|
|||||||
@@ -2,17 +2,16 @@ package xmpp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mediated Read / Write on socket
|
// Mediated Read / Write on socket
|
||||||
// Used if logFile from Config is not nil
|
// Used if logFile from Config is not nil
|
||||||
type streamLogger struct {
|
type streamLogger struct {
|
||||||
socket io.ReadWriter // Actual connection
|
socket io.ReadWriter // Actual connection
|
||||||
logFile *os.File
|
logFile io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStreamLogger(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
func newStreamLogger(conn io.ReadWriter, logFile io.Writer) io.ReadWriter {
|
||||||
if logFile == nil {
|
if logFile == nil {
|
||||||
return conn
|
return conn
|
||||||
} else {
|
} else {
|
||||||
@@ -20,21 +19,21 @@ func newStreamLogger(conn io.ReadWriter, logFile *os.File) io.ReadWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sp *streamLogger) Read(p []byte) (n int, err error) {
|
func (sl *streamLogger) Read(p []byte) (n int, err error) {
|
||||||
n, err = sp.socket.Read(p)
|
n, err = sl.socket.Read(p)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
sp.logFile.Write([]byte("RECV:\n")) // Prefix
|
sl.logFile.Write([]byte("RECV:\n")) // Prefix
|
||||||
if n, err := sp.logFile.Write(p[:n]); err != nil {
|
if n, err := sl.logFile.Write(p[:n]); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
sp.logFile.Write([]byte("\n\n")) // Separator
|
sl.logFile.Write([]byte("\n\n")) // Separator
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sp *streamLogger) Write(p []byte) (n int, err error) {
|
func (sl *streamLogger) Write(p []byte) (n int, err error) {
|
||||||
sp.logFile.Write([]byte("SEND:\n")) // Prefix
|
sl.logFile.Write([]byte("SEND:\n")) // Prefix
|
||||||
for _, w := range []io.Writer{sp.socket, sp.logFile} {
|
for _, w := range []io.Writer{sl.socket, sl.logFile} {
|
||||||
n, err = w.Write(p)
|
n, err = w.Write(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -44,7 +43,7 @@ func (sp *streamLogger) Write(p []byte) (n int, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sp.logFile.Write([]byte("\n\n")) // Separator
|
sl.logFile.Write([]byte("\n\n")) // Separator
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,7 +25,9 @@ import (
|
|||||||
// set callback and trigger reconnection.
|
// set callback and trigger reconnection.
|
||||||
type StreamClient interface {
|
type StreamClient interface {
|
||||||
Connect() error
|
Connect() error
|
||||||
|
Resume(state SMState) error
|
||||||
Send(packet stanza.Packet) error
|
Send(packet stanza.Packet) error
|
||||||
|
SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error)
|
||||||
SendRaw(packet string) error
|
SendRaw(packet string) error
|
||||||
Disconnect()
|
Disconnect()
|
||||||
SetHandler(handler EventHandler)
|
SetHandler(handler EventHandler)
|
||||||
@@ -34,6 +37,7 @@ type StreamClient interface {
|
|||||||
// It is mostly use in callback to pass a limited subset of the stream client interface
|
// It is mostly use in callback to pass a limited subset of the stream client interface
|
||||||
type Sender interface {
|
type Sender interface {
|
||||||
Send(packet stanza.Packet) error
|
Send(packet stanza.Packet) error
|
||||||
|
SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error)
|
||||||
SendRaw(packet string) error
|
SendRaw(packet string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +74,7 @@ func (sm *StreamManager) Run() error {
|
|||||||
return errors.New("missing stream client")
|
return errors.New("missing stream client")
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := func(e Event) {
|
handler := func(e Event) error {
|
||||||
switch e.State {
|
switch e.State {
|
||||||
case StateConnected:
|
case StateConnected:
|
||||||
sm.Metrics.setConnectTime()
|
sm.Metrics.setConnectTime()
|
||||||
@@ -78,14 +82,18 @@ func (sm *StreamManager) Run() error {
|
|||||||
sm.Metrics.setLoginTime()
|
sm.Metrics.setLoginTime()
|
||||||
case StateDisconnected:
|
case StateDisconnected:
|
||||||
// Reconnect on disconnection
|
// Reconnect on disconnection
|
||||||
sm.connect()
|
return sm.resume(e.SMState)
|
||||||
case StateStreamError:
|
case StateStreamError:
|
||||||
sm.client.Disconnect()
|
sm.client.Disconnect()
|
||||||
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
||||||
|
// TODO: Make this conflict exception a permanent error
|
||||||
if e.StreamError != "conflict" {
|
if e.StreamError != "conflict" {
|
||||||
sm.connect()
|
return sm.connect()
|
||||||
}
|
}
|
||||||
|
case StatePermanentError:
|
||||||
|
// Do not attempt to reconnect
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
sm.client.SetHandler(handler)
|
sm.client.SetHandler(handler)
|
||||||
|
|
||||||
@@ -106,8 +114,13 @@ func (sm *StreamManager) Stop() {
|
|||||||
sm.wg.Done()
|
sm.wg.Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
|
||||||
func (sm *StreamManager) connect() error {
|
func (sm *StreamManager) connect() error {
|
||||||
|
var state SMState
|
||||||
|
return sm.resume(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resume manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
||||||
|
func (sm *StreamManager) resume(state SMState) error {
|
||||||
var backoff backoff // TODO: Group backoff calculation features with connection manager?
|
var backoff backoff // TODO: Group backoff calculation features with connection manager?
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -115,11 +128,11 @@ func (sm *StreamManager) connect() error {
|
|||||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
||||||
sm.Metrics = initMetrics()
|
sm.Metrics = initMetrics()
|
||||||
|
|
||||||
if err = sm.client.Connect(); err != nil {
|
if err = sm.client.Resume(state); err != nil {
|
||||||
var actualErr ConnError
|
var actualErr ConnError
|
||||||
if xerrors.As(err, &actualErr) {
|
if xerrors.As(err, &actualErr) {
|
||||||
if actualErr.Permanent {
|
if actualErr.Permanent {
|
||||||
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
|
return xerrors.Errorf("unrecoverable connect error %#v", actualErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
backoff.wait()
|
backoff.wait()
|
||||||
@@ -146,7 +159,7 @@ type Metrics struct {
|
|||||||
ConnectTime time.Duration
|
ConnectTime time.Duration
|
||||||
// LoginTime returns the between client initiation of the TCP/IP
|
// LoginTime returns the between client initiation of the TCP/IP
|
||||||
// connection to the server and the return of the login result.
|
// connection to the server and the return of the login result.
|
||||||
// This includes ConnectTime, but also XMPP level protocol negociation
|
// This includes ConnectTime, but also XMPP level protocol negotiation
|
||||||
// like starttls.
|
// like starttls.
|
||||||
LoginTime time.Duration
|
LoginTime time.Duration
|
||||||
}
|
}
|
||||||
|
|||||||
2
test.sh
2
test.sh
@@ -5,7 +5,7 @@ export GO111MODULE=on
|
|||||||
echo "" > coverage.txt
|
echo "" > coverage.txt
|
||||||
|
|
||||||
for d in $(go list ./... | grep -v vendor); do
|
for d in $(go list ./... | grep -v vendor); do
|
||||||
go test -race -coverprofile=profile.out -covermode=atomic ${d}
|
go test -race -coverprofile=profile.out -covermode=atomic "${d}"
|
||||||
if [ -f profile.out ]; then
|
if [ -f profile.out ]; then
|
||||||
cat profile.out >> coverage.txt
|
cat profile.out >> coverage.txt
|
||||||
rm profile.out
|
rm profile.out
|
||||||
|
|||||||
75
transport.go
Normal file
75
transport.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrTransportProtocolNotSupported = errors.New("Transport protocol not supported")
|
||||||
|
var ErrTLSNotSupported = errors.New("Transport does not support StartTLS")
|
||||||
|
|
||||||
|
// TODO: rename to transport config?
|
||||||
|
type TransportConfiguration struct {
|
||||||
|
// Address is the XMPP Host and port to connect to. Host is of
|
||||||
|
// the form 'serverhost:port' i.e "localhost:8888"
|
||||||
|
Address string
|
||||||
|
Domain string
|
||||||
|
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||||
|
// tls.Config must not be modified after having been passed to NewClient. Any
|
||||||
|
// changes made after connecting are ignored.
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
CharsetReader func(charset string, input io.Reader) (io.Reader, error) // passed to xml decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transport interface {
|
||||||
|
Connect() (string, error)
|
||||||
|
DoesStartTLS() bool
|
||||||
|
StartTLS() error
|
||||||
|
|
||||||
|
LogTraffic(logFile io.Writer)
|
||||||
|
|
||||||
|
StartStream() (string, error)
|
||||||
|
GetDecoder() *xml.Decoder
|
||||||
|
IsSecure() bool
|
||||||
|
|
||||||
|
Ping() error
|
||||||
|
Read(p []byte) (n int, err error)
|
||||||
|
Write(p []byte) (n int, err error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientTransport creates a new Transport instance for clients.
|
||||||
|
// The type of transport is determined by the address in the configuration:
|
||||||
|
// - if the address is a URL with the `ws` or `wss` scheme WebsocketTransport is used
|
||||||
|
// - in all other cases a XMPPTransport is used
|
||||||
|
// For XMPPTransport it is mandatory for the address to have a port specified.
|
||||||
|
func NewClientTransport(config TransportConfiguration) Transport {
|
||||||
|
if strings.HasPrefix(config.Address, "ws:") || strings.HasPrefix(config.Address, "wss:") {
|
||||||
|
return &WebsocketTransport{Config: config}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Address = ensurePort(config.Address, 5222)
|
||||||
|
return &XMPPTransport{
|
||||||
|
Config: config,
|
||||||
|
openStatement: clientStreamOpen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewComponentTransport creates a new Transport instance for components.
|
||||||
|
// Only XMPP transports are allowed. If you try to use any other protocol an error
|
||||||
|
// will be returned.
|
||||||
|
func NewComponentTransport(config TransportConfiguration) (Transport, error) {
|
||||||
|
if strings.HasPrefix(config.Address, "ws:") || strings.HasPrefix(config.Address, "wss:") {
|
||||||
|
return nil, fmt.Errorf("Components only support XMPP transport: %w", ErrTransportProtocolNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Address = ensurePort(config.Address, 5222)
|
||||||
|
return &XMPPTransport{
|
||||||
|
Config: config,
|
||||||
|
openStatement: componentStreamOpen,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
179
websocket_transport.go
Normal file
179
websocket_transport.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxPacketSize = 32768
|
||||||
|
|
||||||
|
const pingTimeout = time.Duration(5) * time.Second
|
||||||
|
|
||||||
|
var ServerDoesNotSupportXmppOverWebsocket = errors.New("The websocket server does not support the xmpp subprotocol")
|
||||||
|
|
||||||
|
// The decoder is expected to be initialized after connecting to a server.
|
||||||
|
type WebsocketTransport struct {
|
||||||
|
Config TransportConfiguration
|
||||||
|
decoder *xml.Decoder
|
||||||
|
wsConn *websocket.Conn
|
||||||
|
queue chan []byte
|
||||||
|
logFile io.Writer
|
||||||
|
|
||||||
|
closeCtx context.Context
|
||||||
|
closeFunc context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WebsocketTransport) Connect() (string, error) {
|
||||||
|
t.queue = make(chan []byte, 256)
|
||||||
|
t.closeCtx, t.closeFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
ctx = context.Background()
|
||||||
|
if t.Config.ConnectTimeout > 0 {
|
||||||
|
var cancelConnect context.CancelFunc
|
||||||
|
ctx, cancelConnect = context.WithTimeout(t.closeCtx, time.Duration(t.Config.ConnectTimeout)*time.Second)
|
||||||
|
defer cancelConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConn, response, err := websocket.Dial(ctx, t.Config.Address, &websocket.DialOptions{
|
||||||
|
Subprotocols: []string{"xmpp"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", NewConnError(err, true)
|
||||||
|
}
|
||||||
|
if response.Header.Get("Sec-WebSocket-Protocol") != "xmpp" {
|
||||||
|
t.cleanup(websocket.StatusBadGateway)
|
||||||
|
return "", NewConnError(ServerDoesNotSupportXmppOverWebsocket, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConn.SetReadLimit(maxPacketSize)
|
||||||
|
t.wsConn = wsConn
|
||||||
|
t.startReader()
|
||||||
|
|
||||||
|
t.decoder = xml.NewDecoder(bufio.NewReaderSize(t, maxPacketSize))
|
||||||
|
t.decoder.CharsetReader = t.Config.CharsetReader
|
||||||
|
|
||||||
|
return t.StartStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) StartStream() (string, error) {
|
||||||
|
if _, err := fmt.Fprintf(t, `<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" to="%s" version="1.0" />`, t.Config.Domain); err != nil {
|
||||||
|
t.cleanup(websocket.StatusBadGateway)
|
||||||
|
return "", NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, err := stanza.InitStream(t.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
t.Close()
|
||||||
|
return "", NewConnError(err, false)
|
||||||
|
}
|
||||||
|
return sessionID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startReader runs a go function that keeps reading from the websocket. This
|
||||||
|
// is required to allow Ping() to work: Ping requires a Reader to be running
|
||||||
|
// to process incoming control frames.
|
||||||
|
func (t WebsocketTransport) startReader() {
|
||||||
|
go func() {
|
||||||
|
buffer := make([]byte, maxPacketSize)
|
||||||
|
for {
|
||||||
|
_, reader, err := t.wsConn.Reader(t.closeCtx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := reader.Read(buffer)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
// We need to make a copy, otherwise we will overwrite the slice content
|
||||||
|
// on the next iteration of the for loop.
|
||||||
|
tmp := make([]byte, n)
|
||||||
|
copy(tmp, buffer)
|
||||||
|
t.queue <- tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) StartTLS() error {
|
||||||
|
return ErrTLSNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) DoesStartTLS() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) GetDomain() string {
|
||||||
|
return t.Config.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) GetDecoder() *xml.Decoder {
|
||||||
|
return t.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) IsSecure() bool {
|
||||||
|
return strings.HasPrefix(t.Config.Address, "wss:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) Ping() error {
|
||||||
|
ctx, cancel := context.WithTimeout(t.closeCtx, pingTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return t.wsConn.Ping(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WebsocketTransport) Read(p []byte) (int, error) {
|
||||||
|
select {
|
||||||
|
case <-t.closeCtx.Done():
|
||||||
|
return 0, t.closeCtx.Err()
|
||||||
|
case data := <-t.queue:
|
||||||
|
if t.logFile != nil && len(data) > 0 {
|
||||||
|
_, _ = fmt.Fprintf(t.logFile, "RECV:\n%s\n\n", data)
|
||||||
|
}
|
||||||
|
copy(p, data)
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) Write(p []byte) (int, error) {
|
||||||
|
if t.logFile != nil {
|
||||||
|
_, _ = fmt.Fprintf(t.logFile, "SEND:\n%s\n\n", p)
|
||||||
|
}
|
||||||
|
return len(p), t.wsConn.Write(t.closeCtx, websocket.MessageText, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t WebsocketTransport) Close() error {
|
||||||
|
t.Write([]byte("<close xmlns=\"urn:ietf:params:xml:ns:xmpp-framing\" />"))
|
||||||
|
return t.cleanup(websocket.StatusGoingAway)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WebsocketTransport) LogTraffic(logFile io.Writer) {
|
||||||
|
t.logFile = logFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WebsocketTransport) cleanup(code websocket.StatusCode) error {
|
||||||
|
var err error
|
||||||
|
if t.queue != nil {
|
||||||
|
close(t.queue)
|
||||||
|
t.queue = nil
|
||||||
|
}
|
||||||
|
if t.wsConn != nil {
|
||||||
|
err = t.wsConn.Close(websocket.StatusGoingAway, "Done")
|
||||||
|
t.wsConn = nil
|
||||||
|
}
|
||||||
|
if t.closeFunc != nil {
|
||||||
|
t.closeFunc()
|
||||||
|
t.closeFunc = nil
|
||||||
|
t.closeCtx = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
145
xmpp_transport.go
Normal file
145
xmpp_transport.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gosrc.io/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// XMPPTransport implements the XMPP native TCP transport
|
||||||
|
// The decoder is expected to be initialized after connecting to a server.
|
||||||
|
type XMPPTransport struct {
|
||||||
|
openStatement string
|
||||||
|
Config TransportConfiguration
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
decoder *xml.Decoder
|
||||||
|
conn net.Conn
|
||||||
|
readWriter io.ReadWriter
|
||||||
|
logFile io.Writer
|
||||||
|
isSecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentStreamOpen = fmt.Sprintf("<?xml version='1.0'?><stream:stream to='%%s' xmlns='%s' xmlns:stream='%s'>", stanza.NSComponent, stanza.NSStream)
|
||||||
|
|
||||||
|
var clientStreamOpen = fmt.Sprintf("<?xml version='1.0'?><stream:stream to='%%s' xmlns='%s' xmlns:stream='%s' version='1.0'>", stanza.NSClient, stanza.NSStream)
|
||||||
|
|
||||||
|
func (t *XMPPTransport) Connect() (string, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
t.conn, err = net.DialTimeout("tcp", t.Config.Address, time.Duration(t.Config.ConnectTimeout)*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return "", NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.readWriter = newStreamLogger(t.conn, t.logFile)
|
||||||
|
t.decoder = xml.NewDecoder(bufio.NewReaderSize(t.readWriter, maxPacketSize))
|
||||||
|
t.decoder.CharsetReader = t.Config.CharsetReader
|
||||||
|
return t.StartStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) StartStream() (string, error) {
|
||||||
|
if _, err := fmt.Fprintf(t, t.openStatement, t.Config.Domain); err != nil {
|
||||||
|
t.Close()
|
||||||
|
return "", NewConnError(err, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, err := stanza.InitStream(t.GetDecoder())
|
||||||
|
if err != nil {
|
||||||
|
t.Close()
|
||||||
|
return "", NewConnError(err, false)
|
||||||
|
}
|
||||||
|
return sessionID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) DoesStartTLS() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) GetDomain() string {
|
||||||
|
return t.Config.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) GetDecoder() *xml.Decoder {
|
||||||
|
return t.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) IsSecure() bool {
|
||||||
|
return t.isSecure
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *XMPPTransport) StartTLS() error {
|
||||||
|
if t.Config.TLSConfig == nil {
|
||||||
|
t.TLSConfig = &tls.Config{}
|
||||||
|
} else {
|
||||||
|
t.TLSConfig = t.Config.TLSConfig.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.TLSConfig.ServerName == "" {
|
||||||
|
t.TLSConfig.ServerName = t.Config.Domain
|
||||||
|
}
|
||||||
|
tlsConn := tls.Client(t.conn, t.TLSConfig)
|
||||||
|
// We convert existing connection to TLS
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.conn = tlsConn
|
||||||
|
t.readWriter = newStreamLogger(tlsConn, t.logFile)
|
||||||
|
t.decoder = xml.NewDecoder(bufio.NewReaderSize(t.readWriter, maxPacketSize))
|
||||||
|
t.decoder.CharsetReader = t.Config.CharsetReader
|
||||||
|
|
||||||
|
if !t.TLSConfig.InsecureSkipVerify {
|
||||||
|
if err := tlsConn.VerifyHostname(t.Config.Domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.isSecure = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) Ping() error {
|
||||||
|
n, err := t.conn.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
return errors.New("Could not write ping")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) Read(p []byte) (n int, err error) {
|
||||||
|
if t.readWriter == nil {
|
||||||
|
return 0, errors.New("cannot read: not connected, no readwriter")
|
||||||
|
}
|
||||||
|
return t.readWriter.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) Write(p []byte) (n int, err error) {
|
||||||
|
if t.readWriter == nil {
|
||||||
|
return 0, errors.New("cannot write: not connected, no readwriter")
|
||||||
|
}
|
||||||
|
return t.readWriter.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t XMPPTransport) Close() error {
|
||||||
|
if t.readWriter != nil {
|
||||||
|
_, _ = t.readWriter.Write([]byte("</stream:stream>"))
|
||||||
|
}
|
||||||
|
if t.conn != nil {
|
||||||
|
return t.conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *XMPPTransport) LogTraffic(logFile io.Writer) {
|
||||||
|
t.logFile = logFile
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user