Merge pull request #79: Stanza package & pattern to help building stanzas

- Move parsing and stanza marshalling / unmarshalling to stanza package
- Add pattern & basic helpers to simplify stanza building.
This was requested on #61
This commit is contained in:
Mickaël Rémond 2019-06-27 14:57:26 +02:00 committed by GitHub
commit 0fd1bb2483
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1065 additions and 798 deletions

View file

@ -32,6 +32,7 @@ import (
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
@ -57,15 +58,15 @@ func main() {
log.Fatal(cm.Run())
}
func handleMessage(s xmpp.Sender, p xmpp.Packet) {
msg, ok := p.(xmpp.Message)
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)
reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body}
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}
```

View file

@ -6,6 +6,7 @@ import (
"log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
@ -23,8 +24,8 @@ func main() {
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
router.NewRoute().
IQNamespaces(xmpp.NSDiscoInfo).
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
IQNamespaces(stanza.NSDiscoInfo).
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
discoInfo(s, p, opts)
})
router.NewRoute().
@ -43,14 +44,14 @@ func main() {
log.Fatal(cm.Run())
}
func handleMessage(_ xmpp.Sender, p xmpp.Packet) {
msg, ok := p.(xmpp.Message)
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
var msgProcessed bool
for _, ext := range msg.Extensions {
delegation, ok := ext.(*xmpp.Delegation)
delegation, ok := ext.(*stanza.Delegation)
if ok {
msgProcessed = true
fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace)
@ -72,18 +73,18 @@ const (
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
// ctx.Stream.Send / SendRaw
// ctx.Opts
func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
// Type conversion & sanity checks
iq, ok := p.(xmpp.IQ)
iq, ok := p.(stanza.IQ)
if !ok {
return
}
info, ok := iq.Payload.(*xmpp.DiscoInfo)
info, ok := iq.Payload.(*stanza.DiscoInfo)
if !ok {
return
}
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
switch info.Node {
case "":
@ -97,22 +98,22 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
_ = c.Send(iqResp)
}
func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
// Higher level discovery
identity := xmpp.Identity{
identity := stanza.Identity{
Name: opts.Name,
Category: opts.Category,
Type: opts.Type,
}
payload := xmpp.DiscoInfo{
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: xmpp.NSDiscoInfo,
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: identity,
Features: []xmpp.Feature{
{Var: xmpp.NSDiscoInfo},
{Var: xmpp.NSDiscoItems},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
},
@ -120,14 +121,14 @@ func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
iqResp.Payload = &payload
}
func discoInfoPubSub(iqResp *xmpp.IQ) {
payload := xmpp.DiscoInfo{
func discoInfoPubSub(iqResp *stanza.IQ) {
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: xmpp.NSDiscoInfo,
Space: stanza.NSDiscoInfo,
Local: "query",
},
Node: pubsubNode,
Features: []xmpp.Feature{
Features: []stanza.Feature{
{Var: "http://jabber.org/protocol/pubsub"},
{Var: "http://jabber.org/protocol/pubsub#publish"},
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
@ -137,19 +138,19 @@ func discoInfoPubSub(iqResp *xmpp.IQ) {
iqResp.Payload = &payload
}
func discoInfoPEP(iqResp *xmpp.IQ) {
identity := xmpp.Identity{
func discoInfoPEP(iqResp *stanza.IQ) {
identity := stanza.Identity{
Category: "pubsub",
Type: "pep",
}
payload := xmpp.DiscoInfo{
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: xmpp.NSDiscoInfo,
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: identity,
Identity: []stanza.Identity{identity},
Node: pepNode,
Features: []xmpp.Feature{
Features: []stanza.Feature{
{Var: "http://jabber.org/protocol/pubsub#access-presence"},
{Var: "http://jabber.org/protocol/pubsub#auto-create"},
{Var: "http://jabber.org/protocol/pubsub#auto-subscribe"},
@ -166,25 +167,25 @@ func discoInfoPEP(iqResp *xmpp.IQ) {
iqResp.Payload = &payload
}
func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
// Type conversion & sanity checks
iq, ok := p.(xmpp.IQ)
iq, ok := p.(stanza.IQ)
if !ok {
return
}
delegation, ok := iq.Payload.(*xmpp.Delegation)
delegation, ok := iq.Payload.(*stanza.Delegation)
if !ok {
return
}
forwardedPacket := delegation.Forwarded.Stanza
fmt.Println(forwardedPacket)
forwardedIQ, ok := forwardedPacket.(xmpp.IQ)
forwardedIQ, ok := forwardedPacket.(stanza.IQ)
if !ok {
return
}
pubsub, ok := forwardedIQ.Payload.(*xmpp.PubSub)
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub)
if !ok {
// We only support pubsub delegation
return
@ -192,8 +193,8 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
if pubsub.Publish.XMLName.Local == "publish" {
// Prepare pubsub IQ reply
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
payload := xmpp.PubSub{
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
payload := stanza.PubSub{
XMLName: xml.Name{
Space: "http://jabber.org/protocol/pubsub",
Local: "pubsub",
@ -201,13 +202,13 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
}
iqResp.Payload = &payload
// Wrap the reply in delegation 'forward'
iqForward := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
delegPayload := xmpp.Delegation{
iqForward := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
delegPayload := stanza.Delegation{
XMLName: xml.Name{
Space: "urn:xmpp:delegation:1",
Local: "delegation",
},
Forwarded: &xmpp.Forwarded{
Forwarded: &stanza.Forwarded{
XMLName: xml.Name{
Space: "urn:xmpp:forward:0",
Local: "forward",

View file

@ -6,6 +6,7 @@ import (
"log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
@ -21,12 +22,12 @@ func main() {
router := xmpp.NewRouter()
router.HandleFunc("message", handleMessage)
router.NewRoute().
IQNamespaces(xmpp.NSDiscoInfo).
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
IQNamespaces(stanza.NSDiscoInfo).
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
discoInfo(s, p, opts)
})
router.NewRoute().
IQNamespaces(xmpp.NSDiscoItems).
IQNamespaces(stanza.NSDiscoItems).
HandlerFunc(discoItems)
router.NewRoute().
IQNamespaces("jabber:iq:version").
@ -44,36 +45,36 @@ func main() {
log.Fatal(cm.Run())
}
func handleMessage(_ xmpp.Sender, p xmpp.Packet) {
msg, ok := p.(xmpp.Message)
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
fmt.Println("Received message:", msg.Body)
}
func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
// Type conversion & sanity checks
iq, ok := p.(xmpp.IQ)
iq, ok := p.(stanza.IQ)
if !ok || iq.Type != "get" {
return
}
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
identity := xmpp.Identity{
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 := xmpp.DiscoInfo{
payload := stanza.DiscoInfo{
XMLName: xml.Name{
Space: xmpp.NSDiscoInfo,
Space: stanza.NSDiscoInfo,
Local: "query",
},
Identity: identity,
Features: []xmpp.Feature{
{Var: xmpp.NSDiscoInfo},
{Var: xmpp.NSDiscoItems},
Identity: []stanza.Identity{identity},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
{Var: "jabber:iq:version"},
{Var: "urn:xmpp:delegation:1"},
},
@ -83,24 +84,24 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
}
// TODO: Handle iq error responses
func discoItems(c xmpp.Sender, p xmpp.Packet) {
func discoItems(c xmpp.Sender, p stanza.Packet) {
// Type conversion & sanity checks
iq, ok := p.(xmpp.IQ)
iq, ok := p.(stanza.IQ)
if !ok || iq.Type != "get" {
return
}
discoItems, ok := iq.Payload.(*xmpp.DiscoItems)
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
if !ok {
return
}
iqResp := xmpp.NewIQ(xmpp.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 xmpp.DiscoItems
var payload stanza.DiscoItems
if discoItems.Node == "" {
payload = xmpp.DiscoItems{
Items: []xmpp.DiscoItem{
payload = stanza.DiscoItems{
Items: []stanza.DiscoItem{
{Name: "test node", JID: "service.localhost", Node: "node1"},
},
}
@ -109,15 +110,15 @@ func discoItems(c xmpp.Sender, p xmpp.Packet) {
_ = c.Send(iqResp)
}
func handleVersion(c xmpp.Sender, p xmpp.Packet) {
func handleVersion(c xmpp.Sender, p stanza.Packet) {
// Type conversion & sanity checks
iq, ok := p.(xmpp.IQ)
iq, ok := p.(stanza.IQ)
if !ok {
return
}
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
var payload xmpp.Version
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

View file

@ -10,6 +10,7 @@ import (
"os"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func main() {
@ -35,15 +36,15 @@ func main() {
log.Fatal(cm.Run())
}
func handleMessage(s xmpp.Sender, p xmpp.Packet) {
msg, ok := p.(xmpp.Message)
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)
reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body}
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
_ = s.Send(reply)
}

View file

@ -12,6 +12,7 @@ import (
"github.com/processone/mpg123"
"github.com/processone/soundcloud"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
// Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player.
@ -41,12 +42,12 @@ func main() {
router := xmpp.NewRouter()
router.NewRoute().
Packet("message").
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleMessage(s, p, player)
})
router.NewRoute().
Packet("message").
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
handleIQ(s, p, player)
})
@ -59,8 +60,8 @@ func main() {
log.Fatal(cm.Run())
}
func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
msg, ok := p.(xmpp.Message)
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
msg, ok := p.(stanza.Message)
if !ok {
return
}
@ -73,14 +74,14 @@ func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
}
}
func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
iq, ok := p.(xmpp.IQ)
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
iq, ok := p.(stanza.IQ)
if !ok {
return
}
switch payload := iq.Payload.(type) {
// We support IOT Control IQ
case *xmpp.ControlSet:
case *stanza.ControlSet:
var url string
for _, element := range payload.Fields {
if element.XMLName.Local == "string" && element.Name == "url" {
@ -90,9 +91,9 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
}
playSCURL(player, url)
setResponse := new(xmpp.ControlSetResponse)
setResponse := new(stanza.ControlSetResponse)
// FIXME: Broken
reply := xmpp.IQ{Attrs: xmpp.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
_ = s.Send(reply)
// TODO add Soundclound artist / title retrieval
sendUserTune(s, "Radiohead", "Spectre")
@ -102,9 +103,9 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
}
func sendUserTune(s xmpp.Sender, artist string, title string) {
tune := xmpp.Tune{Artist: artist, Title: title}
iq := xmpp.NewIQ(xmpp.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
tune := stanza.Tune{Artist: artist, Title: title}
iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
iq.Payload = &payload
_ = s.Send(iq)
}

81
auth.go
View file

@ -6,9 +6,11 @@ import (
"errors"
"fmt"
"io"
"gosrc.io/xmpp/stanza"
)
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f StreamFeatures, user string, password string) (err error) {
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 {
@ -30,17 +32,17 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
raw := "\x00" + user + "\x00" + password
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
// Next message should be either success or failure.
val, err := nextPacket(decoder)
val, err := stanza.NextPacket(decoder)
if err != nil {
return err
}
switch v := val.(type) {
case SASLSuccess:
case SASLFailure:
case stanza.SASLSuccess:
case stanza.SASLFailure:
// v.Any is type of sub-element in failure, which gives a description of what failed.
err := errors.New("auth failure: " + v.Any.Local)
return NewConnError(err, true)
@ -49,72 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
}
return err
}
// ============================================================================
// SASLSuccess
type SASLSuccess struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
}
func (SASLSuccess) Name() string {
return "sasl:success"
}
type saslSuccessDecoder struct{}
var saslSuccess saslSuccessDecoder
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
var packet SASLSuccess
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// SASLFailure
type SASLFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name // error reason is a subelement
}
func (SASLFailure) Name() string {
return "sasl:failure"
}
type saslFailureDecoder struct{}
var saslFailure saslFailureDecoder
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
var packet SASLFailure
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
type auth struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
Mechanism string `xml:"mecanism,attr"`
Value string `xml:",innerxml"`
}
type BindBind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string `xml:"resource,omitempty"`
Jid string `xml:"jid,omitempty"`
}
func (b *BindBind) Namespace() string {
return b.XMLName.Space
}
// Session is obsolete in RFC 6121.
// Added for compliance with RFC 3121.
// Remove when ejabberd purely conforms to RFC 6121.
type sessionSession struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
optional xml.Name // If it does exist, it mean we are not required to open session
}

View file

@ -8,6 +8,8 @@ import (
"net"
"strings"
"time"
"gosrc.io/xmpp/stanza"
)
// TODO: Should I move this as an extension of the client?
@ -49,28 +51,28 @@ func (c *ServerCheck) Check() error {
decoder := xml.NewDecoder(tcpconn)
// Send stream open tag
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, NSClient, NSStream); err != nil {
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil {
return err
}
// Set xml decoder and extract streamID from reply (not used for now)
_, err = initStream(decoder)
_, err = stanza.InitStream(decoder)
if err != nil {
return err
}
// extract stream features
var f StreamFeatures
packet, err := nextPacket(decoder)
var f stanza.StreamFeatures
packet, err := stanza.NextPacket(decoder)
if err != nil {
err = fmt.Errorf("stream open decode features: %s", err)
return err
}
switch p := packet.(type) {
case StreamFeatures:
case stanza.StreamFeatures:
f = p
case StreamError:
case stanza.StreamError:
return errors.New("open stream error: " + p.Error.Local)
default:
return errors.New("expected packet received while expecting features, got " + p.Name())
@ -79,13 +81,13 @@ func (c *ServerCheck) Check() error {
if _, ok := f.DoesStartTLS(); ok {
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
var k tlsProceed
var k stanza.TLSProceed
if err = decoder.DecodeElement(&k, nil); err != nil {
return fmt.Errorf("expecting starttls proceed: %s", err)
}
DefaultTlsConfig.ServerName = c.domain
tlsConn := tls.Client(tcpconn, &DefaultTlsConfig)
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

@ -6,6 +6,8 @@ import (
"fmt"
"net"
"time"
"gosrc.io/xmpp/stanza"
)
//=============================================================================
@ -153,7 +155,7 @@ func (c *Client) SetHandler(handler EventHandler) {
}
// Send marshals XMPP stanza and sends it to the server.
func (c *Client) Send(packet Packet) error {
func (c *Client) Send(packet stanza.Packet) error {
conn := c.conn
if conn == nil {
return errors.New("client is not connected")
@ -191,7 +193,7 @@ func (c *Client) SendRaw(packet string) error {
// Loop: Receive data from server
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
for {
val, err := nextPacket(c.Session.decoder)
val, err := stanza.NextPacket(c.Session.decoder)
if err != nil {
close(keepaliveQuit)
c.updateState(StateDisconnected)
@ -200,7 +202,7 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
// Handle stream errors
switch packet := val.(type) {
case StreamError:
case stanza.StreamError:
c.router.route(c, val)
close(keepaliveQuit)
c.streamError(packet.Error.Local, packet.Text)

View file

@ -7,6 +7,8 @@ import (
"net"
"testing"
"time"
"gosrc.io/xmpp/stanza"
)
const (
@ -126,11 +128,11 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
switch elem := token.(type) {
// Wait for first startElement
case xml.StartElement:
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
return
}
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", NSClient, NSStream); err != nil {
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
t.Errorf("cannot write server stream open: %s", err)
}
return
@ -152,14 +154,14 @@ func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
// TODO return err in case of error reading the auth params
func readAuth(t *testing.T, decoder *xml.Decoder) string {
se, err := nextStart(decoder)
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read auth: %s", err)
return ""
}
var nv interface{}
nv = &auth{}
nv = &stanza.Auth{}
// Decode element into pointer storage
if err = decoder.DecodeElement(nv, &se); err != nil {
t.Errorf("cannot decode auth: %s", err)
@ -167,7 +169,7 @@ func readAuth(t *testing.T, decoder *xml.Decoder) string {
}
switch v := nv.(type) {
case *auth:
case *stanza.Auth:
return v.Value
}
return ""
@ -184,13 +186,13 @@ func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
}
func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
se, err := nextStart(decoder)
se, err := stanza.NextStart(decoder)
if err != nil {
t.Errorf("cannot read bind: %s", err)
return
}
iq := &IQ{}
iq := &stanza.IQ{}
// Decode element into pointer storage
if err = decoder.DecodeElement(&iq, &se); err != nil {
t.Errorf("cannot decode bind iq: %s", err)
@ -199,7 +201,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
// TODO Check all elements
switch iq.Payload.(type) {
case *BindBind:
case *stanza.BindBind:
result := `<iq id='%s' type='result'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>%s</jid>

View file

@ -9,6 +9,8 @@ import (
"io"
"net"
"time"
"gosrc.io/xmpp/stanza"
)
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
@ -72,13 +74,13 @@ func (c *Component) Connect() error {
c.conn = conn
// 1. Send stream open tag
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, NSComponent, NSStream); err != nil {
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
return errors.New("cannot send stream open " + err.Error())
}
c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply
streamId, err := initStream(c.decoder)
streamId, err := stanza.InitStream(c.decoder)
if err != nil {
return errors.New("cannot init decoder " + err.Error())
}
@ -89,15 +91,15 @@ func (c *Component) Connect() error {
}
// 4. Check server response for authentication
val, err := nextPacket(c.decoder)
val, err := stanza.NextPacket(c.decoder)
if err != nil {
return err
}
switch v := val.(type) {
case StreamError:
case stanza.StreamError:
return errors.New("handshake failed " + v.Error.Local)
case Handshake:
case stanza.Handshake:
// Start the receiver go routine
go c.recv()
return nil
@ -119,7 +121,7 @@ func (c *Component) SetHandler(handler EventHandler) {
// Receiver Go routine receiver
func (c *Component) recv() (err error) {
for {
val, err := nextPacket(c.decoder)
val, err := stanza.NextPacket(c.decoder)
if err != nil {
c.updateState(StateDisconnected)
return err
@ -127,7 +129,7 @@ func (c *Component) recv() (err error) {
// Handle stream errors
switch p := val.(type) {
case StreamError:
case stanza.StreamError:
c.router.route(c, val)
c.streamError(p.Error.Local, p.Text)
return errors.New("stream error: " + p.Error.Local)
@ -137,7 +139,7 @@ func (c *Component) recv() (err error) {
}
// Send marshalls XMPP stanza and sends it to the server.
func (c *Component) Send(packet Packet) error {
func (c *Component) Send(packet stanza.Packet) error {
conn := c.conn
if conn == nil {
return errors.New("component is not connected")
@ -186,90 +188,6 @@ func (c *Component) handshake(streamId string) string {
return encodedStr
}
// ============================================================================
// Handshake Stanza
// Handshake is a stanza used by XMPP components to authenticate on XMPP
// component port.
type Handshake struct {
XMLName xml.Name `xml:"jabber:component:accept handshake"`
// TODO Add handshake value with test for proper serialization
// Value string `xml:",innerxml"`
}
func (Handshake) Name() string {
return "component:handshake"
}
// Handshake decoding wrapper
type handshakeDecoder struct{}
var handshake handshakeDecoder
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
var packet Handshake
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// Component delegation
// XEP-0355
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
// depending on the context.
type Delegation struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
Delegated *Delegated // This is used in a message to confirm delegated namespace
}
func (d *Delegation) Namespace() string {
return d.XMLName.Space
}
type Forwarded struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Stanza Packet
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if packet, err := decodeClient(d, tt); err == nil {
f.Stanza = packet
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
type Delegated struct {
XMLName xml.Name `xml:"delegated"`
Namespace string `xml:"namespace,attr,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
}
/*
TODO: Add support for discovery management directly in component
TODO: Support multiple identities on disco info

View file

@ -1,7 +1,6 @@
package xmpp
import (
"encoding/xml"
"testing"
)
@ -24,76 +23,3 @@ func TestHandshake(t *testing.T) {
func TestGenerateHandshake(t *testing.T) {
// TODO
}
// We should be able to properly parse delegation confirmation messages
func TestParsingDelegationMessage(t *testing.T) {
packetStr := `<message to='service.localhost' from='localhost'>
<delegation xmlns='urn:xmpp:delegation:1'>
<delegated namespace='http://jabber.org/protocol/pubsub'/>
</delegation>
</message>`
var msg Message
data := []byte(packetStr)
if err := xml.Unmarshal(data, &msg); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as MsgExtension
var nsDelegated string
for _, ext := range msg.Extensions {
if delegation, ok := ext.(*Delegation); ok {
nsDelegated = delegation.Delegated.Namespace
}
}
if nsDelegated != "http://jabber.org/protocol/pubsub" {
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
}
}
// Check that we can parse a delegation IQ.
// The most important thing is to be able to
func TestParsingDelegationIQ(t *testing.T) {
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
<delegation xmlns='urn:xmpp:delegation:1'>
<forwarded xmlns='urn:xmpp:forward:0'>
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='http://jabber.org/protocol/mood'>
<item id='current'>
<mood xmlns='http://jabber.org/protocol/mood'>
<excited/>
</mood>
</item>
</publish>
</pubsub>
</iq>
</forwarded>
</delegation>
</iq>`
var iq IQ
data := []byte(packetStr)
if err := xml.Unmarshal(data, &iq); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as IQPayload
var node string
if iq.Payload != nil {
if delegation, ok := iq.Payload.(*Delegation); ok {
packet := delegation.Forwarded.Stanza
forwardedIQ, ok := packet.(IQ)
if !ok {
t.Errorf("Could not extract packet IQ")
return
}
if forwardedIQ.Payload != nil {
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
node = pubsub.Publish.Node
}
}
}
}
if node != "http://jabber.org/protocol/mood" {
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
}
}

254
iq.go
View file

@ -1,254 +0,0 @@
package xmpp
import (
"encoding/xml"
"fmt"
)
/*
TODO support ability to put Raw payload inside IQ
*/
// ============================================================================
// IQ Packet
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
type IQ struct { // Info/Query
XMLName xml.Name `xml:"iq"`
// MUST have a ID
Attrs
// We can only have one payload on IQ:
// "An IQ stanza of type "get" or "set" MUST contain exactly one
// child element, which specifies the semantics of the particular
// request."
Payload IQPayload `xml:",omitempty"`
Error Err `xml:"error,omitempty"`
// Any is used to decode unknown payload as a generique structure
Any *Node `xml:",any"`
}
type IQPayload interface {
Namespace() string
}
func NewIQ(a Attrs) IQ {
// TODO generate IQ ID if not set
// TODO ensure that type is set, as it is required
return IQ{
XMLName: xml.Name{Local: "iq"},
Attrs: a,
}
}
func (iq IQ) MakeError(xerror Err) IQ {
from := iq.From
to := iq.To
iq.Type = "error"
iq.From = to
iq.To = from
iq.Error = xerror
return iq
}
func (IQ) Name() string {
return "iq"
}
type iqDecoder struct{}
var iq iqDecoder
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
var packet IQ
err := p.DecodeElement(&packet, &se)
return packet, err
}
// UnmarshalXML implements custom parsing for IQs
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.XMLName = start.Name
// Extract IQ attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
iq.Id = attr.Value
}
if attr.Name.Local == "type" {
iq.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
iq.To = attr.Value
}
if attr.Name.Local == "from" {
iq.From = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if tt.Name.Local == "error" {
var xmppError Err
err = d.DecodeElement(&xmppError, &tt)
if err != nil {
fmt.Println(err)
return err
}
iq.Error = xmppError
continue
}
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
// Decode payload extension
err = d.DecodeElement(iqExt, &tt)
if err != nil {
return err
}
iq.Payload = iqExt
continue
}
// TODO: If unknown decode as generic node
node := new(Node)
err = d.DecodeElement(node, &tt)
if err != nil {
return err
}
iq.Any = node
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
// ============================================================================
// Generic / unknown content
// Node is a generic structure to represent XML data. It is used to parse
// unreferenced or custom stanza payload.
type Node struct {
XMLName xml.Name
Attrs []xml.Attr `xml:"-"`
Content string `xml:",innerxml"`
Nodes []Node `xml:",any"`
}
func (n *Node) Namespace() string {
return n.XMLName.Space
}
// Attr represents generic XML attributes, as used on the generic XML Node
// representation.
type Attr struct {
K string
V string
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
for _, attr := range start.Attr {
// Do not repeat xmlns, it is already in XMLName
if attr.Name.Local != "xmlns" {
n.Attrs = append(n.Attrs, attr)
}
}
type node Node
return d.DecodeElement((*node)(n), &start)
}
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
// Node structure to XML.
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
start.Attr = n.Attrs
start.Name = n.XMLName
err = e.EncodeToken(start)
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
// ============================================================================
// Disco
const (
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
NSDiscoItems = "http://jabber.org/protocol/disco#items"
)
// Disco Info
type DiscoInfo struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
Node string `xml:"node,attr,omitempty"`
Identity Identity `xml:"identity"`
Features []Feature `xml:"feature"`
}
func (d *DiscoInfo) Namespace() string {
return d.XMLName.Space
}
type Identity struct {
XMLName xml.Name `xml:"identity,omitempty"`
Name string `xml:"name,attr,omitempty"`
Category string `xml:"category,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
}
type Feature struct {
XMLName xml.Name `xml:"feature"`
Var string `xml:"var,attr"`
}
// Disco Items
type DiscoItems struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
Node string `xml:"node,attr,omitempty"`
Items []DiscoItem `xml:"item"`
}
func (d *DiscoItems) Namespace() string {
return d.XMLName.Space
}
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"`
}
// ============================================================================
// Software Version (XEP-0092)
// Version
type Version struct {
XMLName xml.Name `xml:"jabber:iq:version query"`
Name string `xml:"name,omitempty"`
Version string `xml:"version,omitempty"`
OS string `xml:"os,omitempty"`
}
func (v *Version) Namespace() string {
return v.XMLName.Space
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
}

View file

@ -3,6 +3,8 @@ package xmpp
import (
"encoding/xml"
"strings"
"gosrc.io/xmpp/stanza"
)
/*
@ -32,7 +34,7 @@ func NewRouter() *Router {
// route is called by the XMPP client to dispatch stanza received using the set up routes.
// It is also used by test, but is not supposed to be used directly by users of the library.
func (r *Router) route(s Sender, p Packet) {
func (r *Router) route(s Sender, p stanza.Packet) {
var match RouteMatch
if r.Match(p, &match) {
@ -41,15 +43,15 @@ func (r *Router) route(s Sender, p Packet) {
return
}
// If there is no match and we receive an iq set or get, we need to send a reply
if iq, ok := p.(IQ); ok {
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
if iq, ok := p.(stanza.IQ); ok {
if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet {
iqNotImplemented(s, iq)
}
}
}
func iqNotImplemented(s Sender, iq IQ) {
err := Err{
func iqNotImplemented(s Sender, iq stanza.IQ) {
err := stanza.Err{
XMLName: xml.Name{Local: "error"},
Code: 501,
Type: "cancel",
@ -66,7 +68,7 @@ func (r *Router) NewRoute() *Route {
return route
}
func (r *Router) Match(p Packet, match *RouteMatch) bool {
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
for _, route := range r.routes {
if route.Match(p, match) {
return true
@ -83,14 +85,14 @@ func (r *Router) Handle(name string, handler Handler) *Route {
// HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence)
// See Route.Path() and Route.HandlerFunc().
func (r *Router) HandleFunc(name string, f func(s Sender, p Packet)) *Route {
func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Route {
return r.NewRoute().Packet(name).HandlerFunc(f)
}
// ============================================================================
// Route
type Handler interface {
HandlePacket(s Sender, p Packet)
HandlePacket(s Sender, p stanza.Packet)
}
type Route struct {
@ -108,10 +110,10 @@ func (r *Route) Handler(handler Handler) *Route {
// ordinary functions as XMPP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(s Sender, p Packet)
type HandlerFunc func(s Sender, p stanza.Packet)
// HandlePacket calls f(s, p)
func (f HandlerFunc) HandlePacket(s Sender, p Packet) {
func (f HandlerFunc) HandlePacket(s Sender, p stanza.Packet) {
f(s, p)
}
@ -126,7 +128,7 @@ func (r *Route) addMatcher(m matcher) *Route {
return r
}
func (r *Route) Match(p Packet, match *RouteMatch) bool {
func (r *Route) Match(p stanza.Packet, match *RouteMatch) bool {
for _, m := range r.matchers {
if matched := m.Match(p, match); !matched {
return false
@ -144,18 +146,18 @@ func (r *Route) Match(p Packet, match *RouteMatch) bool {
type nameMatcher string
func (n nameMatcher) Match(p Packet, match *RouteMatch) bool {
func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
var name string
// TODO: To avoid type switch everywhere in matching, I think we will need to have
// to move to a concrete type for packets, to make matching and comparison more natural.
// Current code structure is probably too rigid.
// Maybe packet types should even be from an enum.
switch p.(type) {
case Message:
case stanza.Message:
name = "message"
case IQ:
case stanza.IQ:
name = "iq"
case Presence:
case stanza.Presence:
name = "presence"
}
if name == string(n) {
@ -177,14 +179,14 @@ func (r *Route) Packet(name string) *Route {
// nsTypeMather matches on a list of IQ payload namespaces
type nsTypeMatcher []string
func (m nsTypeMatcher) Match(p Packet, match *RouteMatch) bool {
var stanzaType StanzaType
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
var stanzaType stanza.StanzaType
switch packet := p.(type) {
case IQ:
case stanza.IQ:
stanzaType = packet.Type
case Presence:
case stanza.Presence:
stanzaType = packet.Type
case Message:
case stanza.Message:
if packet.Type == "" {
// optional on message, normal is the default type
stanzaType = "normal"
@ -211,8 +213,8 @@ func (r *Route) StanzaType(types ...string) *Route {
// nsIqMather matches on a list of IQ payload namespaces
type nsIQMatcher []string
func (m nsIQMatcher) Match(p Packet, match *RouteMatch) bool {
iq, ok := p.(IQ)
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
iq, ok := p.(stanza.IQ)
if !ok {
return false
}
@ -235,7 +237,7 @@ func (r *Route) IQNamespaces(namespaces ...string) *Route {
// Matchers are used to "specialize" routes and focus on specific packet features
type matcher interface {
Match(Packet, *RouteMatch) bool
Match(stanza.Packet, *RouteMatch) bool
}
// RouteMatch extracts and gather match information

View file

@ -4,6 +4,8 @@ import (
"bytes"
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// ============================================================================
@ -11,13 +13,13 @@ import (
func TestNameMatcher(t *testing.T) {
router := NewRouter()
router.HandleFunc("message", func(s Sender, p Packet) {
router.HandleFunc("message", func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that a message packet is properly matched
conn := NewSenderMock()
msg := NewMessage(Attrs{Type: MessageTypeChat, To: "test@localhost", Id: "1"})
msg := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, To: "test@localhost", Id: "1"})
msg.Body = "Hello"
router.route(conn, msg)
if conn.String() != successFlag {
@ -26,8 +28,8 @@ func TestNameMatcher(t *testing.T) {
// Check that an IQ packet is not matched
conn = NewSenderMock()
iq := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"})
iq.Payload = &DiscoInfo{}
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
iq.Payload = &stanza.DiscoInfo{}
router.route(conn, iq)
if conn.String() == successFlag {
t.Error("IQ should not have been matched and routed")
@ -37,18 +39,18 @@ func TestNameMatcher(t *testing.T) {
func TestIQNSMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
IQNamespaces(NSDiscoInfo, NSDiscoItems).
HandlerFunc(func(s Sender, p Packet) {
IQNamespaces(stanza.NSDiscoInfo, stanza.NSDiscoItems).
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that an IQ with proper namespace does match
conn := NewSenderMock()
iqDisco := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"})
iqDisco := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
// TODO: Add a function to generate payload with proper namespace initialisation
iqDisco.Payload = &DiscoInfo{
iqDisco.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: NSDiscoInfo,
Space: stanza.NSDiscoInfo,
Local: "query",
}}
router.route(conn, iqDisco)
@ -58,9 +60,9 @@ func TestIQNSMatcher(t *testing.T) {
// Check that another namespace is not matched
conn = NewSenderMock()
iqVersion := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"})
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
// TODO: Add a function to generate payload with proper namespace initialisation
iqVersion.Payload = &DiscoInfo{
iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
@ -75,13 +77,13 @@ func TestTypeMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
StanzaType("normal").
HandlerFunc(func(s Sender, p Packet) {
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that a packet with the proper type matches
conn := NewSenderMock()
message := NewMessage(Attrs{Type: "normal", To: "test@localhost", Id: "1"})
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
message.Body = "hello"
router.route(conn, message)
@ -91,7 +93,7 @@ func TestTypeMatcher(t *testing.T) {
// We should match on default type 'normal' for message without a type
conn = NewSenderMock()
message = NewMessage(Attrs{To: "test@localhost", Id: "1"})
message = stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
message.Body = "hello"
router.route(conn, message)
@ -101,8 +103,8 @@ func TestTypeMatcher(t *testing.T) {
// We do not match on other types
conn = NewSenderMock()
iqVersion := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &DiscoInfo{
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
@ -119,38 +121,38 @@ func TestCompositeMatcher(t *testing.T) {
router.NewRoute().
IQNamespaces("jabber:iq:version").
StanzaType("get").
HandlerFunc(func(s Sender, p Packet) {
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Data set
getVersionIq := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
getVersionIq.Payload = &Version{
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
getVersionIq.Payload = &stanza.Version{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
setVersionIq := NewIQ(Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
setVersionIq.Payload = &Version{
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
setVersionIq.Payload = &stanza.Version{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
}}
GetDiscoIq := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
GetDiscoIq.Payload = &DiscoInfo{
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
GetDiscoIq.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "http://jabber.org/protocol/disco#info",
Local: "query",
}}
message := NewMessage(Attrs{Type: "normal", To: "test@localhost", Id: "1"})
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
message.Body = "hello"
tests := []struct {
name string
input Packet
input stanza.Packet
want bool
}{
{name: "match get version iq", input: getVersionIq, want: true},
@ -178,13 +180,13 @@ func TestCompositeMatcher(t *testing.T) {
func TestCatchallMatcher(t *testing.T) {
router := NewRouter()
router.NewRoute().
HandlerFunc(func(s Sender, p Packet) {
HandlerFunc(func(s Sender, p stanza.Packet) {
_ = s.SendRaw(successFlag)
})
// Check that we match on several packets
conn := NewSenderMock()
message := NewMessage(Attrs{Type: "chat", To: "test@localhost", Id: "1"})
message := stanza.NewMessage(stanza.Attrs{Type: "chat", To: "test@localhost", Id: "1"})
message.Body = "hello"
router.route(conn, message)
@ -193,8 +195,8 @@ func TestCatchallMatcher(t *testing.T) {
}
conn = NewSenderMock()
iqVersion := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &DiscoInfo{
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
iqVersion.Payload = &stanza.DiscoInfo{
XMLName: xml.Name{
Space: "jabber:iq:version",
Local: "query",
@ -219,7 +221,7 @@ func NewSenderMock() SenderMock {
return SenderMock{buffer: new(bytes.Buffer)}
}
func (s SenderMock) Send(packet Packet) error {
func (s SenderMock) Send(packet stanza.Packet) error {
out, err := xml.Marshal(packet)
if err != nil {
return err
@ -239,7 +241,7 @@ func (s SenderMock) String() string {
func TestSenderMock(t *testing.T) {
conn := NewSenderMock()
msg := NewMessage(Attrs{To: "test@localhost", Id: "1"})
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
msg.Body = "Hello"
if err := conn.Send(msg); err != nil {
t.Error("Could not send message")

View file

@ -7,6 +7,8 @@ import (
"fmt"
"io"
"net"
"gosrc.io/xmpp/stanza"
)
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
@ -15,7 +17,7 @@ type Session struct {
// Session info
BindJid string // Jabber ID as provided by XMPP server
StreamId string
Features StreamFeatures
Features stanza.StreamFeatures
TlsEnabled bool
lastPacketId int
@ -85,14 +87,14 @@ func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Config) {
s.decoder.CharsetReader = o.CharsetReader
}
func (s *Session) open(domain string) (f StreamFeatures) {
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
// Send stream open tag
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, NSClient, NSStream); s.err != nil {
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
return
}
// Set xml decoder and extract streamID from reply
s.StreamId, s.err = initStream(s.decoder) // TODO refactor / rename
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
if s.err != nil {
return
}
@ -112,7 +114,7 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
if _, ok := s.Features.DoesStartTLS(); ok {
fmt.Fprintf(s.socketProxy, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
var k tlsProceed
var k stanza.TLSProceed
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
return conn
@ -120,8 +122,8 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
s.TlsEnabled = true
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
DefaultTlsConfig.ServerName = domain
tlsConn := tls.Client(conn, &DefaultTlsConfig)
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
@ -153,12 +155,12 @@ func (s *Session) bind(o Config) {
var resource = o.parsedJid.Resource
if resource != "" {
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
s.PacketId(), nsBind, resource)
s.PacketId(), stanza.NSBind, resource)
} else {
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
}
var iq IQ
var iq stanza.IQ
if s.err = s.decoder.Decode(&iq); s.err != nil {
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
return
@ -166,7 +168,7 @@ func (s *Session) bind(o Config) {
// TODO Check all elements
switch payload := iq.Payload.(type) {
case *BindBind:
case *stanza.BindBind:
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
default:
s.err = errors.New("iq bind result missing")
@ -182,9 +184,9 @@ func (s *Session) rfc3921Session(o Config) {
return
}
var iq IQ
if s.Features.Session.optional.Local != "" {
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
var iq stanza.IQ
if s.Features.Session.Optional.Local != "" {
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
if s.err = s.decoder.Decode(&iq); s.err != nil {
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
return

76
stanza/README.md Normal file
View file

@ -0,0 +1,76 @@
# XMPP Stanza
XMPP `stanza` package is use to parse, marshall and and unmarshal XMPP stanzas and nonzas.
## Stanza creation
When creating stanzas, you can use two approaches:
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
stanza.
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
final stanza.
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
struct that you can build by hand.
### Composing stanzas manually with structs
Here is for example how you would generate an IQ discovery result:
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
### Using helpers
Here is for example how you would generate an IQ discovery result using Builder:
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")
## Payload and extensions
### Message
### Presence
### IQ
IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with
Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support
having multiple payloads.
Here is the list of structs implementing IQPayloads:
- BindBind
- Pubsub
Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic
`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases,
you may also consider [adding your own custom extensions on stanzas]().
## Adding your own custom extensions on stanzas
Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may
want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas.

79
stanza/auth_sasl.go Normal file
View file

@ -0,0 +1,79 @@
package stanza
import "encoding/xml"
// ============================================================================
// SASLSuccess
type SASLSuccess struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
}
func (SASLSuccess) Name() string {
return "sasl:success"
}
type saslSuccessDecoder struct{}
var saslSuccess saslSuccessDecoder
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
var packet SASLSuccess
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// SASLFailure
type SASLFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
Any xml.Name // error reason is a subelement
}
func (SASLFailure) Name() string {
return "sasl:failure"
}
type saslFailureDecoder struct{}
var saslFailure saslFailureDecoder
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
var packet SASLFailure
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
type Auth struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
Mechanism string `xml:"mecanism,attr"`
Value string `xml:",innerxml"`
}
type BindBind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string `xml:"resource,omitempty"`
Jid string `xml:"jid,omitempty"`
}
func (b *BindBind) Namespace() string {
return b.XMLName.Space
}
// Session is obsolete in RFC 6121.
// Added for compliance with RFC 3121.
// Remove when ejabberd purely conforms to RFC 6121.
type sessionSession struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
Optional xml.Name // If it does exist, it mean we are not required to open session
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{})
}

89
stanza/component.go Normal file
View file

@ -0,0 +1,89 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Handshake Stanza
// Handshake is a stanza used by XMPP components to authenticate on XMPP
// component port.
type Handshake struct {
XMLName xml.Name `xml:"jabber:component:accept handshake"`
// TODO Add handshake value with test for proper serialization
// Value string `xml:",innerxml"`
}
func (Handshake) Name() string {
return "component:handshake"
}
// Handshake decoding wrapper
type handshakeDecoder struct{}
var handshake handshakeDecoder
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
var packet Handshake
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// Component delegation
// XEP-0355
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
// depending on the context.
type Delegation struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
Delegated *Delegated // This is used in a message to confirm delegated namespace
}
func (d *Delegation) Namespace() string {
return d.XMLName.Space
}
type Forwarded struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Stanza Packet
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if packet, err := decodeClient(d, tt); err == nil {
f.Stanza = packet
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
type Delegated struct {
XMLName xml.Name `xml:"delegated"`
Namespace string `xml:"namespace,attr,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
}

79
stanza/component_test.go Normal file
View file

@ -0,0 +1,79 @@
package stanza
import (
"encoding/xml"
"testing"
)
// We should be able to properly parse delegation confirmation messages
func TestParsingDelegationMessage(t *testing.T) {
packetStr := `<message to='service.localhost' from='localhost'>
<delegation xmlns='urn:xmpp:delegation:1'>
<delegated namespace='http://jabber.org/protocol/pubsub'/>
</delegation>
</message>`
var msg Message
data := []byte(packetStr)
if err := xml.Unmarshal(data, &msg); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as MsgExtension
var nsDelegated string
for _, ext := range msg.Extensions {
if delegation, ok := ext.(*Delegation); ok {
nsDelegated = delegation.Delegated.Namespace
}
}
if nsDelegated != "http://jabber.org/protocol/pubsub" {
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
}
}
// Check that we can parse a delegation IQ.
// The most important thing is to be able to
func TestParsingDelegationIQ(t *testing.T) {
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
<delegation xmlns='urn:xmpp:delegation:1'>
<forwarded xmlns='urn:xmpp:forward:0'>
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='http://jabber.org/protocol/mood'>
<item id='current'>
<mood xmlns='http://jabber.org/protocol/mood'>
<excited/>
</mood>
</item>
</publish>
</pubsub>
</iq>
</forwarded>
</delegation>
</iq>`
var iq IQ
data := []byte(packetStr)
if err := xml.Unmarshal(data, &iq); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as IQPayload
var node string
if iq.Payload != nil {
if delegation, ok := iq.Payload.(*Delegation); ok {
packet := delegation.Forwarded.Stanza
forwardedIQ, ok := packet.(IQ)
if !ok {
t.Errorf("Could not extract packet IQ")
return
}
if forwardedIQ.Payload != nil {
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
node = pubsub.Publish.Node
}
}
}
}
if node != "http://jabber.org/protocol/mood" {
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
}
}

View file

@ -1,14 +1,10 @@
package xmpp
package stanza
import (
"encoding/xml"
"strconv"
)
/*
TODO support ability to put Raw payload inside IQ
*/
// ============================================================================
// XMPP Errors

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
// ErrorType is a Enum of error attribute type
type ErrorType string

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"
@ -24,10 +24,16 @@ type ControlField struct {
}
type ControlSetResponse struct {
IQPayload
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
}
func (c *ControlSetResponse) Namespace() string {
return c.XMLName.Space
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
}

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

130
stanza/iq.go Normal file
View file

@ -0,0 +1,130 @@
package stanza
import (
"encoding/xml"
"fmt"
)
/*
TODO support ability to put Raw payload inside IQ
*/
// ============================================================================
// IQ Packet
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
type IQ struct { // Info/Query
XMLName xml.Name `xml:"iq"`
// MUST have a ID
Attrs
// We can only have one payload on IQ:
// "An IQ stanza of type "get" or "set" MUST contain exactly one
// child element, which specifies the semantics of the particular
// request."
Payload IQPayload `xml:",omitempty"`
Error Err `xml:"error,omitempty"`
// Any is used to decode unknown payload as a generique structure
Any *Node `xml:",any"`
}
type IQPayload interface {
Namespace() string
}
func NewIQ(a Attrs) IQ {
// TODO generate IQ ID if not set
// TODO ensure that type is set, as it is required
return IQ{
XMLName: xml.Name{Local: "iq"},
Attrs: a,
}
}
func (iq IQ) MakeError(xerror Err) IQ {
from := iq.From
to := iq.To
iq.Type = "error"
iq.From = to
iq.To = from
iq.Error = xerror
return iq
}
func (IQ) Name() string {
return "iq"
}
type iqDecoder struct{}
var iq iqDecoder
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
var packet IQ
err := p.DecodeElement(&packet, &se)
return packet, err
}
// UnmarshalXML implements custom parsing for IQs
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.XMLName = start.Name
// Extract IQ attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
iq.Id = attr.Value
}
if attr.Name.Local == "type" {
iq.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
iq.To = attr.Value
}
if attr.Name.Local == "from" {
iq.From = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if tt.Name.Local == "error" {
var xmppError Err
err = d.DecodeElement(&xmppError, &tt)
if err != nil {
fmt.Println(err)
return err
}
iq.Error = xmppError
continue
}
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
// Decode payload extension
err = d.DecodeElement(iqExt, &tt)
if err != nil {
return err
}
iq.Payload = iqExt
continue
}
// TODO: If unknown decode as generic node
node := new(Node)
err = d.DecodeElement(node, &tt)
if err != nil {
return err
}
iq.Any = node
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}

120
stanza/iq_disco.go Normal file
View file

@ -0,0 +1,120 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Disco Info
const (
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
)
// ----------
// Namespaces
type DiscoInfo struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
Node string `xml:"node,attr,omitempty"`
Identity []Identity `xml:"identity"`
Features []Feature `xml:"feature"`
}
func (d *DiscoInfo) Namespace() string {
return d.XMLName.Space
}
// ---------------
// Builder helpers
// DiscoInfo builds a default DiscoInfo payload
func (iq *IQ) DiscoInfo() *DiscoInfo {
d := DiscoInfo{
XMLName: xml.Name{
Space: NSDiscoInfo,
Local: "query",
},
}
iq.Payload = &d
return &d
}
func (d *DiscoInfo) AddIdentity(name, category, typ string) {
identity := Identity{
XMLName: xml.Name{Local: "identity"},
Name: name,
Category: category,
Type: typ,
}
d.Identity = append(d.Identity, identity)
}
func (d *DiscoInfo) AddFeatures(namespace ...string) {
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
}
func (d *DiscoInfo) SetNode(node string) {
d.Node = node
}
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
d.Identity = ident
return d
}
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
for _, ns := range namespace {
d.Features = append(d.Features, Feature{Var: ns})
}
return d
}
// -----------
// SubElements
type Identity struct {
XMLName xml.Name `xml:"identity,omitempty"`
Name string `xml:"name,attr,omitempty"`
Category string `xml:"category,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
}
type Feature struct {
XMLName xml.Name `xml:"feature"`
Var string `xml:"var,attr"`
}
// ============================================================================
// Disco Info
const (
NSDiscoItems = "http://jabber.org/protocol/disco#items"
)
type DiscoItems struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
Node string `xml:"node,attr,omitempty"`
Items []DiscoItem `xml:"item"`
}
func (d *DiscoItems) Namespace() string {
return d.XMLName.Space
}
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"`
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
}

55
stanza/iq_disco_test.go Normal file
View file

@ -0,0 +1,55 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestDiscoInfoBuilder(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)
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")
}
// Check features
features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"}
if len(pp.Features) != len(features) {
t.Errorf("Features length mismatch: %#v", pp.Features)
} else {
for i, f := range pp.Features {
if f.Var != features[i] {
t.Errorf("Missing feature: %s", features[i])
}
}
}
// Check identity
if len(pp.Identity) != 1 {
t.Errorf("Identity length mismatch: %#v", pp.Identity)
} else {
if pp.Identity[0].Name != "Test Component" {
t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name)
}
}
}

View file

@ -1,4 +1,4 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
@ -6,22 +6,22 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func TestUnmarshalIqs(t *testing.T) {
//var cs1 = new(iot.ControlSet)
var tests = []struct {
iqString string
parsedIQ xmpp.IQ
parsedIQ stanza.IQ
}{
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
xmpp.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: xmpp.Attrs{Type: xmpp.IQTypeSet, To: "test@localhost", Id: "1"}}},
stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.IQTypeSet, To: "test@localhost", Id: "1"}}},
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
}
for _, test := range tests {
parsedIQ := xmpp.IQ{}
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal(%s) returned error", test.iqString)
@ -35,16 +35,16 @@ func TestUnmarshalIqs(t *testing.T) {
}
func TestGenerateIq(t *testing.T) {
iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := xmpp.DiscoInfo{
Identity: xmpp.Identity{
Name: "Test Gateway",
Category: "gateway",
Type: "mqtt",
},
Features: []xmpp.Feature{
{Var: xmpp.NSDiscoInfo},
{Var: xmpp.NSDiscoItems},
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := stanza.DiscoInfo{
Identity: []stanza.Identity{
{Name: "Test Gateway",
Category: "gateway",
Type: "mqtt",
}},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
},
}
iq.Payload = &payload
@ -58,18 +58,18 @@ func TestGenerateIq(t *testing.T) {
t.Error("empty error should not be serialized")
}
parsedIQ := xmpp.IQ{}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
}
}
func TestErrorTag(t *testing.T) {
xError := xmpp.Err{
xError := stanza.Err{
XMLName: xml.Name{Local: "error"},
Code: 503,
Type: "cancel",
@ -82,7 +82,7 @@ func TestErrorTag(t *testing.T) {
t.Errorf("cannot marshal xml structure: %s", err)
}
parsedError := xmpp.Err{}
parsedError := stanza.Err{}
if err = xml.Unmarshal(data, &parsedError); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
@ -93,8 +93,8 @@ func TestErrorTag(t *testing.T) {
}
func TestDiscoItems(t *testing.T) {
iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
payload := xmpp.DiscoItems{
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
payload := stanza.DiscoItems{
Node: "music",
}
iq.Payload = &payload
@ -104,7 +104,7 @@ func TestDiscoItems(t *testing.T) {
t.Errorf("cannot marshal xml structure")
}
parsedIQ := xmpp.IQ{}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
@ -117,7 +117,7 @@ func TestDiscoItems(t *testing.T) {
func TestUnmarshalPayload(t *testing.T) {
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
parsedIQ := xmpp.IQ{}
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(query), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal(%s) returned error", query)
@ -142,7 +142,7 @@ func TestPayloadWithError(t *testing.T) {
</error>
</iq>`
parsedIQ := xmpp.IQ{}
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %s", iq)
@ -158,7 +158,7 @@ func TestUnknownPayload(t *testing.T) {
iq := `<iq type="get" to="service.localhost" id="1" >
<query xmlns="unknown:ns"/>
</iq>`
parsedIQ := xmpp.IQ{}
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %#v (%s)", err, iq)

25
stanza/iq_version.go Normal file
View file

@ -0,0 +1,25 @@
package stanza
import "encoding/xml"
// ============================================================================
// Software Version (XEP-0092)
// Version
type Version struct {
XMLName xml.Name `xml:"jabber:iq:version query"`
Name string `xml:"name,omitempty"`
Version string `xml:"version,omitempty"`
OS string `xml:"os,omitempty"`
}
func (v *Version) Namespace() string {
return v.XMLName.Space
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
}

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,15 +1,15 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func TestGenerateMessage(t *testing.T) {
message := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
message.Body = "Hi"
message.Subject = "Msg Subject"
@ -18,7 +18,7 @@ func TestGenerateMessage(t *testing.T) {
t.Errorf("cannot marshal xml structure")
}
parsedMessage := xmpp.Message{}
parsedMessage := stanza.Message{}
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
@ -38,7 +38,7 @@ func TestDecodeError(t *testing.T) {
</error>
</message>`
parsedMessage := xmpp.Message{}
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message error stanza unmarshall error: %v", err)
return
@ -50,15 +50,15 @@ func TestDecodeError(t *testing.T) {
func TestGetOOB(t *testing.T) {
image := "https://localhost/image.png"
msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost"})
ext := xmpp.OOB{
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
ext := stanza.OOB{
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
URL: image,
}
msg.Extensions = append(msg.Extensions, &ext)
// OOB can properly be found
var oob xmpp.OOB
var oob stanza.OOB
// Try to find and
if ok := msg.Get(&oob); !ok {
t.Error("could not find oob extension")
@ -69,7 +69,7 @@ func TestGetOOB(t *testing.T) {
}
// Markable is not found
var m xmpp.Markable
var m stanza.Markable
if ok := msg.Get(&m); ok {
t.Error("we should not have found markable extension")
}

View file

@ -1,6 +1,8 @@
package xmpp
package stanza
import "encoding/xml"
import (
"encoding/xml"
)
/*
Support for:

View file

@ -1,6 +1,8 @@
package xmpp
package stanza
import "encoding/xml"
import (
"encoding/xml"
)
/*
Support for:

View file

@ -1,6 +1,8 @@
package xmpp
package stanza
import "encoding/xml"
import (
"encoding/xml"
)
type HTML struct {
MsgExtension

View file

@ -1,20 +1,20 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func TestHTMLGen(t *testing.T) {
htmlBody := "<p>Hello <b>World</b></p>"
msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost"})
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
msg.Body = "Hello World"
body := xmpp.HTMLBody{
body := stanza.HTMLBody{
InnerXML: htmlBody,
}
html := xmpp.HTML{Body: body}
html := stanza.HTML{Body: body}
msg.Extensions = append(msg.Extensions, html)
result := msg.XMPPFormat()
@ -23,7 +23,7 @@ func TestHTMLGen(t *testing.T) {
t.Errorf("incorrect serialize message:\n%s", result)
}
parsedMessage := xmpp.Message{}
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message HTML unmarshall error: %v", err)
return
@ -33,7 +33,7 @@ func TestHTMLGen(t *testing.T) {
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
}
var h xmpp.HTML
var h stanza.HTML
if ok := parsedMessage.Get(&h); !ok {
t.Error("could not extract HTML body")
}

View file

@ -1,6 +1,8 @@
package xmpp
package stanza
import "encoding/xml"
import (
"encoding/xml"
)
/*
Support for:

View file

@ -1,6 +1,8 @@
package xmpp
package stanza
import "encoding/xml"
import (
"encoding/xml"
)
/*
Support for:

View file

@ -1,10 +1,10 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func TestDecodeRequest(t *testing.T) {
@ -15,7 +15,7 @@ func TestDecodeRequest(t *testing.T) {
<body>My lord, dispatch; read o'er these articles.</body>
<request xmlns='urn:xmpp:receipts'/>
</message>`
parsedMessage := xmpp.Message{}
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message receipt unmarshall error: %v", err)
return
@ -31,7 +31,7 @@ func TestDecodeRequest(t *testing.T) {
}
switch ext := parsedMessage.Extensions[0].(type) {
case *xmpp.ReceiptRequest:
case *stanza.ReceiptRequest:
if ext.XMLName.Local != "request" {
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
}

51
stanza/node.go Normal file
View file

@ -0,0 +1,51 @@
package stanza
import "encoding/xml"
// ============================================================================
// Generic / unknown content
// Node is a generic structure to represent XML data. It is used to parse
// unreferenced or custom stanza payload.
type Node struct {
XMLName xml.Name
Attrs []xml.Attr `xml:"-"`
Content string `xml:",innerxml"`
Nodes []Node `xml:",any"`
}
func (n *Node) Namespace() string {
return n.XMLName.Space
}
// Attr represents generic XML attributes, as used on the generic XML Node
// representation.
type Attr struct {
K string
V string
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
for _, attr := range start.Attr {
// Do not repeat xmlns, it is already in XMLName
if attr.Name.Local != "xmlns" {
n.Attrs = append(n.Attrs, attr)
}
}
type node Node
return d.DecodeElement((*node)(n), &start)
}
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
// Node structure to XML.
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
start.Attr = n.Attrs
start.Name = n.XMLName
err = e.EncodeToken(start)
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
return e.EncodeToken(xml.EndElement{Name: start.Name})
}

View file

@ -1,11 +1,11 @@
package xmpp
package stanza
const (
NSStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
NSClient = "jabber:client"
NSComponent = "jabber:component:accept"
)

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
type Packet interface {
Name() string

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
type StanzaType string

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"
@ -14,7 +14,7 @@ import (
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
// getting through the authentication process.
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
func initStream(p *xml.Decoder) (sessionID string, err error) {
func InitStream(p *xml.Decoder) (sessionID string, err error) {
for {
var t xml.Token
t, err = p.Token()
@ -41,31 +41,14 @@ func initStream(p *xml.Decoder) (sessionID string, err error) {
}
}
// Scan XML token stream to find next StartElement.
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err == io.EOF {
return xml.StartElement{}, errors.New("connection closed")
}
if err != nil {
return xml.StartElement{}, fmt.Errorf("nextStart %s", err)
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
// nextPacket scans XML token stream for next complete XMPP stanza.
// NextPacket scans XML token stream for next complete XMPP stanza.
// Once the type of stanza has been identified, a structure is created to decode
// that stanza and returned.
// TODO Use an interface to return packets interface xmppDecoder
// TODO make auth and bind use nextPacket instead of directly nextStart
func nextPacket(p *xml.Decoder) (Packet, error) {
// TODO make auth and bind use NextPacket instead of directly NextStart
func NextPacket(p *xml.Decoder) (Packet, error) {
// Read start element to find out how we want to parse the XMPP packet
se, err := nextStart(p)
se, err := NextStart(p)
if err != nil {
return nil, err
}
@ -74,7 +57,7 @@ func nextPacket(p *xml.Decoder) (Packet, error) {
switch se.Name.Space {
case NSStream:
return decodeStream(p, se)
case nsSASL:
case NSSASL:
return decodeSASL(p, se)
case NSClient:
return decodeClient(p, se)
@ -86,6 +69,23 @@ func nextPacket(p *xml.Decoder) (Packet, error) {
}
}
// Scan XML token stream to find next StartElement.
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err == io.EOF {
return xml.StartElement{}, errors.New("connection closed")
}
if err != nil {
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
/*
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
type.

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,10 +1,10 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
// https://xmpp.org/extensions/xep-0045.html#example-27
@ -18,12 +18,12 @@ func TestMucPassword(t *testing.T) {
</x>
</presence>`
var parsedPresence xmpp.Presence
var parsedPresence stanza.Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", str)
}
var muc xmpp.MucPresence
var muc stanza.MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
}
@ -44,13 +44,13 @@ func TestMucHistory(t *testing.T) {
</x>
</presence>`
var parsedPresence xmpp.Presence
var parsedPresence stanza.Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", str, err)
return
}
var muc xmpp.MucPresence
var muc stanza.MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
return
@ -74,14 +74,14 @@ func TestMucNoHistory(t *testing.T) {
maxstanzas := 0
pres := xmpp.Presence{Attrs: xmpp.Attrs{
pres := stanza.Presence{Attrs: stanza.Attrs{
From: "hag66@shakespeare.lit/pda",
Id: "n13mt3l",
To: "coven@chat.shakespeare.lit/thirdwitch",
},
Extensions: []xmpp.PresExtension{
xmpp.MucPresence{
History: xmpp.History{MaxStanzas: xmpp.NewNullableInt(maxstanzas)},
Extensions: []stanza.PresExtension{
stanza.MucPresence{
History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)},
},
},
}

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
// PresenceShow is a Enum of presence element show
type PresenceShow string

View file

@ -1,24 +1,23 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestGeneratePresence(t *testing.T) {
presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = xmpp.PresenceShowChat
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = stanza.PresenceShowChat
data, err := xml.Marshal(presence)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
var parsedPresence xmpp.Presence
var parsedPresence stanza.Presence
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
@ -32,13 +31,13 @@ func TestPresenceSubElt(t *testing.T) {
// Test structure to ensure that show, status and priority are correctly defined as presence
// package sub-elements
type pres struct {
Show xmpp.PresenceShow `xml:"show"`
Status string `xml:"status"`
Priority int8 `xml:"priority"`
Show stanza.PresenceShow `xml:"show"`
Status string `xml:"status"`
Priority int8 `xml:"priority"`
}
presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = xmpp.PresenceShowXA
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = stanza.PresenceShowXA
presence.Status = "Coding"
presence.Priority = 10

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"crypto/tls"
@ -8,7 +8,7 @@ import (
var DefaultTlsConfig tls.Config
// Used during stream initiation / session establishment
type tlsProceed struct {
type TLSProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
}

View file

@ -1,4 +1,4 @@
package xmpp
package stanza
import (
"encoding/xml"

View file

@ -1,4 +1,4 @@
package xmpp_test
package stanza_test
import (
"encoding/xml"
@ -10,6 +10,15 @@ import (
// marshal / unmarshal. There is no need to manage them on the manually
// crafted structure.
func xmlEqual(x, y interface{}) bool {
return cmp.Equal(x, y, xmlOpts())
}
// xmlDiff compares xml structures ignoring namespace preferences
func xmlDiff(x, y interface{}) string {
return cmp.Diff(x, y, xmlOpts())
}
func xmlOpts() cmp.Options {
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
opts := cmp.Options{
cmp.FilterValues(func(x, y interface{}) bool {
@ -20,10 +29,12 @@ func xmlEqual(x, y interface{}) bool {
if xx == zero || yy == zero {
return true
}
if xx.Space == "" || yy.Space == "" {
return true
}
}
return false
}, alwaysEqual),
}
return cmp.Equal(x, y, opts)
return opts
}

View file

@ -6,6 +6,7 @@ import (
"time"
"golang.org/x/xerrors"
"gosrc.io/xmpp/stanza"
)
// The Fluux XMPP lib can manage client or component XMPP streams.
@ -29,7 +30,7 @@ type StreamClient interface {
// Sender is an interface provided by Stream clients to allow sending XMPP data.
type Sender interface {
Send(packet Packet) error
Send(packet stanza.Packet) error
SendRaw(packet string) error
}

View file

@ -4,14 +4,14 @@ import (
"encoding/xml"
"testing"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func TestNoStartTLS(t *testing.T) {
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
</stream:features>`
var parsedSF xmpp.StreamFeatures
var parsedSF stanza.StreamFeatures
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
}
@ -32,7 +32,7 @@ func TestStartTLS(t *testing.T) {
</starttls>
</stream:features>`
var parsedSF xmpp.StreamFeatures
var parsedSF stanza.StreamFeatures
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
}
@ -52,7 +52,7 @@ func TestStreamManagement(t *testing.T) {
<sm xmlns='urn:xmpp:sm:3'/>
</stream:features>`
var parsedSF xmpp.StreamFeatures
var parsedSF stanza.StreamFeatures
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
}