5 Commits

Author SHA1 Message Date
Mickael Remond
5c76c9c0a0 Simplify disco with builder helpers 2019-07-09 18:09:23 +02:00
Mickael Remond
0b0c0fa5ab Simplify disco and software version
Make use of helpers.
2019-07-09 18:05:26 +02:00
Mickael Remond
6872ed8d1b Add builder & test on software version helpers 2019-07-09 17:53:49 +02:00
Mickael Remond
0e3101c2be Expand comments 2019-07-09 17:06:57 +02:00
Mickael Remond
026e5e6fe1 Add helpers for IQ DiscoItems 2019-07-09 16:59:54 +02:00
14 changed files with 189 additions and 120 deletions

View File

@@ -13,32 +13,6 @@ The goal is to make simple to write simple XMPP clients and components:
The library is designed to have minimal dependencies. For now, the library does not depend on any other library.
## Configuration and connection
### Allowing Insecure TLS connection during development
It is not recommended to disable the check for domain name and certificate chain. Doing so would open your client
to man-in-the-middle attacks.
However, in development, XMPP servers often use self-signed certificates. In that situation, it is better to add the
root CA that signed the certificate to your trusted list of root CA. It avoids changing the code and limit the risk
of shipping an insecure client to production.
That said, if you really want to allow your client to trust any TLS certificate, you can customize Go standard
`tls.Config` and set it in Config struct.
Here is an example code to configure a client to allow connecting to a server with self-signed certificate. Note the
`InsecureSkipVerify` option. When using this `tls.Config` option, all the checks on the certificate are skipped.
```go
config := xmpp.Config{
Address: "localhost:5222",
Jid: "test@localhost",
Password: "test",
TLSConfig: tls.Config{InsecureSkipVerify: true},
}
```
## Supported specifications
### Clients

View File

@@ -99,26 +99,9 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
}
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
// Higher level discovery
identity := stanza.Identity{
Name: opts.Name,
Category: opts.Category,
Type: opts.Type,
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
},
}
iqResp.Payload = &payload
disco := iqResp.DiscoInfo()
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
}
func discoInfoPubSub(iqResp *stanza.IQ) {

View File

@@ -1,7 +1,6 @@
package main
import (
"encoding/xml"
"fmt"
"log"
@@ -61,25 +60,9 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
identity := stanza.Identity{
Name: opts.Name,
Category: opts.Category,
Type: opts.Type,
}
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
},
}
iqResp.Payload = &payload
disco := iqResp.DiscoInfo()
disco.AddIdentity(opts.Name, opts.Category, opts.Type)
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
_ = c.Send(iqResp)
}
@@ -97,16 +80,11 @@ func discoItems(c xmpp.Sender, p stanza.Packet) {
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
items := iqResp.DiscoItems()
var payload stanza.DiscoItems
if discoItems.Node == "" {
payload = stanza.DiscoItems{
Items: []stanza.DiscoItem{
{Name: "test node", JID: "service.localhost", Node: "node1"},
},
}
items.AddItem("service.localhost", "node1", "test node")
}
iqResp.Payload = &payload
_ = c.Send(iqResp)
}
@@ -118,9 +96,6 @@ func handleVersion(c xmpp.Sender, p stanza.Packet) {
}
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
var payload stanza.Version
payload.Name = "Fluux XMPP Component"
payload.Version = "0.0.1"
iq.Payload = &payload
iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
_ = c.Send(iqResp)
}

View File

@@ -20,7 +20,6 @@ func main() {
Password: "test",
StreamLogger: os.Stdout,
Insecure: true,
// TLSConfig: tls.Config{InsecureSkipVerify: true},
}
router := xmpp.NewRouter()

View File

@@ -86,9 +86,8 @@ func (c *ServerCheck) Check() error {
return fmt.Errorf("expecting starttls proceed: %s", err)
}
var tlsConfig tls.Config
tlsConfig.ServerName = c.domain
tlsConn := tls.Client(tcpconn, &tlsConfig)
stanza.DefaultTlsConfig.ServerName = c.domain
tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
// We convert existing connection to TLS
if err = tlsConn.Handshake(); err != nil {
return err

View File

@@ -1,7 +1,6 @@
package xmpp
import (
"crypto/tls"
"io"
"os"
)
@@ -14,7 +13,6 @@ type Config struct {
StreamLogger *os.File // Used for debugging
Lang string // TODO: should default to 'en'
ConnectTimeout int // Client timeout in seconds. Default to 15
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

View File

@@ -35,17 +35,15 @@ func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
// starttls
var tlsConn net.Conn
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o)
if !s.TlsEnabled && !o.Insecure {
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
return nil, nil, NewConnError(err, true)
}
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain)
if s.TlsEnabled {
s.reset(conn, tlsConn, o)
}
if !s.TlsEnabled && !o.Insecure {
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
}
// auth
s.auth(o)
s.reset(tlsConn, tlsConn, o)
@@ -103,7 +101,7 @@ func (s *Session) open(domain string) (f stanza.StreamFeatures) {
return
}
func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) net.Conn {
func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
if s.err != nil {
return conn
}
@@ -116,30 +114,21 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string, o Config) ne
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
return conn
}
s.TlsEnabled = true
o.TLSConfig.ServerName = domain
tlsConn := tls.Client(conn, &o.TLSConfig)
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
stanza.DefaultTlsConfig.ServerName = domain
tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig)
// We convert existing connection to TLS
if s.err = tlsConn.Handshake(); s.err != nil {
return tlsConn
}
if !o.TLSConfig.InsecureSkipVerify {
// We check that cert matches hostname
s.err = tlsConn.VerifyHostname(domain)
}
if s.err == nil {
s.TlsEnabled = true
}
// We check that cert matches hostname
s.err = tlsConn.VerifyHostname(domain)
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
}

View File

@@ -56,8 +56,9 @@ func (d *DiscoInfo) AddFeatures(namespace ...string) {
}
}
func (d *DiscoInfo) SetNode(node string) {
func (d *DiscoInfo) SetNode(node string) *DiscoInfo {
d.Node = node
return d
}
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
@@ -66,6 +67,7 @@ func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
}
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
d.Features = []Feature{}
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
@@ -104,11 +106,38 @@ func (d *DiscoItems) Namespace() string {
return d.XMLName.Space
}
// ---------------
// Builder helpers
// DiscoItems builds a default DiscoItems payload
func (iq *IQ) DiscoItems() *DiscoItems {
d := DiscoItems{
XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"},
}
iq.Payload = &d
return &d
}
func (d *DiscoItems) SetNode(node string) *DiscoItems {
d.Node = node
return d
}
func (d *DiscoItems) AddItem(jid, node, name string) *DiscoItems {
item := DiscoItem{
JID: jid,
Node: node,
Name: name,
}
d.Items = append(d.Items, item)
return d
}
type DiscoItem struct {
XMLName xml.Name `xml:"item"`
Name string `xml:"name,attr,omitempty"`
JID string `xml:"jid,attr,omitempty"`
Node string `xml:"node,attr,omitempty"`
Name string `xml:"name,attr,omitempty"`
}
// ============================================================================

View File

@@ -7,29 +7,22 @@ import (
"gosrc.io/xmpp/stanza"
)
func TestDiscoInfoBuilder(t *testing.T) {
// Test DiscoInfo Builder with several features
func TestDiscoInfo_Builder(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
// Marshall
data, err := xml.Marshal(iq)
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
return
}
// Unmarshall
var parsedIQ stanza.IQ
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
if !ok {
t.Errorf("Parsed stanza does not contain an IQ payload")
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check features
@@ -53,3 +46,45 @@ func TestDiscoInfoBuilder(t *testing.T) {
}
}
}
// Implements XEP-0030 example 17
// https://xmpp.org/extensions/xep-0030.html#example-17
func TestDiscoItems_Builder(t *testing.T) {
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit",
To: "romeo@montague.net/orchard", Id: "items-2"})
iq.DiscoItems().
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
AddItem("catalog.shakespeare.lit", "music", "Music from the time of Shakespeare")
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoItems)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check items
items := []stanza.DiscoItem{{xml.Name{}, "catalog.shakespeare.lit", "books", "Books by and about Shakespeare"},
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
if len(pp.Items) != len(items) {
t.Errorf("Items length mismatch: %#v", pp.Items)
} else {
for i, item := range pp.Items {
if item.JID != items[i].JID {
t.Errorf("JID Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
if item.Node != items[i].Node {
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
if item.Name != items[i].Name {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
}
}
}

View File

@@ -17,6 +17,26 @@ func (v *Version) Namespace() string {
return v.XMLName.Space
}
// ---------------
// Builder helpers
// Version builds a default software version payload
func (iq *IQ) Version() *Version {
d := Version{
XMLName: xml.Name{Space: "jabber:iq:version", Local: "query"},
}
iq.Payload = &d
return &d
}
// Set all software version info
func (v *Version) SetInfo(name, version, os string) *Version {
v.Name = name
v.Version = version
v.OS = os
return v
}
// ============================================================================
// Registry init

40
stanza/iq_version_test.go Normal file
View File

@@ -0,0 +1,40 @@
package stanza_test
import (
"testing"
"gosrc.io/xmpp/stanza"
)
// Build a Software Version reply
// https://xmpp.org/extensions/xep-0092.html#example-2
func TestVersion_Builder(t *testing.T) {
name := "Exodus"
version := "0.7.0.4"
os := "Windows-XP 5.01.2600"
iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
To: "juliet@capulet.com/balcony", Id: "version_1"})
iq.Version().SetInfo(name, version, os)
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.Version)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check version info
if pp.Name != name {
t.Errorf("Name Mismatch (expected: %s): %s", name, pp.Name)
}
if pp.Version != version {
t.Errorf("Version Mismatch (expected: %s): %s", version, pp.Version)
}
if pp.OS != os {
t.Errorf("OS Mismatch (expected: %s): %s", os, pp.OS)
}
}

View File

@@ -1,9 +1,12 @@
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"`

View File

@@ -2,10 +2,35 @@ package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
// ============================================================================
// Marshaller / unmarshaller test
func checkMarshalling(t *testing.T, iq stanza.IQ) (*stanza.IQ, error) {
// Marshall
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
return nil, err
}
// Unmarshall
var parsedIQ stanza.IQ
err = xml.Unmarshal(data, &parsedIQ)
if err != nil {
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
}
return &parsedIQ, err
}
// ============================================================================
// XML structs comparison
// Compare iq structure but ignore empty namespace as they are set properly on
// marshal / unmarshal. There is no need to manage them on the manually
// crafted structure.

View File

@@ -119,7 +119,7 @@ func (sm *StreamManager) connect() error {
var actualErr ConnError
if xerrors.As(err, &actualErr) {
if actualErr.Permanent {
return xerrors.Errorf("unrecoverable connect error %#v", actualErr)
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
}
}
backoff.wait()