51 Commits

Author SHA1 Message Date
Mickael Remond
2781563ea7 Update go.sum file 2019-10-01 11:40:31 +02:00
Mickael Remond
4f68c5eee2 Add X-OAUTH2 authentication and example 2019-10-01 11:40:31 +02:00
Mickael Remond
9c8353d081 Introduce Credential structure to define auth type
For now we are planning to support Password and OAuthToken.
In the future, we would like to add certificate-based authentication.
2019-10-01 11:40:31 +02:00
Mickael Remond
3b66e31888 We do not need the Content to be innerxml. cdata is enough.
Fixes #110
2019-09-27 18:25:27 +02:00
Mickael Remond
005c8823d9 Fixes issue with unescaped character %
Fixes #113
2019-09-27 16:32:53 +02:00
Mickael Remond
2cdda48467 Update xerrors dependency for go 1.13 2019-09-27 16:32:53 +02:00
Mickael Remond
8794ea6ed8 Check if we have a connection open before trying to close it
Fixes #22
2019-09-06 10:28:49 +02:00
Martin/Geno
7e596fc33c quick fix for component see #47 2019-09-06 09:27:38 +02:00
Martin/Geno
6f9808fe16 fix(doc): typos 2019-09-05 10:54:28 +02:00
Wichert Akkerman
7b1f83f6b7 Expose Route.AddMatcher
This fixes #102
2019-09-03 14:38:34 +02:00
Martin/Geno
6005a964ba fix stream management for component + add test 2019-09-02 11:50:29 +02:00
Mickael Remond
d3b45b42a5 Fix typo 2019-08-06 18:27:51 +02:00
Martin/Geno
6e65ba2a0b fix naming from fluxxmpp to fluuxmpp 2019-08-06 18:06:34 +02:00
Martin/Geno
76f59be5ed merge commandline tools 2019-08-06 18:06:34 +02:00
Mickael Remond
80d2e0fa1e Remove debug print-out 2019-08-06 16:02:33 +02:00
Mickael Remond
2e864ff7f6 Clean-up: remove test/debug code 2019-08-06 16:02:33 +02:00
Mickael Remond
3de99e0e0e Add initial support for stream management
For now it support enabling SM, replying to ack requests from server,
and trying resuming the session with existing Stream Management state.
2019-08-06 16:02:33 +02:00
Mickael Remond
e531370dc9 An invalid certificate is a permanent error if we do not skip cert check 2019-07-31 11:43:54 +02:00
Mickael Remond
4e185f4bb6 Use intermediate version (before 0.2.0) to fix stanza package usage 2019-07-30 10:55:49 +02:00
Mickael Remond
4f1e0ded97 Simplify disco with builder helpers 2019-07-30 10:45:20 +02:00
Mickael Remond
176dcdce33 Simplify disco and software version
Make use of helpers.
2019-07-30 10:45:20 +02:00
Mickael Remond
61adf7e414 Add builder & test on software version helpers 2019-07-30 10:45:20 +02:00
Mickael Remond
014957e029 Expand comments 2019-07-30 10:45:20 +02:00
Mickael Remond
69118a952a Add helpers for IQ DiscoItems 2019-07-30 10:45:20 +02:00
Mickael Remond
1c74d102c7 Fix reference to missing tag 2019-07-30 10:39:19 +02:00
Mickael Remond
7ab6c3a62d Refactor to start removing global variables 2019-07-27 18:06:55 -07:00
Mickael Remond
a3867dd0b3 Expand TODO list 2019-07-27 17:50:45 -07:00
Mickael Remond
d2a1329dc6 Report errors 2019-07-27 17:50:28 -07:00
Mickael Remond
6ff7812ac4 go mod tidy 2019-07-27 17:34:10 -07:00
Mickael Remond
3453336f27 For now we need to use master version for xmpp module 2019-07-27 17:31:11 -07:00
Mickael Remond
a23194ad96 Add submodule for commands
The goal is to keep dependencies list minimal for users of the xmpp
modules. We do not want to force to increase largely the number of
indirect dependencies when you require xmpp.

The command-line stuff may not likely be needed in the end developer
application.
2019-07-27 17:15:28 -07:00
Mickael Remond
f984a93e63 Formatting 2019-07-27 16:50:41 -07:00
Mickael Remond
6a5f2750f1 Clean-up 2019-07-27 16:50:10 -07:00
Mickaël Rémond
e553028754 Minor wording fixes 2019-07-27 16:36:35 -07:00
Mickaël Rémond
fed23ad7ad Minor improvements for sendxmpp doc 2019-07-27 16:36:35 -07:00
Mickaël Rémond
244acdc02a Fix typos 2019-07-27 16:36:35 -07:00
Mickaël Rémond
4d6c783619 Improve wording 2019-07-27 16:36:35 -07:00
Martin/Geno
5697d40e5c use - instatt of --stdin to detect stdin 2019-07-27 16:36:35 -07:00
genofire
ff5885f29d todo for sendxmpp 2019-07-27 16:36:35 -07:00
Martin/Geno
e3e57ac803 add parameter and config for address to sendxmpp 2019-07-27 16:36:35 -07:00
Martin/Geno
3daa5c505c fix README.md 2019-07-27 16:36:35 -07:00
Martin/Geno
0fb90abcf7 improve authentification 2019-07-27 16:36:35 -07:00
Martin/Geno
6aa942dd58 first idea of sendxmpp 2019-07-27 16:36:35 -07:00
Mickael Remond
c41d068c9f Improve comments 2019-07-27 15:19:32 -07:00
Mickael Remond
9f095cb90f Update dependencies 2019-07-27 09:22:44 -07:00
Mickael Remond
7deaf59642 Quickfix for build error
See #94
2019-07-27 09:22:04 -07:00
genofire
fe6cea870d use highest DNS-SRV entry for client connection 2019-07-27 09:11:00 -07:00
Martin/Geno
323de704f6 improve command xmpp-check 2019-07-17 10:41:49 +02:00
Martin/Geno
e05f36c69f init empty TLSConfig, if nothing given 2019-07-16 11:00:42 +02:00
Mickael Remond
d36428fb2f Avoid copying tls.Config lock
Fixes #90
2019-07-15 18:40:20 +02:00
Mickael Remond
9577036327 Add support for self-signed certificates 2019-07-15 12:22:21 +02:00
42 changed files with 1276 additions and 179 deletions

View File

@@ -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",
Credential: xmpp.Password("Test"),
TLSConfig: tls.Config{InsecureSkipVerify: true},
}
```
## Supported specifications
### Clients
@@ -70,7 +96,7 @@ func main() {
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
Credential: xmpp.Password("Test"),
StreamLogger: os.Stdout,
Insecure: true,
}

View File

@@ -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

View File

@@ -1,10 +1,8 @@
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 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -17,9 +17,10 @@ func main() {
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
Credential: xmpp.Password("test"),
StreamLogger: os.Stdout,
Insecure: true,
// TLSConfig: tls.Config{InsecureSkipVerify: true},
}
router := xmpp.NewRouter()
@@ -47,6 +48,3 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) {
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
// TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
// (using templates ?)

View File

@@ -32,9 +32,9 @@ func main() {
// 2. Prepare XMPP client
config := xmpp.Config{
Address: *address,
Jid: *jid,
Password: *password,
Address: *address,
Jid: *jid,
Credential: xmpp.Password(*password),
// StreamLogger: os.Stdout,
Insecure: true,
}

View File

@@ -0,0 +1,48 @@
/*
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{
Address: "localhost:5222",
Jid: "test@localhost",
Credential: xmpp.OAuthToken("OdAIsBlY83SLBaqQoClAn7vrZSHxixT8"),
StreamLogger: os.Stdout,
// Insecure: true,
// TLSConfig: tls.Config{InsecureSkipVerify: 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)
}

64
auth.go
View File

@@ -10,29 +10,57 @@ import (
"gosrc.io/xmpp/stanza"
)
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
// TODO: Implement other type of SASL Authentication
havePlain := false
for _, m := range f.Mechanisms.Mechanism {
if m == "PLAIN" {
havePlain = true
// Credential is used to pass the type of secret that will be used to connect to XMPP server.
// It can be either a password or an OAuth 2 bearer token.
type Credential struct {
secret string
mechanisms []string
}
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
}
}
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 authPlain(socket, decoder, user, password)
}
// Plain authentication: send base64-encoded \x00 user \x00 password
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
raw := "\x00" + user + "\x00" + password
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error {
raw := "\x00" + user + "\x00" + secret
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc)
// Next message should be either success or failure.
val, err := stanza.NextPacket(decoder)
@@ -51,3 +79,13 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
}
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
}

View File

@@ -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

View File

@@ -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 {
@@ -91,14 +111,28 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
return nil, NewConnError(err, true)
}
if config.Password == "" {
err = errors.New("missing password")
if config.Credential.secret == "" {
err = errors.New("missing credential")
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
View 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())
}
}

View File

@@ -25,7 +25,7 @@ func TestClient_Connect(t *testing.T) {
mock.Start(t, testXMPPAddress, handlerConnectSuccess)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test"), Insecure: true}
var client *Client
var err error
@@ -47,7 +47,7 @@ func TestClient_NoInsecure(t *testing.T) {
mock.Start(t, testXMPPAddress, handlerAbortTLS)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test")}
var client *Client
var err error
@@ -71,7 +71,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
mock.Start(t, testXMPPAddress, handlerAbortTLS)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"}
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test")}
var client *Client
var err error
@@ -94,7 +94,7 @@ func TestClient_RFC3921Session(t *testing.T) {
mock.Start(t, testXMPPAddress, handlerConnectWithSession)
// Test / Check result
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true}
config := Config{Address: testXMPPAddress, Jid: "test@localhost", Credential: Password("test"), Insecure: true}
var client *Client
var err error

198
cmd/fluuxmpp/README.md Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
/*
fluuxmpp: fluuxIO's xmpp comandline tool
*/
package main

34
cmd/fluuxmpp/log.go Normal file
View 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
View 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
View 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"),
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
View 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
View 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
View 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 => ./../

161
cmd/go.sum Normal file
View File

@@ -0,0 +1,161 @@
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 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/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=

View File

@@ -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.

View File

@@ -1,3 +0,0 @@
# TODO
- Use a config file to define the checks to perform as client on an XMPP server.

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -1,6 +1,7 @@
package xmpp
import (
"crypto/tls"
"io"
"os"
)
@@ -9,10 +10,13 @@ type Config struct {
Address string
Jid string
parsedJid *Jid // For easier manipulation
Password string
Credential Credential
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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
}
@@ -138,7 +168,39 @@ func (s *Session) auth(o Config) {
return
}
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
s.err = authSASL(s.streamLogger, s.decoder, 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.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) {
@@ -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
View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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
View 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)
}
}

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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
}