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
-26
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. 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 ## Supported specifications
### Clients ### Clients
+3 -20
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) { func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
// Higher level discovery disco := iqResp.DiscoInfo()
identity := stanza.Identity{ disco.AddIdentity(opts.Name, opts.Category, opts.Type)
Name: opts.Name, disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
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
} }
func discoInfoPubSub(iqResp *stanza.IQ) { func discoInfoPubSub(iqResp *stanza.IQ) {
+6 -31
View File
@@ -1,7 +1,6 @@
package main package main
import ( import (
"encoding/xml"
"fmt" "fmt"
"log" "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"}) iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
identity := stanza.Identity{ disco := iqResp.DiscoInfo()
Name: opts.Name, disco.AddIdentity(opts.Name, opts.Category, opts.Type)
Category: opts.Category, disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
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
_ = c.Send(iqResp) _ = 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"}) 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 == "" { if discoItems.Node == "" {
payload = stanza.DiscoItems{ items.AddItem("service.localhost", "node1", "test node")
Items: []stanza.DiscoItem{
{Name: "test node", JID: "service.localhost", Node: "node1"},
},
} }
}
iqResp.Payload = &payload
_ = c.Send(iqResp) _ = 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"}) iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
var payload stanza.Version iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "")
payload.Name = "Fluux XMPP Component"
payload.Version = "0.0.1"
iq.Payload = &payload
_ = c.Send(iqResp) _ = c.Send(iqResp)
} }
-1
View File
@@ -20,7 +20,6 @@ func main() {
Password: "test", Password: "test",
StreamLogger: os.Stdout, StreamLogger: os.Stdout,
Insecure: true, Insecure: true,
// TLSConfig: tls.Config{InsecureSkipVerify: true},
} }
router := xmpp.NewRouter() router := xmpp.NewRouter()
+2 -3
View File
@@ -86,9 +86,8 @@ func (c *ServerCheck) Check() error {
return fmt.Errorf("expecting starttls proceed: %s", err) return fmt.Errorf("expecting starttls proceed: %s", err)
} }
var tlsConfig tls.Config stanza.DefaultTlsConfig.ServerName = c.domain
tlsConfig.ServerName = c.domain tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
tlsConn := tls.Client(tcpconn, &tlsConfig)
// We convert existing connection to TLS // We convert existing connection to TLS
if err = tlsConn.Handshake(); err != nil { if err = tlsConn.Handshake(); err != nil {
return err return err
-2
View File
@@ -1,7 +1,6 @@
package xmpp package xmpp
import ( import (
"crypto/tls"
"io" "io"
"os" "os"
) )
@@ -14,7 +13,6 @@ type Config struct {
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
TLSConfig tls.Config
// Insecure can be set to true to allow to open a session without TLS. If TLS // 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. // is supported on the server, we will still try to use it.
Insecure bool Insecure bool
+10 -21
View File
@@ -35,17 +35,15 @@ func NewSession(conn net.Conn, o Config) (net.Conn, *Session, error) {
// starttls // starttls
var tlsConn net.Conn var tlsConn net.Conn
tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain, o) tlsConn = s.startTlsIfSupported(conn, o.parsedJid.Domain)
if !s.TlsEnabled && !o.Insecure {
err := fmt.Errorf("failed to negotiate TLS session : %s", s.err)
return nil, nil, NewConnError(err, true)
}
if s.TlsEnabled { if s.TlsEnabled {
s.reset(conn, tlsConn, o) s.reset(conn, tlsConn, o)
} }
if !s.TlsEnabled && !o.Insecure {
return nil, nil, NewConnError(errors.New("failed to negotiate TLS session"), true)
}
// auth // auth
s.auth(o) s.auth(o)
s.reset(tlsConn, tlsConn, o) s.reset(tlsConn, tlsConn, o)
@@ -103,7 +101,7 @@ func (s *Session) open(domain string) (f stanza.StreamFeatures) {
return 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 { if s.err != nil {
return conn 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()) s.err = errors.New("expecting starttls proceed: " + s.err.Error())
return conn return conn
} }
s.TlsEnabled = true
o.TLSConfig.ServerName = domain // TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
tlsConn := tls.Client(conn, &o.TLSConfig) stanza.DefaultTlsConfig.ServerName = domain
tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig)
// We convert existing connection to TLS // We convert existing connection to TLS
if s.err = tlsConn.Handshake(); s.err != nil { if s.err = tlsConn.Handshake(); s.err != nil {
return tlsConn return tlsConn
} }
if !o.TLSConfig.InsecureSkipVerify {
// We check that cert matches hostname // We check that cert matches hostname
s.err = tlsConn.VerifyHostname(domain) s.err = tlsConn.VerifyHostname(domain)
}
if s.err == nil {
s.TlsEnabled = true
}
return tlsConn 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: // starttls is not supported => we do not upgrade the connection:
return conn return conn
} }
+31 -2
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 d.Node = node
return d
} }
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo { 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 { func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
d.Features = []Feature{}
for _, ns := range namespace { for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns}) d.Features = append(d.Features, Feature{Var: ns})
} }
@@ -104,11 +106,38 @@ func (d *DiscoItems) Namespace() string {
return d.XMLName.Space 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 { type DiscoItem struct {
XMLName xml.Name `xml:"item"` XMLName xml.Name `xml:"item"`
Name string `xml:"name,attr,omitempty"`
JID string `xml:"jid,attr,omitempty"` JID string `xml:"jid,attr,omitempty"`
Node string `xml:"node,attr,omitempty"` Node string `xml:"node,attr,omitempty"`
Name string `xml:"name,attr,omitempty"`
} }
// ============================================================================ // ============================================================================
+46 -11
View File
@@ -7,29 +7,22 @@ import (
"gosrc.io/xmpp/stanza" "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"}) iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
disco := iq.DiscoInfo() disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service") disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1") disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
// Marshall parsedIQ, err := checkMarshalling(t, iq)
data, err := xml.Marshal(iq)
if err != nil { if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
return 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 // Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo) pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
if !ok { if !ok {
t.Errorf("Parsed stanza does not contain an IQ payload") t.Errorf("Parsed stanza does not contain correct IQ payload")
} }
// Check features // 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)
}
}
}
}
+20
View File
@@ -17,6 +17,26 @@ func (v *Version) Namespace() string {
return v.XMLName.Space 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 // Registry init
+40
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)
}
}
+3
View File
@@ -1,9 +1,12 @@
package stanza package stanza
import ( import (
"crypto/tls"
"encoding/xml" "encoding/xml"
) )
var DefaultTlsConfig tls.Config
// Used during stream initiation / session establishment // Used during stream initiation / session establishment
type TLSProceed struct { type TLSProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"` XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
+25
View File
@@ -2,10 +2,35 @@ package stanza_test
import ( import (
"encoding/xml" "encoding/xml"
"testing"
"github.com/google/go-cmp/cmp" "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 // 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 // marshal / unmarshal. There is no need to manage them on the manually
// crafted structure. // crafted structure.
+1 -1
View File
@@ -119,7 +119,7 @@ func (sm *StreamManager) connect() error {
var actualErr ConnError var actualErr ConnError
if xerrors.As(err, &actualErr) { if xerrors.As(err, &actualErr) {
if actualErr.Permanent { if actualErr.Permanent {
return xerrors.Errorf("unrecoverable connect error %#v", actualErr) return xerrors.Errorf("unrecoverable connect error %w", actualErr)
} }
} }
backoff.wait() backoff.wait()