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" "os"
"gosrc.io/xmpp" "gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
) )
func main() { func main() {
@ -57,15 +58,15 @@ func main() {
log.Fatal(cm.Run()) log.Fatal(cm.Run())
} }
func handleMessage(s xmpp.Sender, p xmpp.Packet) { func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(xmpp.Message) msg, ok := p.(stanza.Message)
if !ok { if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p) _, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return return
} }
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From) _, _ = 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) _ = s.Send(reply)
} }
``` ```

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import (
"os" "os"
"gosrc.io/xmpp" "gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
) )
func main() { func main() {
@ -35,15 +36,15 @@ func main() {
log.Fatal(cm.Run()) log.Fatal(cm.Run())
} }
func handleMessage(s xmpp.Sender, p xmpp.Packet) { func handleMessage(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(xmpp.Message) msg, ok := p.(stanza.Message)
if !ok { if !ok {
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p) _, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
return return
} }
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From) _, _ = 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) _ = s.Send(reply)
} }

View file

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

81
auth.go
View file

@ -6,9 +6,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "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 // TODO: Implement other type of SASL Authentication
havePlain := false havePlain := false
for _, m := range f.Mechanisms.Mechanism { 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 raw := "\x00" + user + "\x00" + password
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
base64.StdEncoding.Encode(enc, []byte(raw)) base64.StdEncoding.Encode(enc, []byte(raw))
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='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. // Next message should be either success or failure.
val, err := nextPacket(decoder) val, err := stanza.NextPacket(decoder)
if err != nil { if err != nil {
return err return err
} }
switch v := val.(type) { switch v := val.(type) {
case SASLSuccess: case stanza.SASLSuccess:
case SASLFailure: case stanza.SASLFailure:
// v.Any is type of sub-element in failure, which gives a description of what failed. // v.Any is type of sub-element in failure, which gives a description of what failed.
err := errors.New("auth failure: " + v.Any.Local) err := errors.New("auth failure: " + v.Any.Local)
return NewConnError(err, true) return NewConnError(err, true)
@ -49,72 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
} }
return err 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" "net"
"strings" "strings"
"time" "time"
"gosrc.io/xmpp/stanza"
) )
// TODO: Should I move this as an extension of the client? // TODO: Should I move this as an extension of the client?
@ -49,28 +51,28 @@ func (c *ServerCheck) Check() error {
decoder := xml.NewDecoder(tcpconn) decoder := xml.NewDecoder(tcpconn)
// Send stream open tag // 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 return err
} }
// Set xml decoder and extract streamID from reply (not used for now) // Set xml decoder and extract streamID from reply (not used for now)
_, err = initStream(decoder) _, err = stanza.InitStream(decoder)
if err != nil { if err != nil {
return err return err
} }
// extract stream features // extract stream features
var f StreamFeatures var f stanza.StreamFeatures
packet, err := nextPacket(decoder) packet, err := stanza.NextPacket(decoder)
if err != nil { if err != nil {
err = fmt.Errorf("stream open decode features: %s", err) err = fmt.Errorf("stream open decode features: %s", err)
return err return err
} }
switch p := packet.(type) { switch p := packet.(type) {
case StreamFeatures: case stanza.StreamFeatures:
f = p f = p
case StreamError: case stanza.StreamError:
return errors.New("open stream error: " + p.Error.Local) return errors.New("open stream error: " + p.Error.Local)
default: default:
return errors.New("expected packet received while expecting features, got " + p.Name()) 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 { if _, ok := f.DoesStartTLS(); ok {
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>") 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 { if err = decoder.DecodeElement(&k, nil); err != nil {
return fmt.Errorf("expecting starttls proceed: %s", err) return fmt.Errorf("expecting starttls proceed: %s", err)
} }
DefaultTlsConfig.ServerName = c.domain stanza.DefaultTlsConfig.ServerName = c.domain
tlsConn := tls.Client(tcpconn, &DefaultTlsConfig) tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
// 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

View file

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"net" "net"
"time" "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. // 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 conn := c.conn
if conn == nil { if conn == nil {
return errors.New("client is not connected") return errors.New("client is not connected")
@ -191,7 +193,7 @@ func (c *Client) SendRaw(packet string) error {
// Loop: Receive data from server // Loop: Receive data from server
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) { func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
for { for {
val, err := nextPacket(c.Session.decoder) val, err := stanza.NextPacket(c.Session.decoder)
if err != nil { if err != nil {
close(keepaliveQuit) close(keepaliveQuit)
c.updateState(StateDisconnected) c.updateState(StateDisconnected)
@ -200,7 +202,7 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
// Handle stream errors // Handle stream errors
switch packet := val.(type) { switch packet := val.(type) {
case StreamError: case stanza.StreamError:
c.router.route(c, val) c.router.route(c, val)
close(keepaliveQuit) close(keepaliveQuit)
c.streamError(packet.Error.Local, packet.Text) c.streamError(packet.Error.Local, packet.Text)

View file

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

View file

@ -9,6 +9,8 @@ import (
"io" "io"
"net" "net"
"time" "time"
"gosrc.io/xmpp/stanza"
) )
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>" 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 c.conn = conn
// 1. Send stream open tag // 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()) return errors.New("cannot send stream open " + err.Error())
} }
c.decoder = xml.NewDecoder(conn) c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply // 2. Initialize xml decoder and extract streamID from reply
streamId, err := initStream(c.decoder) streamId, err := stanza.InitStream(c.decoder)
if err != nil { if err != nil {
return errors.New("cannot init decoder " + err.Error()) return errors.New("cannot init decoder " + err.Error())
} }
@ -89,15 +91,15 @@ func (c *Component) Connect() error {
} }
// 4. Check server response for authentication // 4. Check server response for authentication
val, err := nextPacket(c.decoder) val, err := stanza.NextPacket(c.decoder)
if err != nil { if err != nil {
return err return err
} }
switch v := val.(type) { switch v := val.(type) {
case StreamError: case stanza.StreamError:
return errors.New("handshake failed " + v.Error.Local) return errors.New("handshake failed " + v.Error.Local)
case Handshake: case stanza.Handshake:
// Start the receiver go routine // Start the receiver go routine
go c.recv() go c.recv()
return nil return nil
@ -119,7 +121,7 @@ func (c *Component) SetHandler(handler EventHandler) {
// Receiver Go routine receiver // Receiver Go routine receiver
func (c *Component) recv() (err error) { func (c *Component) recv() (err error) {
for { for {
val, err := nextPacket(c.decoder) val, err := stanza.NextPacket(c.decoder)
if err != nil { if err != nil {
c.updateState(StateDisconnected) c.updateState(StateDisconnected)
return err return err
@ -127,7 +129,7 @@ func (c *Component) recv() (err error) {
// Handle stream errors // Handle stream errors
switch p := val.(type) { switch p := val.(type) {
case StreamError: case stanza.StreamError:
c.router.route(c, val) c.router.route(c, val)
c.streamError(p.Error.Local, p.Text) c.streamError(p.Error.Local, p.Text)
return errors.New("stream error: " + p.Error.Local) 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. // 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 conn := c.conn
if conn == nil { if conn == nil {
return errors.New("component is not connected") return errors.New("component is not connected")
@ -186,90 +188,6 @@ func (c *Component) handshake(streamId string) string {
return encodedStr 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: Add support for discovery management directly in component
TODO: Support multiple identities on disco info TODO: Support multiple identities on disco info

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
package xmpp_test package stanza_test
import ( import (
"encoding/xml" "encoding/xml"
"testing" "testing"
"gosrc.io/xmpp" "gosrc.io/xmpp/stanza"
) )
func TestDecodeRequest(t *testing.T) { func TestDecodeRequest(t *testing.T) {
@ -15,7 +15,7 @@ func TestDecodeRequest(t *testing.T) {
<body>My lord, dispatch; read o'er these articles.</body> <body>My lord, dispatch; read o'er these articles.</body>
<request xmlns='urn:xmpp:receipts'/> <request xmlns='urn:xmpp:receipts'/>
</message>` </message>`
parsedMessage := xmpp.Message{} parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message receipt unmarshall error: %v", err) t.Errorf("message receipt unmarshall error: %v", err)
return return
@ -31,7 +31,7 @@ func TestDecodeRequest(t *testing.T) {
} }
switch ext := parsedMessage.Extensions[0].(type) { switch ext := parsedMessage.Extensions[0].(type) {
case *xmpp.ReceiptRequest: case *stanza.ReceiptRequest:
if ext.XMLName.Local != "request" { if ext.XMLName.Local != "request" {
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local) 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 ( const (
NSStream = "http://etherx.jabber.org/streams" NSStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind" NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsSession = "urn:ietf:params:xml:ns:xmpp-session" NSSession = "urn:ietf:params:xml:ns:xmpp-session"
NSClient = "jabber:client" NSClient = "jabber:client"
NSComponent = "jabber:component:accept" NSComponent = "jabber:component:accept"
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package xmpp package stanza
import ( import (
"crypto/tls" "crypto/tls"
@ -8,7 +8,7 @@ import (
var DefaultTlsConfig tls.Config 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"`
} }

View file

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

View file

@ -1,4 +1,4 @@
package xmpp_test package stanza_test
import ( import (
"encoding/xml" "encoding/xml"
@ -10,6 +10,15 @@ import (
// 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.
func xmlEqual(x, y interface{}) bool { 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 }) alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
opts := cmp.Options{ opts := cmp.Options{
cmp.FilterValues(func(x, y interface{}) bool { cmp.FilterValues(func(x, y interface{}) bool {
@ -20,10 +29,12 @@ func xmlEqual(x, y interface{}) bool {
if xx == zero || yy == zero { if xx == zero || yy == zero {
return true return true
} }
if xx.Space == "" || yy.Space == "" {
return true
}
} }
return false return false
}, alwaysEqual), }, alwaysEqual),
} }
return opts
return cmp.Equal(x, y, opts)
} }

View file

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

View file

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