3 Commits

Author SHA1 Message Date
Mickael Remond
1c4dd6c967 Remove debug print-out 2019-07-31 18:54:49 +02:00
Mickael Remond
1b3dec3902 Clean-up: remove test/debug code 2019-07-31 18:51:16 +02:00
Mickael Remond
3f48672946 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-07-31 18:47:30 +02:00
39 changed files with 329 additions and 512 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{ config := xmpp.Config{
Address: "localhost:5222", Address: "localhost:5222",
Jid: "test@localhost", Jid: "test@localhost",
Credential: xmpp.Password("Test"), Password: "test",
TLSConfig: tls.Config{InsecureSkipVerify: true}, TLSConfig: tls.Config{InsecureSkipVerify: true},
} }
``` ```
@@ -96,7 +96,7 @@ func main() {
config := xmpp.Config{ config := xmpp.Config{
Address: "localhost:5222", Address: "localhost:5222",
Jid: "test@localhost", Jid: "test@localhost",
Credential: xmpp.Password("Test"), Password: "test",
StreamLogger: os.Stdout, StreamLogger: os.Stdout,
Insecure: true, Insecure: true,
} }

View File

@@ -4,5 +4,3 @@ github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

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

View File

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

View File

@@ -1,48 +0,0 @@
/*
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,57 +10,29 @@ import (
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
// Credential is used to pass the type of secret that will be used to connect to XMPP server. func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
// It can be either a password or an OAuth 2 bearer token. // TODO: Implement other type of SASL Authentication
type Credential struct { havePlain := false
secret string for _, m := range f.Mechanisms.Mechanism {
mechanisms []string if m == "PLAIN" {
} havePlain = true
func Password(pwd string) Credential {
credential := Credential{
secret: pwd,
mechanisms: []string{"PLAIN"},
}
return credential
}
func OAuthToken(token string) Credential {
credential := Credential{
secret: token,
mechanisms: []string{"X-OAUTH2"},
}
return credential
}
// ============================================================================
// Authentication flow for SASL mechanisms
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, credential Credential) (err error) {
var matchingMech string
for _, mech := range credential.mechanisms {
if isSupportedMech(mech, f.Mechanisms.Mechanism) {
matchingMech = mech
break break
} }
} }
if !havePlain {
switch matchingMech { err := fmt.Errorf("PLAIN authentication is not supported by server: %v", f.Mechanisms.Mechanism)
case "PLAIN", "X-OAUTH2":
// TODO: Implement other type of SASL mechanisms
return authPlain(socket, decoder, matchingMech, user, credential.secret)
default:
err := fmt.Errorf("no matching authentication (%v) supported by server: %v", credential.mechanisms, f.Mechanisms.Mechanism)
return NewConnError(err, true) return NewConnError(err, true)
} }
return authPlain(socket, decoder, user, password)
} }
// Plain authentication: send base64-encoded \x00 user \x00 password // Plain authentication: send base64-encoded \x00 user \x00 password
func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user string, secret string) error { func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password string) error {
raw := "\x00" + user + "\x00" + secret raw := "\x00" + user + "\x00" + password
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw)) base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc) fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
// Next message should be either success or failure. // Next message should be either success or failure.
val, err := stanza.NextPacket(decoder) val, err := stanza.NextPacket(decoder)
@@ -79,13 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str
} }
return err return err
} }
// isSupportedMech returns true if the mechanism is supported in the provided list.
func isSupportedMech(mech string, mechanisms []string) bool {
for _, m := range mechanisms {
if mech == m {
return true
}
}
return false
}

View File

@@ -4,7 +4,6 @@ import (
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"time" "time"
@@ -111,8 +110,8 @@ func NewClient(config Config, r *Router) (c *Client, err error) {
return nil, NewConnError(err, true) return nil, NewConnError(err, true)
} }
if config.Credential.secret == "" { if config.Password == "" {
err = errors.New("missing credential") err = errors.New("missing password")
return nil, NewConnError(err, true) return nil, NewConnError(err, true)
} }
@@ -190,10 +189,7 @@ func (c *Client) Resume(state SMState) error {
func (c *Client) Disconnect() { func (c *Client) Disconnect() {
_ = c.SendRaw("</stream:stream>") _ = c.SendRaw("</stream:stream>")
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect // TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
conn := c.conn _ = c.conn.Close()
if conn != nil {
_ = conn.Close()
}
} }
func (c *Client) SetHandler(handler EventHandler) { func (c *Client) SetHandler(handler EventHandler) {
@@ -212,7 +208,7 @@ func (c *Client) Send(packet stanza.Packet) error {
return errors.New("cannot marshal packet " + err.Error()) return errors.New("cannot marshal packet " + err.Error())
} }
return c.sendWithWriter(c.Session.streamLogger, data) return c.sendWithLogger(string(data))
} }
// SendRaw sends an XMPP stanza as a string to the server. // SendRaw sends an XMPP stanza as a string to the server.
@@ -225,12 +221,12 @@ func (c *Client) SendRaw(packet string) error {
return errors.New("client is not connected") return errors.New("client is not connected")
} }
return c.sendWithWriter(c.Session.streamLogger, []byte(packet)) return c.sendWithLogger(packet)
} }
func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { func (c *Client) sendWithLogger(packet string) error {
var err error var err error
_, err = writer.Write(packet) _, err = fmt.Fprintf(c.Session.streamLogger, packet)
return err return err
} }

View File

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

View File

@@ -1,198 +0,0 @@
# 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,5 +0,0 @@
/*
fluuxmpp: fluuxIO's xmpp comandline tool
*/
package main

View File

@@ -1,19 +0,0 @@
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

@@ -143,8 +143,6 @@ 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/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 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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/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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

131
cmd/sendxmpp/README.md Normal file
View File

@@ -0,0 +1,131 @@
# 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,18 +1,11 @@
# TODO # TODO
## check ## Issues
### Features
- Use a config file to define the checks to perform as client on an XMPP server.
## send
### Issues
- Remove global variable (like mucToleave) - 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). - Does not report error when trying to connect to a non open port (for example localhost with no server running).
### Features ## Features
- configuration - configuration
- allow unencrypted - allow unencrypted

View File

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

6
cmd/sendxmpp/doc.go Normal file
View File

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

12
cmd/sendxmpp/main.go Normal file
View File

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

49
cmd/xmpp-check/README.md Normal file
View File

@@ -0,0 +1,49 @@
# 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.

3
cmd/xmpp-check/TODO.md Normal file
View File

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

6
cmd/xmpp-check/doc.go Normal file
View File

@@ -0,0 +1,6 @@
/*
xmpp-check is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires.
*/
package main

34
cmd/xmpp-check/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
}

View File

@@ -6,11 +6,15 @@ import (
"gosrc.io/xmpp" "gosrc.io/xmpp"
) )
func main() {
log.AddHook(&hook{})
cmd.Execute()
}
var domain = "" var domain = ""
var cmdCheck = &cobra.Command{ var cmd = &cobra.Command{
Use: "check <host[:port]>", Use: "xmpp-check <host[:port]>",
Short: "is a command-line to check if you XMPP TLS certificate is valid and warn you before it expires", Example: "xmpp-check chat.sum7.eu:5222 --domain meckerspace.de",
Example: "fluuxmpp check chat.sum7.eu:5222 --domain meckerspace.de",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
runCheck(args[0], domain) runCheck(args[0], domain)
@@ -18,8 +22,7 @@ var cmdCheck = &cobra.Command{
} }
func init() { func init() {
cmdRoot.AddCommand(cmdCheck) cmd.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
cmdCheck.Flags().StringVarP(&domain, "domain", "d", "", "domain if host handle multiple domains")
} }
func runCheck(address, domain string) { func runCheck(address, domain string) {

View File

@@ -66,67 +66,56 @@ func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
// Connect triggers component connection to XMPP server component port. // Connect triggers component connection to XMPP server component port.
// TODO: Failed handshake should be a permanent error // TODO: Failed handshake should be a permanent error
func (c *Component) Connect() error { func (c *Component) Connect() error {
var state SMState
return c.Resume(state)
}
func (c *Component) Resume(sm SMState) error {
var conn net.Conn var conn net.Conn
var err error var err error
if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil { if conn, err = net.DialTimeout("tcp", c.Address, time.Duration(5)*time.Second); err != nil {
return err return err
} }
c.conn = conn c.conn = conn
c.updateState(StateConnected)
// 1. Send stream open tag // 1. Send stream open tag
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil { if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
c.updateState(StateStreamError) return errors.New("cannot send stream open " + err.Error())
return NewConnError(errors.New("cannot send stream open "+err.Error()), false)
} }
c.decoder = xml.NewDecoder(conn) c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply // 2. Initialize xml decoder and extract streamID from reply
streamId, err := stanza.InitStream(c.decoder) streamId, err := stanza.InitStream(c.decoder)
if err != nil { if err != nil {
c.updateState(StateStreamError) return errors.New("cannot init decoder " + err.Error())
return NewConnError(errors.New("cannot init decoder "+err.Error()), false)
} }
// 3. Authentication // 3. Authentication
if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil { if _, err := fmt.Fprintf(conn, "<handshake>%s</handshake>", c.handshake(streamId)); err != nil {
c.updateState(StateStreamError) return errors.New("cannot send handshake " + err.Error())
return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
} }
// 4. Check server response for authentication // 4. Check server response for authentication
val, err := stanza.NextPacket(c.decoder) val, err := stanza.NextPacket(c.decoder)
if err != nil { if err != nil {
c.updateState(StateDisconnected) return err
return NewConnError(err, true)
} }
switch v := val.(type) { switch v := val.(type) {
case stanza.StreamError: case stanza.StreamError:
c.streamError("conflict", "no auth loop") return errors.New("handshake failed " + v.Error.Local)
return NewConnError(errors.New("handshake failed "+v.Error.Local), true)
case stanza.Handshake: case stanza.Handshake:
// Start the receiver go routine // Start the receiver go routine
c.updateState(StateSessionEstablished)
go c.recv() go c.recv()
return nil return nil
default: default:
c.updateState(StateStreamError) return errors.New("expecting handshake result, got " + v.Name())
return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
} }
} }
func (c *Component) Resume() error {
return errors.New("components do not support stream management")
}
func (c *Component) Disconnect() { func (c *Component) Disconnect() {
_ = c.SendRaw("</stream:stream>") _ = c.SendRaw("</stream:stream>")
// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect // TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
conn := c.conn _ = c.conn.Close()
if conn != nil {
_ = conn.Close()
}
} }
func (c *Component) SetHandler(handler EventHandler) { func (c *Component) SetHandler(handler EventHandler) {

View File

@@ -23,10 +23,3 @@ func TestHandshake(t *testing.T) {
func TestGenerateHandshake(t *testing.T) { func TestGenerateHandshake(t *testing.T) {
// TODO // 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 Address string
Jid string Jid string
parsedJid *Jid // For easier manipulation parsedJid *Jid // For easier manipulation
Credential Credential Password string
StreamLogger *os.File // Used for debugging StreamLogger *os.File // Used for debugging
Lang string // TODO: should default to 'en' Lang string // TODO: should default to 'en'
ConnectTimeout int // Client timeout in seconds. Default to 15 ConnectTimeout int // Client timeout in seconds. Default to 15

2
go.mod
View File

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

2
go.sum
View File

@@ -2,5 +2,3 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.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 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -98,7 +98,7 @@ type Handler interface {
type Route struct { type Route struct {
handler Handler handler Handler
// Matchers are used to "specialize" routes and focus on specific packet features // Matchers are used to "specialize" routes and focus on specific packet features
matchers []Matcher matchers []matcher
} }
func (r *Route) Handler(handler Handler) *Route { func (r *Route) Handler(handler Handler) *Route {
@@ -122,8 +122,8 @@ func (r *Route) HandlerFunc(f HandlerFunc) *Route {
return r.Handler(f) return r.Handler(f)
} }
// AddMatcher adds a matcher to the route // addMatcher adds a matcher to the route
func (r *Route) AddMatcher(m Matcher) *Route { func (r *Route) addMatcher(m matcher) *Route {
r.matchers = append(r.matchers, m) r.matchers = append(r.matchers, m)
return r return r
} }
@@ -170,7 +170,7 @@ func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
// It matches on the Local part of the xml.Name // It matches on the Local part of the xml.Name
func (r *Route) Packet(name string) *Route { func (r *Route) Packet(name string) *Route {
name = strings.ToLower(name) name = strings.ToLower(name)
return r.AddMatcher(nameMatcher(name)) return r.addMatcher(nameMatcher(name))
} }
// ------------------------- // -------------------------
@@ -204,7 +204,7 @@ func (r *Route) StanzaType(types ...string) *Route {
for k, v := range types { for k, v := range types {
types[k] = strings.ToLower(v) types[k] = strings.ToLower(v)
} }
return r.AddMatcher(nsTypeMatcher(types)) return r.addMatcher(nsTypeMatcher(types))
} }
// ------------------------- // -------------------------
@@ -229,15 +229,14 @@ func (r *Route) IQNamespaces(namespaces ...string) *Route {
for k, v := range namespaces { for k, v := range namespaces {
namespaces[k] = strings.ToLower(v) namespaces[k] = strings.ToLower(v)
} }
return r.AddMatcher(nsIQMatcher(namespaces)) return r.addMatcher(nsIQMatcher(namespaces))
} }
// ============================================================================ // ============================================================================
// Matchers // Matchers
// Matchers are used to "specialize" routes and focus on specific packet features. // Matchers are used to "specialize" routes and focus on specific packet features
// You can register attach them to a route via the AddMatcher method. type matcher interface {
type Matcher interface {
Match(stanza.Packet, *RouteMatch) bool Match(stanza.Packet, *RouteMatch) bool
} }

View File

@@ -168,7 +168,7 @@ func (s *Session) auth(o Config) {
return return
} }
s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Credential) s.err = authSASL(s.streamLogger, s.decoder, s.Features, o.parsedJid.Node, o.Password)
} }
// Attempt to resume session using stream management // Attempt to resume session using stream management

View File

@@ -1,31 +0,0 @@
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 { if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
t.Errorf("Payload is not an iot control set: %v", cs) t.Errorf("Paylod is not an iot control set: %v", cs)
} }
} }

View File

@@ -22,7 +22,7 @@ type IQ struct { // Info/Query
// request." // request."
Payload IQPayload `xml:",omitempty"` Payload IQPayload `xml:",omitempty"`
Error Err `xml:"error,omitempty"` Error Err `xml:"error,omitempty"`
// Any is used to decode unknown payload as a generic structure // Any is used to decode unknown payload as a generique structure
Any *Node `xml:",any"` Any *Node `xml:",any"`
} }

View File

@@ -10,7 +10,7 @@ import "encoding/xml"
type Node struct { type Node struct {
XMLName xml.Name XMLName xml.Name
Attrs []xml.Attr `xml:"-"` Attrs []xml.Attr `xml:"-"`
Content string `xml:",cdata"` Content string `xml:",innerxml"`
Nodes []Node `xml:",any"` Nodes []Node `xml:",any"`
} }
@@ -47,8 +47,5 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
err = e.EncodeToken(start) err = e.EncodeToken(start)
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName}) 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}) return e.EncodeToken(xml.EndElement{Name: start.Name})
} }

View File

@@ -1,30 +0,0 @@
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

@@ -152,7 +152,7 @@ type Metrics struct {
ConnectTime time.Duration ConnectTime time.Duration
// LoginTime returns the between client initiation of the TCP/IP // LoginTime returns the between client initiation of the TCP/IP
// connection to the server and the return of the login result. // connection to the server and the return of the login result.
// This includes ConnectTime, but also XMPP level protocol negotiation // This includes ConnectTime, but also XMPP level protocol negociation
// like starttls. // like starttls.
LoginTime time.Duration LoginTime time.Duration
} }