mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2025-11-06 02:03:43 -08:00
Compare commits
48 Commits
stanza-bui
...
v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
README.md
28
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 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.
|
||||
|
||||
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",
|
||||
Password: "test",
|
||||
TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
```
|
||||
|
||||
## Supported specifications
|
||||
|
||||
### Clients
|
||||
|
||||
@@ -99,26 +99,9 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
}
|
||||
|
||||
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
|
||||
// Higher level discovery
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
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
|
||||
disco := iqResp.DiscoInfo()
|
||||
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
}
|
||||
|
||||
func discoInfoPubSub(iqResp *stanza.IQ) {
|
||||
|
||||
@@ -3,7 +3,6 @@ module gosrc.io/xmpp/_examples
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/processone/mpg123 v1.0.0
|
||||
github.com/processone/soundcloud v1.0.0
|
||||
gosrc.io/xmpp v0.1.1
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
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/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
@@ -61,25 +60,9 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
}
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
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
|
||||
disco := iqResp.DiscoInfo()
|
||||
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
|
||||
@@ -97,16 +80,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"})
|
||||
items := iqResp.DiscoItems()
|
||||
|
||||
var payload stanza.DiscoItems
|
||||
if discoItems.Node == "" {
|
||||
payload = stanza.DiscoItems{
|
||||
Items: []stanza.DiscoItem{
|
||||
{Name: "test node", JID: "service.localhost", Node: "node1"},
|
||||
},
|
||||
}
|
||||
items.AddItem("service.localhost", "node1", "test node")
|
||||
}
|
||||
iqResp.Payload = &payload
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
|
||||
@@ -118,9 +96,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"})
|
||||
var payload stanza.Version
|
||||
payload.Name = "Fluux XMPP Component"
|
||||
payload.Version = "0.0.1"
|
||||
iq.Payload = &payload
|
||||
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func main() {
|
||||
Password: "test",
|
||||
StreamLogger: os.Stdout,
|
||||
Insecure: true,
|
||||
// TLSConfig: tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
router := xmpp.NewRouter()
|
||||
|
||||
@@ -86,8 +86,9 @@ func (c *ServerCheck) Check() error {
|
||||
return fmt.Errorf("expecting starttls proceed: %s", err)
|
||||
}
|
||||
|
||||
stanza.DefaultTlsConfig.ServerName = c.domain
|
||||
tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.ServerName = c.domain
|
||||
tlsConn := tls.Client(tcpconn, &tlsConfig)
|
||||
// We convert existing connection to TLS
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return err
|
||||
|
||||
85
client.go
85
client.go
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +32,18 @@ type Event struct {
|
||||
State ConnState
|
||||
Description 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
|
||||
@@ -52,6 +65,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) {
|
||||
em.CurrentState = StateStreamError
|
||||
if em.Handler != nil {
|
||||
@@ -96,9 +116,23 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
||||
return nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
// fallback to jid domain
|
||||
// Fallback to jid domain
|
||||
if config.Address == "" {
|
||||
config.Address = config.parsedJid.Domain
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Address = ensurePort(config.Address, 5222)
|
||||
|
||||
@@ -114,7 +148,15 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
|
||||
}
|
||||
|
||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
||||
// Connect simply triggers resumption, with an empty session state.
|
||||
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
|
||||
|
||||
c.conn, err = net.DialTimeout("tcp", c.config.Address, time.Duration(c.config.ConnectTimeout)*time.Second)
|
||||
@@ -124,30 +166,34 @@ func (c *Client) Connect() error {
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
if c.conn, c.Session, err = NewSession(c.conn, c.config); err != nil {
|
||||
if c.conn, c.Session, err = NewSession(c.conn, c.config, state); err != nil {
|
||||
return err
|
||||
}
|
||||
c.updateState(StateSessionEstablished)
|
||||
|
||||
// Start the keepalive go routine
|
||||
keepaliveQuit := make(chan struct{})
|
||||
go keepalive(c.conn, keepaliveQuit)
|
||||
// Start the receiver go routine
|
||||
state = c.Session.SMState
|
||||
go c.recv(state, keepaliveQuit)
|
||||
|
||||
// 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")
|
||||
// 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 ?
|
||||
fmt.Fprintf(c.Session.streamLogger, "<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
|
||||
}
|
||||
|
||||
func (c *Client) Disconnect() {
|
||||
_ = c.SendRaw("</stream:stream>")
|
||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
||||
_ = c.conn.Close()
|
||||
conn := c.conn
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetHandler(handler EventHandler) {
|
||||
@@ -166,7 +212,7 @@ func (c *Client) Send(packet stanza.Packet) error {
|
||||
return errors.New("cannot marshal packet " + err.Error())
|
||||
}
|
||||
|
||||
return c.sendWithLogger(string(data))
|
||||
return c.sendWithWriter(c.Session.streamLogger, data)
|
||||
}
|
||||
|
||||
// SendRaw sends an XMPP stanza as a string to the server.
|
||||
@@ -179,12 +225,12 @@ func (c *Client) SendRaw(packet string) error {
|
||||
return errors.New("client is not connected")
|
||||
}
|
||||
|
||||
return c.sendWithLogger(packet)
|
||||
return c.sendWithWriter(c.Session.streamLogger, []byte(packet))
|
||||
}
|
||||
|
||||
func (c *Client) sendWithLogger(packet string) error {
|
||||
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
||||
var err error
|
||||
_, err = fmt.Fprintf(c.Session.streamLogger, packet)
|
||||
_, err = writer.Write(packet)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -192,12 +238,12 @@ func (c *Client) sendWithLogger(packet string) error {
|
||||
// Go routines
|
||||
|
||||
// Loop: Receive data from server
|
||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
||||
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) {
|
||||
for {
|
||||
val, err := stanza.NextPacket(c.Session.decoder)
|
||||
if err != nil {
|
||||
close(keepaliveQuit)
|
||||
c.updateState(StateDisconnected)
|
||||
c.disconnected(state)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -208,6 +254,15 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
||||
close(keepaliveQuit)
|
||||
c.streamError(packet.Error.Local, packet.Text)
|
||||
return errors.New("stream error: " + packet.Error.Local)
|
||||
// Process Stream management nonzas
|
||||
case stanza.SMRequest:
|
||||
answer := stanza.SMAnswer{XMLName: xml.Name{
|
||||
Space: stanza.NSStreamManagement,
|
||||
Local: "a",
|
||||
}, H: state.Inbound}
|
||||
c.Send(answer)
|
||||
default:
|
||||
state.Inbound++
|
||||
}
|
||||
|
||||
c.router.route(c, val)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
134
cmd/fluuxmpp/send.go
Normal file
134
cmd/fluuxmpp/send.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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{
|
||||
Jid: viper.GetString("jid"),
|
||||
Address: viper.GetString("addr"),
|
||||
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.12
|
||||
|
||||
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 => ./../
|
||||
160
cmd/go.sum
Normal file
160
cmd/go.sum
Normal file
@@ -0,0 +1,160 @@
|
||||
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/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/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.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/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-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/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/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/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/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/konsorten/go-windows-terminal-sequences v1.0.1/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/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/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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
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.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
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.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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
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.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-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/net v0.0.0-20180826012351-8a410e7b638d/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/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/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-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/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/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/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/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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -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")
|
||||
}
|
||||
29
component.go
29
component.go
@@ -66,52 +66,67 @@ func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
|
||||
// Connect triggers component connection to XMPP server component port.
|
||||
// TODO: Failed handshake should be a permanent error
|
||||
func (c *Component) Connect() error {
|
||||
var state SMState
|
||||
return c.Resume(state)
|
||||
}
|
||||
func (c *Component) Resume(sm SMState) error {
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// 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.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("cannot send stream open "+err.Error()), false)
|
||||
}
|
||||
c.decoder = xml.NewDecoder(conn)
|
||||
|
||||
// 2. Initialize xml decoder and extract streamID from reply
|
||||
streamId, err := stanza.InitStream(c.decoder)
|
||||
if err != nil {
|
||||
return errors.New("cannot init decoder " + err.Error())
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("cannot init decoder "+err.Error()), false)
|
||||
}
|
||||
|
||||
// 3. Authentication
|
||||
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
|
||||
return errors.New("cannot send handshake " + err.Error())
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
|
||||
}
|
||||
|
||||
// 4. Check server response for authentication
|
||||
val, err := stanza.NextPacket(c.decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
c.updateState(StateDisconnected)
|
||||
return NewConnError(err, true)
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
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:
|
||||
// Start the receiver go routine
|
||||
c.updateState(StateSessionEstablished)
|
||||
go c.recv()
|
||||
return nil
|
||||
default:
|
||||
return errors.New("expecting handshake result, got " + v.Name())
|
||||
c.updateState(StateStreamError)
|
||||
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Component) Disconnect() {
|
||||
_ = c.SendRaw("</stream:stream>")
|
||||
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
|
||||
_ = c.conn.Close()
|
||||
conn := c.conn
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Component) SetHandler(handler EventHandler) {
|
||||
|
||||
@@ -23,3 +23,10 @@ func TestHandshake(t *testing.T) {
|
||||
func TestGenerateHandshake(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Test that NewStreamManager can accept a Component.
|
||||
//
|
||||
// This validates that Component conforms to StreamClient interface.
|
||||
func TestStreamManager(t *testing.T) {
|
||||
NewStreamManager(&Component{}, nil)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
@@ -13,6 +14,9 @@ type Config struct {
|
||||
StreamLogger *os.File // Used for debugging
|
||||
Lang string // TODO: should default to 'en'
|
||||
ConnectTimeout int // Client timeout in seconds. Default to 15
|
||||
// tls.Config must not be modified after having been passed to NewClient. The
|
||||
// Client connect method may override the tls.Config.ServerName if it was not set.
|
||||
TLSConfig *tls.Config
|
||||
// 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.
|
||||
Insecure bool
|
||||
|
||||
4
go.mod
4
go.mod
@@ -3,6 +3,6 @@ module gosrc.io/xmpp
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.2.0
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||
github.com/google/go-cmp v0.3.0
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||
)
|
||||
|
||||
6
go.sum
6
go.sum
@@ -1,4 +1,6 @@
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
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=
|
||||
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=
|
||||
|
||||
17
router.go
17
router.go
@@ -98,7 +98,7 @@ type Handler interface {
|
||||
type Route struct {
|
||||
handler Handler
|
||||
// Matchers are used to "specialize" routes and focus on specific packet features
|
||||
matchers []matcher
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
func (r *Route) Handler(handler Handler) *Route {
|
||||
@@ -122,8 +122,8 @@ func (r *Route) HandlerFunc(f HandlerFunc) *Route {
|
||||
return r.Handler(f)
|
||||
}
|
||||
|
||||
// addMatcher adds a matcher to the route
|
||||
func (r *Route) addMatcher(m matcher) *Route {
|
||||
// AddMatcher adds a matcher to the route
|
||||
func (r *Route) AddMatcher(m Matcher) *Route {
|
||||
r.matchers = append(r.matchers, m)
|
||||
return r
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
// It matches on the Local part of the xml.Name
|
||||
func (r *Route) Packet(name string) *Route {
|
||||
name = strings.ToLower(name)
|
||||
return r.addMatcher(nameMatcher(name))
|
||||
return r.AddMatcher(nameMatcher(name))
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
@@ -204,7 +204,7 @@ func (r *Route) StanzaType(types ...string) *Route {
|
||||
for k, v := range types {
|
||||
types[k] = strings.ToLower(v)
|
||||
}
|
||||
return r.addMatcher(nsTypeMatcher(types))
|
||||
return r.AddMatcher(nsTypeMatcher(types))
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
@@ -229,14 +229,15 @@ func (r *Route) IQNamespaces(namespaces ...string) *Route {
|
||||
for k, v := range namespaces {
|
||||
namespaces[k] = strings.ToLower(v)
|
||||
}
|
||||
return r.addMatcher(nsIQMatcher(namespaces))
|
||||
return r.AddMatcher(nsIQMatcher(namespaces))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Matchers
|
||||
|
||||
// Matchers are used to "specialize" routes and focus on specific packet features
|
||||
type matcher interface {
|
||||
// Matchers are used to "specialize" routes and focus on specific packet features.
|
||||
// You can register attach them to a route via the AddMatcher method.
|
||||
type Matcher interface {
|
||||
Match(stanza.Packet, *RouteMatch) bool
|
||||
}
|
||||
|
||||
|
||||
115
session.go
115
session.go
@@ -17,6 +17,7 @@ type Session struct {
|
||||
// Session info
|
||||
BindJid string // Jabber ID as provided by XMPP server
|
||||
StreamId string
|
||||
SMState SMState
|
||||
Features stanza.StreamFeatures
|
||||
TlsEnabled bool
|
||||
lastPacketId int
|
||||
@@ -29,29 +30,44 @@ type Session struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
|
||||
func NewSession(conn net.Conn, o Config, state SMState) (net.Conn, *Session, error) {
|
||||
s := new(Session)
|
||||
s.SMState = state
|
||||
s.init(conn, o)
|
||||
|
||||
// starttls
|
||||
var tlsConn net.Conn
|
||||
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain)
|
||||
if s.TlsEnabled {
|
||||
s.reset(conn, tlsConn, o)
|
||||
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
|
||||
|
||||
if s.err != nil {
|
||||
return nil, nil, NewConnError(s.err, true)
|
||||
}
|
||||
|
||||
if !s.TlsEnabled && !o.Insecure {
|
||||
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
|
||||
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
|
||||
return nil, nil, NewConnError(err, true)
|
||||
}
|
||||
|
||||
if s.TlsEnabled {
|
||||
s.reset(conn, tlsConn, o)
|
||||
}
|
||||
|
||||
// auth
|
||||
s.auth(o)
|
||||
s.reset(tlsConn, tlsConn, o)
|
||||
|
||||
// bind resource and 'start' XMPP session
|
||||
// attempt resumption
|
||||
if s.resume(o) {
|
||||
return tlsConn, s, s.err
|
||||
}
|
||||
|
||||
// otherwise, bind resource and 'start' XMPP session
|
||||
s.bind(o)
|
||||
s.rfc3921Session(o)
|
||||
|
||||
// Enable stream management if supported
|
||||
s.EnableStreamManagement(o)
|
||||
|
||||
return tlsConn, s, s.err
|
||||
}
|
||||
|
||||
@@ -101,7 +117,7 @@ func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
||||
func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) net.Conn {
|
||||
if s.err != nil {
|
||||
return conn
|
||||
}
|
||||
@@ -114,21 +130,35 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
||||
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||
return conn
|
||||
}
|
||||
s.TlsEnabled = true
|
||||
|
||||
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
||||
stanza.DefaultTlsConfig.ServerName = domain
|
||||
tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig)
|
||||
if o.TLSConfig == nil {
|
||||
o.TLSConfig = &tls.Config{}
|
||||
}
|
||||
|
||||
if o.TLSConfig.ServerName == "" {
|
||||
o.TLSConfig.ServerName = domain
|
||||
}
|
||||
tlsConn := tls.Client(conn, o.TLSConfig)
|
||||
// We convert existing connection to TLS
|
||||
if s.err = tlsConn.Handshake(); s.err != nil {
|
||||
return tlsConn
|
||||
}
|
||||
|
||||
// We check that cert matches hostname
|
||||
s.err = tlsConn.VerifyHostname(domain)
|
||||
if !o.TLSConfig.InsecureSkipVerify {
|
||||
s.err = tlsConn.VerifyHostname(domain)
|
||||
}
|
||||
|
||||
if s.err == nil {
|
||||
s.TlsEnabled = true
|
||||
}
|
||||
return tlsConn
|
||||
}
|
||||
|
||||
// If we do not allow cleartext connections, make it explicit that server do not support starttls
|
||||
if !o.Insecure {
|
||||
s.err = errors.New("XMPP server does not advertise support for starttls")
|
||||
}
|
||||
|
||||
// starttls is not supported => we do not upgrade the connection:
|
||||
return conn
|
||||
}
|
||||
@@ -141,6 +171,38 @@ func (s *Session) auth(o Config) {
|
||||
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
|
||||
}
|
||||
|
||||
// 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.streamLogger, "<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.decoder)
|
||||
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) {
|
||||
if s.err != nil {
|
||||
return
|
||||
@@ -188,3 +250,30 @@ func (s *Session) rfc3921Session(o Config) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.streamLogger, "<enable xmlns='%s' resume='true'/>", stanza.NSStreamManagement)
|
||||
|
||||
var packet stanza.Packet
|
||||
packet, s.err = stanza.NextPacket(s.decoder)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Errorf("Paylod is not an iot control set: %v", cs)
|
||||
t.Errorf("Payload is not an iot control set: %v", cs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -23,7 +22,7 @@ type IQ struct { // Info/Query
|
||||
// request."
|
||||
Payload IQPayload `xml:",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"`
|
||||
}
|
||||
|
||||
@@ -99,7 +98,6 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var xmppError Err
|
||||
err = d.DecodeElement(&xmppError, &tt)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
iq.Error = xmppError
|
||||
|
||||
@@ -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
|
||||
return d
|
||||
}
|
||||
|
||||
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 {
|
||||
d.Features = []Feature{}
|
||||
for _, ns := range namespace {
|
||||
d.Features = append(d.Features, Feature{Var: ns})
|
||||
}
|
||||
@@ -104,11 +106,38 @@ func (d *DiscoItems) Namespace() string {
|
||||
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 {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
JID string `xml:"jid,attr,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -7,29 +7,22 @@ import (
|
||||
"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"})
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
parsedIQ, err := checkMarshalling(t, iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
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
|
||||
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain an IQ payload")
|
||||
t.Errorf("Parsed stanza does not contain correct IQ payload")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,26 @@ func (v *Version) Namespace() string {
|
||||
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
|
||||
|
||||
|
||||
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 {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",innerxml"`
|
||||
Content string `xml:",cdata"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
@@ -47,5 +47,8 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
if n.Content != "" {
|
||||
e.EncodeToken(xml.CharData(n.Content))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,8 @@ func NextPacket(p *xml.Decoder) (Packet, error) {
|
||||
return decodeClient(p, se)
|
||||
case NSComponent:
|
||||
return decodeComponent(p, se)
|
||||
case NSStreamManagement:
|
||||
return sm.decode(p, se)
|
||||
default:
|
||||
return nil, errors.New("unknown namespace " +
|
||||
se.Name.Space + " <" + se.Name.Local + "/>")
|
||||
@@ -133,7 +135,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) {
|
||||
switch se.Name.Local {
|
||||
case "handshake": // handshake is used to authenticate components
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
var DefaultTlsConfig tls.Config
|
||||
|
||||
// Used during stream initiation / session establishment
|
||||
type TLSProceed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||
|
||||
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 (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"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
|
||||
// marshal / unmarshal. There is no need to manage them on the manually
|
||||
// crafted structure.
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
// set callback and trigger reconnection.
|
||||
type StreamClient interface {
|
||||
Connect() error
|
||||
Resume(state SMState) error
|
||||
Send(packet stanza.Packet) error
|
||||
SendRaw(packet string) error
|
||||
Disconnect()
|
||||
@@ -78,7 +79,7 @@ func (sm *StreamManager) Run() error {
|
||||
sm.Metrics.setLoginTime()
|
||||
case StateDisconnected:
|
||||
// Reconnect on disconnection
|
||||
sm.connect()
|
||||
sm.resume(e.SMState)
|
||||
case StateStreamError:
|
||||
sm.client.Disconnect()
|
||||
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
|
||||
@@ -106,8 +107,13 @@ func (sm *StreamManager) Stop() {
|
||||
sm.wg.Done()
|
||||
}
|
||||
|
||||
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
|
||||
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?
|
||||
|
||||
for {
|
||||
@@ -115,11 +121,11 @@ func (sm *StreamManager) connect() error {
|
||||
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
|
||||
sm.Metrics = initMetrics()
|
||||
|
||||
if err = sm.client.Connect(); err != nil {
|
||||
if err = sm.client.Resume(state); err != nil {
|
||||
var actualErr ConnError
|
||||
if xerrors.As(err, &actualErr) {
|
||||
if actualErr.Permanent {
|
||||
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
|
||||
return xerrors.Errorf("unrecoverable connect error %#v", actualErr)
|
||||
}
|
||||
}
|
||||
backoff.wait()
|
||||
@@ -146,7 +152,7 @@ type Metrics struct {
|
||||
ConnectTime time.Duration
|
||||
// LoginTime returns the between client initiation of the TCP/IP
|
||||
// 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.
|
||||
LoginTime time.Duration
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user