18 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
41 changed files with 767 additions and 343 deletions

View File

@@ -34,8 +34,8 @@ Here is an example code to configure a client to allow connecting to a server wi
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
TLSConfig: tls.Config{InsecureSkipVerify: true},
Credential: xmpp.Password("Test"),
TLSConfig: tls.Config{InsecureSkipVerify: true},
}
```
@@ -96,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

@@ -2,4 +2,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
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,7 +17,7 @@ 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},
@@ -48,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

@@ -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,8 +111,8 @@ 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)
}
@@ -128,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)
@@ -138,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) {
@@ -180,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.
@@ -193,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
}
@@ -206,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
}
@@ -222,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;
```

View File

@@ -1,11 +1,18 @@
# TODO
## Issues
## 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
### Features
- configuration
- allow unencrypted

View File

@@ -6,15 +6,11 @@ import (
"gosrc.io/xmpp"
)
func main() {
log.AddHook(&hook{})
cmd.Execute()
}
var domain = ""
var cmd = &cobra.Command{
Use: "xmpp-check <host[:port]>",
Example: "xmpp-check chat.sum7.eu:5222 --domain meckerspace.de",
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)
@@ -22,7 +18,8 @@ var cmd = &cobra.Command{
}
func init() {
cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
cmdRoot.AddCommand(cmdCheck)
cmdCheck.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
}
func runCheck(address, domain string) {

5
cmd/fluuxmpp/doc.go Normal file
View File

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

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

View File

@@ -18,9 +18,10 @@ var configFile = ""
// FIXME: Remove global variables
var isMUCRecipient = false
var cmd = &cobra.Command{
Use: "sendxmpp <recipient,> [message]",
Example: `sendxmpp to@chat.sum7.eu "Hello World!"`,
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,
}
@@ -31,9 +32,9 @@ func sendxmpp(cmd *cobra.Command, args []string) {
var err error
client, err := xmpp.NewClient(xmpp.Config{
Jid: viper.GetString("jid"),
Address: viper.GetString("addr"),
Password: viper.GetString("password"),
Jid: viper.GetString("jid"),
Address: viper.GetString("addr"),
Credential: xmpp.Password(viper.GetString("password")),
}, xmpp.NewRouter())
if err != nil {
@@ -95,28 +96,30 @@ func sendxmpp(cmd *cobra.Command, args []string) {
}
func init() {
cobra.OnInitialize(initConfig)
cmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluxxmpp.yml)")
cmdRoot.AddCommand(cmdSend)
cmd.Flags().StringP("jid", "", "", "using jid (required)")
viper.BindPFlag("jid", cmd.Flags().Lookup("jid"))
cobra.OnInitialize(initConfigFile)
cmdSend.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is ~/.config/fluuxmpp.yml)")
cmd.Flags().StringP("password", "", "", "using password for your jid (required)")
viper.BindPFlag("password", cmd.Flags().Lookup("password"))
cmdSend.Flags().StringP("jid", "", "", "using jid (required)")
viper.BindPFlag("jid", cmdSend.Flags().Lookup("jid"))
cmd.Flags().StringP("addr", "", "", "host[:port]")
viper.BindPFlag("addr", cmd.Flags().Lookup("addr"))
cmdSend.Flags().StringP("password", "", "", "using password for your jid (required)")
viper.BindPFlag("password", cmdSend.Flags().Lookup("password"))
cmd.Flags().BoolVarP(&isMUCRecipient, "muc", "m", false, "recipient is a muc (join it before sending messages)")
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 initConfig() {
func initConfigFile() {
if configFile != "" {
viper.SetConfigFile(configFile)
}
viper.SetConfigName("fluxxmpp")
viper.SetConfigName("fluuxmpp")
viper.AddConfigPath("/etc/")
viper.AddConfigPath("$HOME/.config")
viper.AddConfigPath(".")

View File

@@ -143,6 +143,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
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=

View File

@@ -1,131 +0,0 @@
# sendXMPP
sendxmpp is a tool to send messages from command-line.
## Installation
To install `sendxmpp` in your Go path:
```
$ go get -u gosrc.io/xmpp/cmd/sendxmpp
```
## Usage
```
$ sendxmpp --help
Usage:
sendxmpp <recipient,> [message] [flags]
Examples:
sendxmpp to@chat.sum7.eu "Hello World!"
Flags:
--addr string host[:port]
--config string config file (default is ~/.config/fluxxmpp.yml)
-h, --help help for sendxmpp
--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
Message from arguments:
```bash
$ sendxmpp 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 | sendxmpp 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
$ sendxmpp 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 | sendxmpp 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 `fluxxmpp` with you favorite file extenion (e.g. `toml`, `yml`).
e.g. ~/.config/fluxxmpp.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';
sendxmpp 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
sendxmpp to@example.org "Hello World!" --jid bot@example.org --password secret --addr example.com:5222;
```

View File

@@ -1,6 +0,0 @@
/*
sendxmpp is a command-line tool to send to send XMPP messages to users
*/
package main

View File

@@ -1,12 +0,0 @@
package main
import (
"github.com/bdlm/log"
)
func main() {
log.AddHook(&hook{})
if err := cmd.Execute(); err != nil {
log.Fatal(err)
}
}

View File

@@ -1,49 +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
```
$ xmpp-check --help
Usage:
xmpp-check <host[:port]> [flags]
Examples:
xmpp-check chat.sum7.eu:5222 --domain meckerspace.de
Flags:
-d, --domain string domain if host handle multiple domains
-h, --help help for xmpp-check
```
If you server is on standard port and XMPP domains matches the hostname you can simply use:
```
$ xmpp-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:
```
$ xmpp-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.

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,34 +0,0 @@
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
}

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

@@ -10,7 +10,7 @@ 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

2
go.mod
View File

@@ -4,5 +4,5 @@ go 1.12
require (
github.com/google/go-cmp v0.3.0
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
)

2
go.sum
View File

@@ -2,3 +2,5 @@ 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,14 +30,19 @@ 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, o)
if s.err != nil {
return nil, nil, NewConnError(s.err, true)
}
if !s.TlsEnabled && !o.Insecure {
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
return nil, nil, NewConnError(err, true)
@@ -50,10 +56,18 @@ func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
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
}
@@ -131,7 +145,6 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) ne
}
if !o.TLSConfig.InsecureSkipVerify {
// We check that cert matches hostname
s.err = tlsConn.VerifyHostname(domain)
}
@@ -155,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) {
@@ -205,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

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,7 +121,7 @@ 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 {
@@ -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
}