forked from jshiffer/go-xmpp
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:
commit
0fd1bb2483
@ -32,6 +32,7 @@ import (
|
||||
"os"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -57,15 +58,15 @@ func main() {
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func handleMessage(s xmpp.Sender, p xmpp.Packet) {
|
||||
msg, ok := p.(xmpp.Message)
|
||||
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||
reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body}
|
||||
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||
_ = s.Send(reply)
|
||||
}
|
||||
```
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -23,8 +24,8 @@ func main() {
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
router.NewRoute().
|
||||
IQNamespaces(xmpp.NSDiscoInfo).
|
||||
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||
IQNamespaces(stanza.NSDiscoInfo).
|
||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||
discoInfo(s, p, opts)
|
||||
})
|
||||
router.NewRoute().
|
||||
@ -43,14 +44,14 @@ func main() {
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func handleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
||||
msg, ok := p.(xmpp.Message)
|
||||
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var msgProcessed bool
|
||||
for _, ext := range msg.Extensions {
|
||||
delegation, ok := ext.(*xmpp.Delegation)
|
||||
delegation, ok := ext.(*stanza.Delegation)
|
||||
if ok {
|
||||
msgProcessed = true
|
||||
fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace)
|
||||
@ -72,18 +73,18 @@ const (
|
||||
// TODO: replace xmpp.Sender by ctx xmpp.Context ?
|
||||
// ctx.Stream.Send / SendRaw
|
||||
// ctx.Opts
|
||||
func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(xmpp.IQ)
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
info, ok := iq.Payload.(*xmpp.DiscoInfo)
|
||||
info, ok := iq.Payload.(*stanza.DiscoInfo)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
|
||||
switch info.Node {
|
||||
case "":
|
||||
@ -97,22 +98,22 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
|
||||
func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
|
||||
func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) {
|
||||
// Higher level discovery
|
||||
identity := xmpp.Identity{
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
Category: opts.Category,
|
||||
Type: opts.Type,
|
||||
}
|
||||
payload := xmpp.DiscoInfo{
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: xmpp.NSDiscoInfo,
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Identity: identity,
|
||||
Features: []xmpp.Feature{
|
||||
{Var: xmpp.NSDiscoInfo},
|
||||
{Var: xmpp.NSDiscoItems},
|
||||
Identity: []stanza.Identity{identity},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
{Var: "jabber:iq:version"},
|
||||
{Var: "urn:xmpp:delegation:1"},
|
||||
},
|
||||
@ -120,14 +121,14 @@ func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
|
||||
iqResp.Payload = &payload
|
||||
}
|
||||
|
||||
func discoInfoPubSub(iqResp *xmpp.IQ) {
|
||||
payload := xmpp.DiscoInfo{
|
||||
func discoInfoPubSub(iqResp *stanza.IQ) {
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: xmpp.NSDiscoInfo,
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Node: pubsubNode,
|
||||
Features: []xmpp.Feature{
|
||||
Features: []stanza.Feature{
|
||||
{Var: "http://jabber.org/protocol/pubsub"},
|
||||
{Var: "http://jabber.org/protocol/pubsub#publish"},
|
||||
{Var: "http://jabber.org/protocol/pubsub#subscribe"},
|
||||
@ -137,19 +138,19 @@ func discoInfoPubSub(iqResp *xmpp.IQ) {
|
||||
iqResp.Payload = &payload
|
||||
}
|
||||
|
||||
func discoInfoPEP(iqResp *xmpp.IQ) {
|
||||
identity := xmpp.Identity{
|
||||
func discoInfoPEP(iqResp *stanza.IQ) {
|
||||
identity := stanza.Identity{
|
||||
Category: "pubsub",
|
||||
Type: "pep",
|
||||
}
|
||||
payload := xmpp.DiscoInfo{
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: xmpp.NSDiscoInfo,
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Identity: identity,
|
||||
Identity: []stanza.Identity{identity},
|
||||
Node: pepNode,
|
||||
Features: []xmpp.Feature{
|
||||
Features: []stanza.Feature{
|
||||
{Var: "http://jabber.org/protocol/pubsub#access-presence"},
|
||||
{Var: "http://jabber.org/protocol/pubsub#auto-create"},
|
||||
{Var: "http://jabber.org/protocol/pubsub#auto-subscribe"},
|
||||
@ -166,25 +167,25 @@ func discoInfoPEP(iqResp *xmpp.IQ) {
|
||||
iqResp.Payload = &payload
|
||||
}
|
||||
|
||||
func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||
func handleDelegation(s xmpp.Sender, p stanza.Packet) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(xmpp.IQ)
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delegation, ok := iq.Payload.(*xmpp.Delegation)
|
||||
delegation, ok := iq.Payload.(*stanza.Delegation)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
forwardedPacket := delegation.Forwarded.Stanza
|
||||
fmt.Println(forwardedPacket)
|
||||
forwardedIQ, ok := forwardedPacket.(xmpp.IQ)
|
||||
forwardedIQ, ok := forwardedPacket.(stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
pubsub, ok := forwardedIQ.Payload.(*xmpp.PubSub)
|
||||
pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub)
|
||||
if !ok {
|
||||
// We only support pubsub delegation
|
||||
return
|
||||
@ -192,8 +193,8 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||
|
||||
if pubsub.Publish.XMLName.Local == "publish" {
|
||||
// Prepare pubsub IQ reply
|
||||
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
|
||||
payload := xmpp.PubSub{
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id})
|
||||
payload := stanza.PubSub{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://jabber.org/protocol/pubsub",
|
||||
Local: "pubsub",
|
||||
@ -201,13 +202,13 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
|
||||
}
|
||||
iqResp.Payload = &payload
|
||||
// Wrap the reply in delegation 'forward'
|
||||
iqForward := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
delegPayload := xmpp.Delegation{
|
||||
iqForward := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
||||
delegPayload := stanza.Delegation{
|
||||
XMLName: xml.Name{
|
||||
Space: "urn:xmpp:delegation:1",
|
||||
Local: "delegation",
|
||||
},
|
||||
Forwarded: &xmpp.Forwarded{
|
||||
Forwarded: &stanza.Forwarded{
|
||||
XMLName: xml.Name{
|
||||
Space: "urn:xmpp:forward:0",
|
||||
Local: "forward",
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -21,12 +22,12 @@ func main() {
|
||||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
router.NewRoute().
|
||||
IQNamespaces(xmpp.NSDiscoInfo).
|
||||
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||
IQNamespaces(stanza.NSDiscoInfo).
|
||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||
discoInfo(s, p, opts)
|
||||
})
|
||||
router.NewRoute().
|
||||
IQNamespaces(xmpp.NSDiscoItems).
|
||||
IQNamespaces(stanza.NSDiscoItems).
|
||||
HandlerFunc(discoItems)
|
||||
router.NewRoute().
|
||||
IQNamespaces("jabber:iq:version").
|
||||
@ -44,36 +45,36 @@ func main() {
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func handleMessage(_ xmpp.Sender, p xmpp.Packet) {
|
||||
msg, ok := p.(xmpp.Message)
|
||||
func handleMessage(_ xmpp.Sender, p stanza.Packet) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
fmt.Println("Received message:", msg.Body)
|
||||
}
|
||||
|
||||
func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||
func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(xmpp.IQ)
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok || iq.Type != "get" {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
identity := xmpp.Identity{
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
Category: opts.Category,
|
||||
Type: opts.Type,
|
||||
}
|
||||
payload := xmpp.DiscoInfo{
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: xmpp.NSDiscoInfo,
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Identity: identity,
|
||||
Features: []xmpp.Feature{
|
||||
{Var: xmpp.NSDiscoInfo},
|
||||
{Var: xmpp.NSDiscoItems},
|
||||
Identity: []stanza.Identity{identity},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
{Var: "jabber:iq:version"},
|
||||
{Var: "urn:xmpp:delegation:1"},
|
||||
},
|
||||
@ -83,24 +84,24 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
|
||||
}
|
||||
|
||||
// TODO: Handle iq error responses
|
||||
func discoItems(c xmpp.Sender, p xmpp.Packet) {
|
||||
func discoItems(c xmpp.Sender, p stanza.Packet) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(xmpp.IQ)
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok || iq.Type != "get" {
|
||||
return
|
||||
}
|
||||
|
||||
discoItems, ok := iq.Payload.(*xmpp.DiscoItems)
|
||||
discoItems, ok := iq.Payload.(*stanza.DiscoItems)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
|
||||
var payload xmpp.DiscoItems
|
||||
var payload stanza.DiscoItems
|
||||
if discoItems.Node == "" {
|
||||
payload = xmpp.DiscoItems{
|
||||
Items: []xmpp.DiscoItem{
|
||||
payload = stanza.DiscoItems{
|
||||
Items: []stanza.DiscoItem{
|
||||
{Name: "test node", JID: "service.localhost", Node: "node1"},
|
||||
},
|
||||
}
|
||||
@ -109,15 +110,15 @@ func discoItems(c xmpp.Sender, p xmpp.Packet) {
|
||||
_ = c.Send(iqResp)
|
||||
}
|
||||
|
||||
func handleVersion(c xmpp.Sender, p xmpp.Packet) {
|
||||
func handleVersion(c xmpp.Sender, p stanza.Packet) {
|
||||
// Type conversion & sanity checks
|
||||
iq, ok := p.(xmpp.IQ)
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
var payload xmpp.Version
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
var payload stanza.Version
|
||||
payload.Name = "Fluux XMPP Component"
|
||||
payload.Version = "0.0.1"
|
||||
iq.Payload = &payload
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -35,15 +36,15 @@ func main() {
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func handleMessage(s xmpp.Sender, p xmpp.Packet) {
|
||||
msg, ok := p.(xmpp.Message)
|
||||
func handleMessage(s xmpp.Sender, p stanza.Packet) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
|
||||
reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body}
|
||||
reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body}
|
||||
_ = s.Send(reply)
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/processone/mpg123"
|
||||
"github.com/processone/soundcloud"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player.
|
||||
@ -41,12 +42,12 @@ func main() {
|
||||
router := xmpp.NewRouter()
|
||||
router.NewRoute().
|
||||
Packet("message").
|
||||
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||
handleMessage(s, p, player)
|
||||
})
|
||||
router.NewRoute().
|
||||
Packet("message").
|
||||
HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
|
||||
HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
||||
handleIQ(s, p, player)
|
||||
})
|
||||
|
||||
@ -59,8 +60,8 @@ func main() {
|
||||
log.Fatal(cm.Run())
|
||||
}
|
||||
|
||||
func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||
msg, ok := p.(xmpp.Message)
|
||||
func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||
msg, ok := p.(stanza.Message)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@ -73,14 +74,14 @@ func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||
iq, ok := p.(xmpp.IQ)
|
||||
func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) {
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
switch payload := iq.Payload.(type) {
|
||||
// We support IOT Control IQ
|
||||
case *xmpp.ControlSet:
|
||||
case *stanza.ControlSet:
|
||||
var url string
|
||||
for _, element := range payload.Fields {
|
||||
if element.XMLName.Local == "string" && element.Name == "url" {
|
||||
@ -90,9 +91,9 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||
}
|
||||
|
||||
playSCURL(player, url)
|
||||
setResponse := new(xmpp.ControlSetResponse)
|
||||
setResponse := new(stanza.ControlSetResponse)
|
||||
// FIXME: Broken
|
||||
reply := xmpp.IQ{Attrs: xmpp.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
|
||||
reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse}
|
||||
_ = s.Send(reply)
|
||||
// TODO add Soundclound artist / title retrieval
|
||||
sendUserTune(s, "Radiohead", "Spectre")
|
||||
@ -102,9 +103,9 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
|
||||
}
|
||||
|
||||
func sendUserTune(s xmpp.Sender, artist string, title string) {
|
||||
tune := xmpp.Tune{Artist: artist, Title: title}
|
||||
iq := xmpp.NewIQ(xmpp.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
|
||||
payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
|
||||
tune := stanza.Tune{Artist: artist, Title: title}
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"})
|
||||
payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}}
|
||||
iq.Payload = &payload
|
||||
_ = s.Send(iq)
|
||||
}
|
||||
|
81
auth.go
81
auth.go
@ -6,9 +6,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f StreamFeatures, user string, password string) (err error) {
|
||||
func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) {
|
||||
// TODO: Implement other type of SASL Authentication
|
||||
havePlain := false
|
||||
for _, m := range f.Mechanisms.Mechanism {
|
||||
@ -30,17 +32,17 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
||||
raw := "\x00" + user + "\x00" + password
|
||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
|
||||
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", stanza.NSSASL, enc)
|
||||
|
||||
// Next message should be either success or failure.
|
||||
val, err := nextPacket(decoder)
|
||||
val, err := stanza.NextPacket(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case SASLSuccess:
|
||||
case SASLFailure:
|
||||
case stanza.SASLSuccess:
|
||||
case stanza.SASLFailure:
|
||||
// v.Any is type of sub-element in failure, which gives a description of what failed.
|
||||
err := errors.New("auth failure: " + v.Any.Local)
|
||||
return NewConnError(err, true)
|
||||
@ -49,72 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SASLSuccess
|
||||
|
||||
type SASLSuccess struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
||||
}
|
||||
|
||||
func (SASLSuccess) Name() string {
|
||||
return "sasl:success"
|
||||
}
|
||||
|
||||
type saslSuccessDecoder struct{}
|
||||
|
||||
var saslSuccess saslSuccessDecoder
|
||||
|
||||
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
|
||||
var packet SASLSuccess
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SASLFailure
|
||||
|
||||
type SASLFailure struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
||||
Any xml.Name // error reason is a subelement
|
||||
}
|
||||
|
||||
func (SASLFailure) Name() string {
|
||||
return "sasl:failure"
|
||||
}
|
||||
|
||||
type saslFailureDecoder struct{}
|
||||
|
||||
var saslFailure saslFailureDecoder
|
||||
|
||||
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
|
||||
var packet SASLFailure
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
type auth struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
||||
Mechanism string `xml:"mecanism,attr"`
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type BindBind struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
||||
Resource string `xml:"resource,omitempty"`
|
||||
Jid string `xml:"jid,omitempty"`
|
||||
}
|
||||
|
||||
func (b *BindBind) Namespace() string {
|
||||
return b.XMLName.Space
|
||||
}
|
||||
|
||||
// Session is obsolete in RFC 6121.
|
||||
// Added for compliance with RFC 3121.
|
||||
// Remove when ejabberd purely conforms to RFC 6121.
|
||||
type sessionSession struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
||||
optional xml.Name // If it does exist, it mean we are not required to open session
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// TODO: Should I move this as an extension of the client?
|
||||
@ -49,28 +51,28 @@ func (c *ServerCheck) Check() error {
|
||||
decoder := xml.NewDecoder(tcpconn)
|
||||
|
||||
// Send stream open tag
|
||||
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, NSClient, NSStream); err != nil {
|
||||
if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set xml decoder and extract streamID from reply (not used for now)
|
||||
_, err = initStream(decoder)
|
||||
_, err = stanza.InitStream(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// extract stream features
|
||||
var f StreamFeatures
|
||||
packet, err := nextPacket(decoder)
|
||||
var f stanza.StreamFeatures
|
||||
packet, err := stanza.NextPacket(decoder)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("stream open decode features: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
switch p := packet.(type) {
|
||||
case StreamFeatures:
|
||||
case stanza.StreamFeatures:
|
||||
f = p
|
||||
case StreamError:
|
||||
case stanza.StreamError:
|
||||
return errors.New("open stream error: " + p.Error.Local)
|
||||
default:
|
||||
return errors.New("expected packet received while expecting features, got " + p.Name())
|
||||
@ -79,13 +81,13 @@ func (c *ServerCheck) Check() error {
|
||||
if _, ok := f.DoesStartTLS(); ok {
|
||||
fmt.Fprintf(tcpconn, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||
|
||||
var k tlsProceed
|
||||
var k stanza.TLSProceed
|
||||
if err = decoder.DecodeElement(&k, nil); err != nil {
|
||||
return fmt.Errorf("expecting starttls proceed: %s", err)
|
||||
}
|
||||
|
||||
DefaultTlsConfig.ServerName = c.domain
|
||||
tlsConn := tls.Client(tcpconn, &DefaultTlsConfig)
|
||||
stanza.DefaultTlsConfig.ServerName = c.domain
|
||||
tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig)
|
||||
// We convert existing connection to TLS
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
return err
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
//=============================================================================
|
||||
@ -153,7 +155,7 @@ func (c *Client) SetHandler(handler EventHandler) {
|
||||
}
|
||||
|
||||
// Send marshals XMPP stanza and sends it to the server.
|
||||
func (c *Client) Send(packet Packet) error {
|
||||
func (c *Client) Send(packet stanza.Packet) error {
|
||||
conn := c.conn
|
||||
if conn == nil {
|
||||
return errors.New("client is not connected")
|
||||
@ -191,7 +193,7 @@ func (c *Client) SendRaw(packet string) error {
|
||||
// Loop: Receive data from server
|
||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
||||
for {
|
||||
val, err := nextPacket(c.Session.decoder)
|
||||
val, err := stanza.NextPacket(c.Session.decoder)
|
||||
if err != nil {
|
||||
close(keepaliveQuit)
|
||||
c.updateState(StateDisconnected)
|
||||
@ -200,7 +202,7 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
|
||||
|
||||
// Handle stream errors
|
||||
switch packet := val.(type) {
|
||||
case StreamError:
|
||||
case stanza.StreamError:
|
||||
c.router.route(c, val)
|
||||
close(keepaliveQuit)
|
||||
c.streamError(packet.Error.Local, packet.Text)
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -126,11 +128,11 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||
switch elem := token.(type) {
|
||||
// Wait for first startElement
|
||||
case xml.StartElement:
|
||||
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
|
||||
if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" {
|
||||
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
|
||||
return
|
||||
}
|
||||
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", NSClient, NSStream); err != nil {
|
||||
if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil {
|
||||
t.Errorf("cannot write server stream open: %s", err)
|
||||
}
|
||||
return
|
||||
@ -152,14 +154,14 @@ func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
||||
|
||||
// TODO return err in case of error reading the auth params
|
||||
func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
||||
se, err := nextStart(decoder)
|
||||
se, err := stanza.NextStart(decoder)
|
||||
if err != nil {
|
||||
t.Errorf("cannot read auth: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var nv interface{}
|
||||
nv = &auth{}
|
||||
nv = &stanza.Auth{}
|
||||
// Decode element into pointer storage
|
||||
if err = decoder.DecodeElement(nv, &se); err != nil {
|
||||
t.Errorf("cannot decode auth: %s", err)
|
||||
@ -167,7 +169,7 @@ func readAuth(t *testing.T, decoder *xml.Decoder) string {
|
||||
}
|
||||
|
||||
switch v := nv.(type) {
|
||||
case *auth:
|
||||
case *stanza.Auth:
|
||||
return v.Value
|
||||
}
|
||||
return ""
|
||||
@ -184,13 +186,13 @@ func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) {
|
||||
}
|
||||
|
||||
func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||
se, err := nextStart(decoder)
|
||||
se, err := stanza.NextStart(decoder)
|
||||
if err != nil {
|
||||
t.Errorf("cannot read bind: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
iq := &IQ{}
|
||||
iq := &stanza.IQ{}
|
||||
// Decode element into pointer storage
|
||||
if err = decoder.DecodeElement(&iq, &se); err != nil {
|
||||
t.Errorf("cannot decode bind iq: %s", err)
|
||||
@ -199,7 +201,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
|
||||
|
||||
// TODO Check all elements
|
||||
switch iq.Payload.(type) {
|
||||
case *BindBind:
|
||||
case *stanza.BindBind:
|
||||
result := `<iq id='%s' type='result'>
|
||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
|
||||
<jid>%s</jid>
|
||||
|
102
component.go
102
component.go
@ -9,6 +9,8 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
const componentStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s'>"
|
||||
@ -72,13 +74,13 @@ func (c *Component) Connect() error {
|
||||
c.conn = conn
|
||||
|
||||
// 1. Send stream open tag
|
||||
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, NSComponent, NSStream); err != nil {
|
||||
if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil {
|
||||
return errors.New("cannot send stream open " + err.Error())
|
||||
}
|
||||
c.decoder = xml.NewDecoder(conn)
|
||||
|
||||
// 2. Initialize xml decoder and extract streamID from reply
|
||||
streamId, err := initStream(c.decoder)
|
||||
streamId, err := stanza.InitStream(c.decoder)
|
||||
if err != nil {
|
||||
return errors.New("cannot init decoder " + err.Error())
|
||||
}
|
||||
@ -89,15 +91,15 @@ func (c *Component) Connect() error {
|
||||
}
|
||||
|
||||
// 4. Check server response for authentication
|
||||
val, err := nextPacket(c.decoder)
|
||||
val, err := stanza.NextPacket(c.decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := val.(type) {
|
||||
case StreamError:
|
||||
case stanza.StreamError:
|
||||
return errors.New("handshake failed " + v.Error.Local)
|
||||
case Handshake:
|
||||
case stanza.Handshake:
|
||||
// Start the receiver go routine
|
||||
go c.recv()
|
||||
return nil
|
||||
@ -119,7 +121,7 @@ func (c *Component) SetHandler(handler EventHandler) {
|
||||
// Receiver Go routine receiver
|
||||
func (c *Component) recv() (err error) {
|
||||
for {
|
||||
val, err := nextPacket(c.decoder)
|
||||
val, err := stanza.NextPacket(c.decoder)
|
||||
if err != nil {
|
||||
c.updateState(StateDisconnected)
|
||||
return err
|
||||
@ -127,7 +129,7 @@ func (c *Component) recv() (err error) {
|
||||
|
||||
// Handle stream errors
|
||||
switch p := val.(type) {
|
||||
case StreamError:
|
||||
case stanza.StreamError:
|
||||
c.router.route(c, val)
|
||||
c.streamError(p.Error.Local, p.Text)
|
||||
return errors.New("stream error: " + p.Error.Local)
|
||||
@ -137,7 +139,7 @@ func (c *Component) recv() (err error) {
|
||||
}
|
||||
|
||||
// Send marshalls XMPP stanza and sends it to the server.
|
||||
func (c *Component) Send(packet Packet) error {
|
||||
func (c *Component) Send(packet stanza.Packet) error {
|
||||
conn := c.conn
|
||||
if conn == nil {
|
||||
return errors.New("component is not connected")
|
||||
@ -186,90 +188,6 @@ func (c *Component) handshake(streamId string) string {
|
||||
return encodedStr
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Handshake Stanza
|
||||
|
||||
// Handshake is a stanza used by XMPP components to authenticate on XMPP
|
||||
// component port.
|
||||
type Handshake struct {
|
||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||
// TODO Add handshake value with test for proper serialization
|
||||
// Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (Handshake) Name() string {
|
||||
return "component:handshake"
|
||||
}
|
||||
|
||||
// Handshake decoding wrapper
|
||||
|
||||
type handshakeDecoder struct{}
|
||||
|
||||
var handshake handshakeDecoder
|
||||
|
||||
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
|
||||
var packet Handshake
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component delegation
|
||||
// XEP-0355
|
||||
|
||||
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
|
||||
// depending on the context.
|
||||
type Delegation struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
||||
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
||||
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
||||
}
|
||||
|
||||
func (d *Delegation) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
type Forwarded struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
|
||||
Stanza Packet
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Check subelements to extract required field as boolean
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
if packet, err := decodeClient(d, tt); err == nil {
|
||||
f.Stanza = packet
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Delegated struct {
|
||||
XMLName xml.Name `xml:"delegated"`
|
||||
Namespace string `xml:"namespace,attr,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Add support for discovery management directly in component
|
||||
TODO: Support multiple identities on disco info
|
||||
|
@ -1,7 +1,6 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -24,76 +23,3 @@ func TestHandshake(t *testing.T) {
|
||||
func TestGenerateHandshake(t *testing.T) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// We should be able to properly parse delegation confirmation messages
|
||||
func TestParsingDelegationMessage(t *testing.T) {
|
||||
packetStr := `<message to='service.localhost' from='localhost'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<delegated namespace='http://jabber.org/protocol/pubsub'/>
|
||||
</delegation>
|
||||
</message>`
|
||||
var msg Message
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &msg); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as MsgExtension
|
||||
var nsDelegated string
|
||||
for _, ext := range msg.Extensions {
|
||||
if delegation, ok := ext.(*Delegation); ok {
|
||||
nsDelegated = delegation.Delegated.Namespace
|
||||
}
|
||||
}
|
||||
if nsDelegated != "http://jabber.org/protocol/pubsub" {
|
||||
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we can parse a delegation IQ.
|
||||
// The most important thing is to be able to
|
||||
func TestParsingDelegationIQ(t *testing.T) {
|
||||
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<publish node='http://jabber.org/protocol/mood'>
|
||||
<item id='current'>
|
||||
<mood xmlns='http://jabber.org/protocol/mood'>
|
||||
<excited/>
|
||||
</mood>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>
|
||||
</forwarded>
|
||||
</delegation>
|
||||
</iq>`
|
||||
var iq IQ
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &iq); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as IQPayload
|
||||
var node string
|
||||
if iq.Payload != nil {
|
||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||
packet := delegation.Forwarded.Stanza
|
||||
forwardedIQ, ok := packet.(IQ)
|
||||
if !ok {
|
||||
t.Errorf("Could not extract packet IQ")
|
||||
return
|
||||
}
|
||||
if forwardedIQ.Payload != nil {
|
||||
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
|
||||
node = pubsub.Publish.Node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if node != "http://jabber.org/protocol/mood" {
|
||||
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
|
||||
}
|
||||
}
|
||||
|
254
iq.go
254
iq.go
@ -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{})
|
||||
}
|
48
router.go
48
router.go
@ -3,6 +3,8 @@ package xmpp
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -32,7 +34,7 @@ func NewRouter() *Router {
|
||||
|
||||
// route is called by the XMPP client to dispatch stanza received using the set up routes.
|
||||
// It is also used by test, but is not supposed to be used directly by users of the library.
|
||||
func (r *Router) route(s Sender, p Packet) {
|
||||
func (r *Router) route(s Sender, p stanza.Packet) {
|
||||
|
||||
var match RouteMatch
|
||||
if r.Match(p, &match) {
|
||||
@ -41,15 +43,15 @@ func (r *Router) route(s Sender, p Packet) {
|
||||
return
|
||||
}
|
||||
// If there is no match and we receive an iq set or get, we need to send a reply
|
||||
if iq, ok := p.(IQ); ok {
|
||||
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
|
||||
if iq, ok := p.(stanza.IQ); ok {
|
||||
if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet {
|
||||
iqNotImplemented(s, iq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func iqNotImplemented(s Sender, iq IQ) {
|
||||
err := Err{
|
||||
func iqNotImplemented(s Sender, iq stanza.IQ) {
|
||||
err := stanza.Err{
|
||||
XMLName: xml.Name{Local: "error"},
|
||||
Code: 501,
|
||||
Type: "cancel",
|
||||
@ -66,7 +68,7 @@ func (r *Router) NewRoute() *Route {
|
||||
return route
|
||||
}
|
||||
|
||||
func (r *Router) Match(p Packet, match *RouteMatch) bool {
|
||||
func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
for _, route := range r.routes {
|
||||
if route.Match(p, match) {
|
||||
return true
|
||||
@ -83,14 +85,14 @@ func (r *Router) Handle(name string, handler Handler) *Route {
|
||||
|
||||
// HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence)
|
||||
// See Route.Path() and Route.HandlerFunc().
|
||||
func (r *Router) HandleFunc(name string, f func(s Sender, p Packet)) *Route {
|
||||
func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Route {
|
||||
return r.NewRoute().Packet(name).HandlerFunc(f)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Route
|
||||
type Handler interface {
|
||||
HandlePacket(s Sender, p Packet)
|
||||
HandlePacket(s Sender, p stanza.Packet)
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
@ -108,10 +110,10 @@ func (r *Route) Handler(handler Handler) *Route {
|
||||
// ordinary functions as XMPP handlers. If f is a function
|
||||
// with the appropriate signature, HandlerFunc(f) is a
|
||||
// Handler that calls f.
|
||||
type HandlerFunc func(s Sender, p Packet)
|
||||
type HandlerFunc func(s Sender, p stanza.Packet)
|
||||
|
||||
// HandlePacket calls f(s, p)
|
||||
func (f HandlerFunc) HandlePacket(s Sender, p Packet) {
|
||||
func (f HandlerFunc) HandlePacket(s Sender, p stanza.Packet) {
|
||||
f(s, p)
|
||||
}
|
||||
|
||||
@ -126,7 +128,7 @@ func (r *Route) addMatcher(m matcher) *Route {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Route) Match(p Packet, match *RouteMatch) bool {
|
||||
func (r *Route) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
for _, m := range r.matchers {
|
||||
if matched := m.Match(p, match); !matched {
|
||||
return false
|
||||
@ -144,18 +146,18 @@ func (r *Route) Match(p Packet, match *RouteMatch) bool {
|
||||
|
||||
type nameMatcher string
|
||||
|
||||
func (n nameMatcher) Match(p Packet, match *RouteMatch) bool {
|
||||
func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
var name string
|
||||
// TODO: To avoid type switch everywhere in matching, I think we will need to have
|
||||
// to move to a concrete type for packets, to make matching and comparison more natural.
|
||||
// Current code structure is probably too rigid.
|
||||
// Maybe packet types should even be from an enum.
|
||||
switch p.(type) {
|
||||
case Message:
|
||||
case stanza.Message:
|
||||
name = "message"
|
||||
case IQ:
|
||||
case stanza.IQ:
|
||||
name = "iq"
|
||||
case Presence:
|
||||
case stanza.Presence:
|
||||
name = "presence"
|
||||
}
|
||||
if name == string(n) {
|
||||
@ -177,14 +179,14 @@ func (r *Route) Packet(name string) *Route {
|
||||
// nsTypeMather matches on a list of IQ payload namespaces
|
||||
type nsTypeMatcher []string
|
||||
|
||||
func (m nsTypeMatcher) Match(p Packet, match *RouteMatch) bool {
|
||||
var stanzaType StanzaType
|
||||
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
var stanzaType stanza.StanzaType
|
||||
switch packet := p.(type) {
|
||||
case IQ:
|
||||
case stanza.IQ:
|
||||
stanzaType = packet.Type
|
||||
case Presence:
|
||||
case stanza.Presence:
|
||||
stanzaType = packet.Type
|
||||
case Message:
|
||||
case stanza.Message:
|
||||
if packet.Type == "" {
|
||||
// optional on message, normal is the default type
|
||||
stanzaType = "normal"
|
||||
@ -211,8 +213,8 @@ func (r *Route) StanzaType(types ...string) *Route {
|
||||
// nsIqMather matches on a list of IQ payload namespaces
|
||||
type nsIQMatcher []string
|
||||
|
||||
func (m nsIQMatcher) Match(p Packet, match *RouteMatch) bool {
|
||||
iq, ok := p.(IQ)
|
||||
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
|
||||
iq, ok := p.(stanza.IQ)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
@ -235,7 +237,7 @@ func (r *Route) IQNamespaces(namespaces ...string) *Route {
|
||||
|
||||
// Matchers are used to "specialize" routes and focus on specific packet features
|
||||
type matcher interface {
|
||||
Match(Packet, *RouteMatch) bool
|
||||
Match(stanza.Packet, *RouteMatch) bool
|
||||
}
|
||||
|
||||
// RouteMatch extracts and gather match information
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
@ -11,13 +13,13 @@ import (
|
||||
|
||||
func TestNameMatcher(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.HandleFunc("message", func(s Sender, p Packet) {
|
||||
router.HandleFunc("message", func(s Sender, p stanza.Packet) {
|
||||
_ = s.SendRaw(successFlag)
|
||||
})
|
||||
|
||||
// Check that a message packet is properly matched
|
||||
conn := NewSenderMock()
|
||||
msg := NewMessage(Attrs{Type: MessageTypeChat, To: "test@localhost", Id: "1"})
|
||||
msg := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, To: "test@localhost", Id: "1"})
|
||||
msg.Body = "Hello"
|
||||
router.route(conn, msg)
|
||||
if conn.String() != successFlag {
|
||||
@ -26,8 +28,8 @@ func TestNameMatcher(t *testing.T) {
|
||||
|
||||
// Check that an IQ packet is not matched
|
||||
conn = NewSenderMock()
|
||||
iq := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"})
|
||||
iq.Payload = &DiscoInfo{}
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
iq.Payload = &stanza.DiscoInfo{}
|
||||
router.route(conn, iq)
|
||||
if conn.String() == successFlag {
|
||||
t.Error("IQ should not have been matched and routed")
|
||||
@ -37,18 +39,18 @@ func TestNameMatcher(t *testing.T) {
|
||||
func TestIQNSMatcher(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.NewRoute().
|
||||
IQNamespaces(NSDiscoInfo, NSDiscoItems).
|
||||
HandlerFunc(func(s Sender, p Packet) {
|
||||
IQNamespaces(stanza.NSDiscoInfo, stanza.NSDiscoItems).
|
||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||
_ = s.SendRaw(successFlag)
|
||||
})
|
||||
|
||||
// Check that an IQ with proper namespace does match
|
||||
conn := NewSenderMock()
|
||||
iqDisco := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"})
|
||||
iqDisco := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||
iqDisco.Payload = &DiscoInfo{
|
||||
iqDisco.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: NSDiscoInfo,
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
}}
|
||||
router.route(conn, iqDisco)
|
||||
@ -58,9 +60,9 @@ func TestIQNSMatcher(t *testing.T) {
|
||||
|
||||
// Check that another namespace is not matched
|
||||
conn = NewSenderMock()
|
||||
iqVersion := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"})
|
||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"})
|
||||
// TODO: Add a function to generate payload with proper namespace initialisation
|
||||
iqVersion.Payload = &DiscoInfo{
|
||||
iqVersion.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
@ -75,13 +77,13 @@ func TestTypeMatcher(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.NewRoute().
|
||||
StanzaType("normal").
|
||||
HandlerFunc(func(s Sender, p Packet) {
|
||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||
_ = s.SendRaw(successFlag)
|
||||
})
|
||||
|
||||
// Check that a packet with the proper type matches
|
||||
conn := NewSenderMock()
|
||||
message := NewMessage(Attrs{Type: "normal", To: "test@localhost", Id: "1"})
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
|
||||
message.Body = "hello"
|
||||
router.route(conn, message)
|
||||
|
||||
@ -91,7 +93,7 @@ func TestTypeMatcher(t *testing.T) {
|
||||
|
||||
// We should match on default type 'normal' for message without a type
|
||||
conn = NewSenderMock()
|
||||
message = NewMessage(Attrs{To: "test@localhost", Id: "1"})
|
||||
message = stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
|
||||
message.Body = "hello"
|
||||
router.route(conn, message)
|
||||
|
||||
@ -101,8 +103,8 @@ func TestTypeMatcher(t *testing.T) {
|
||||
|
||||
// We do not match on other types
|
||||
conn = NewSenderMock()
|
||||
iqVersion := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
iqVersion.Payload = &DiscoInfo{
|
||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
iqVersion.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
@ -119,38 +121,38 @@ func TestCompositeMatcher(t *testing.T) {
|
||||
router.NewRoute().
|
||||
IQNamespaces("jabber:iq:version").
|
||||
StanzaType("get").
|
||||
HandlerFunc(func(s Sender, p Packet) {
|
||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||
_ = s.SendRaw(successFlag)
|
||||
})
|
||||
|
||||
// Data set
|
||||
getVersionIq := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
getVersionIq.Payload = &Version{
|
||||
getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
getVersionIq.Payload = &stanza.Version{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
}}
|
||||
|
||||
setVersionIq := NewIQ(Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
setVersionIq.Payload = &Version{
|
||||
setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
setVersionIq.Payload = &stanza.Version{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
}}
|
||||
|
||||
GetDiscoIq := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
GetDiscoIq.Payload = &DiscoInfo{
|
||||
GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
GetDiscoIq.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "http://jabber.org/protocol/disco#info",
|
||||
Local: "query",
|
||||
}}
|
||||
|
||||
message := NewMessage(Attrs{Type: "normal", To: "test@localhost", Id: "1"})
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"})
|
||||
message.Body = "hello"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input Packet
|
||||
input stanza.Packet
|
||||
want bool
|
||||
}{
|
||||
{name: "match get version iq", input: getVersionIq, want: true},
|
||||
@ -178,13 +180,13 @@ func TestCompositeMatcher(t *testing.T) {
|
||||
func TestCatchallMatcher(t *testing.T) {
|
||||
router := NewRouter()
|
||||
router.NewRoute().
|
||||
HandlerFunc(func(s Sender, p Packet) {
|
||||
HandlerFunc(func(s Sender, p stanza.Packet) {
|
||||
_ = s.SendRaw(successFlag)
|
||||
})
|
||||
|
||||
// Check that we match on several packets
|
||||
conn := NewSenderMock()
|
||||
message := NewMessage(Attrs{Type: "chat", To: "test@localhost", Id: "1"})
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: "chat", To: "test@localhost", Id: "1"})
|
||||
message.Body = "hello"
|
||||
router.route(conn, message)
|
||||
|
||||
@ -193,8 +195,8 @@ func TestCatchallMatcher(t *testing.T) {
|
||||
}
|
||||
|
||||
conn = NewSenderMock()
|
||||
iqVersion := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
iqVersion.Payload = &DiscoInfo{
|
||||
iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"})
|
||||
iqVersion.Payload = &stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: "jabber:iq:version",
|
||||
Local: "query",
|
||||
@ -219,7 +221,7 @@ func NewSenderMock() SenderMock {
|
||||
return SenderMock{buffer: new(bytes.Buffer)}
|
||||
}
|
||||
|
||||
func (s SenderMock) Send(packet Packet) error {
|
||||
func (s SenderMock) Send(packet stanza.Packet) error {
|
||||
out, err := xml.Marshal(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -239,7 +241,7 @@ func (s SenderMock) String() string {
|
||||
|
||||
func TestSenderMock(t *testing.T) {
|
||||
conn := NewSenderMock()
|
||||
msg := NewMessage(Attrs{To: "test@localhost", Id: "1"})
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"})
|
||||
msg.Body = "Hello"
|
||||
if err := conn.Send(msg); err != nil {
|
||||
t.Error("Could not send message")
|
||||
|
30
session.go
30
session.go
@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
const xmppStreamOpen = "<?xml version='1.0'?><stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='1.0'>"
|
||||
@ -15,7 +17,7 @@ type Session struct {
|
||||
// Session info
|
||||
BindJid string // Jabber ID as provided by XMPP server
|
||||
StreamId string
|
||||
Features StreamFeatures
|
||||
Features stanza.StreamFeatures
|
||||
TlsEnabled bool
|
||||
lastPacketId int
|
||||
|
||||
@ -85,14 +87,14 @@ func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Config) {
|
||||
s.decoder.CharsetReader = o.CharsetReader
|
||||
}
|
||||
|
||||
func (s *Session) open(domain string) (f StreamFeatures) {
|
||||
func (s *Session) open(domain string) (f stanza.StreamFeatures) {
|
||||
// Send stream open tag
|
||||
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, NSClient, NSStream); s.err != nil {
|
||||
if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Set xml decoder and extract streamID from reply
|
||||
s.StreamId, s.err = initStream(s.decoder) // TODO refactor / rename
|
||||
s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename
|
||||
if s.err != nil {
|
||||
return
|
||||
}
|
||||
@ -112,7 +114,7 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
||||
if _, ok := s.Features.DoesStartTLS(); ok {
|
||||
fmt.Fprintf(s.socketProxy, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
|
||||
|
||||
var k tlsProceed
|
||||
var k stanza.TLSProceed
|
||||
if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil {
|
||||
s.err = errors.New("expecting starttls proceed: " + s.err.Error())
|
||||
return conn
|
||||
@ -120,8 +122,8 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn {
|
||||
s.TlsEnabled = true
|
||||
|
||||
// TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify)
|
||||
DefaultTlsConfig.ServerName = domain
|
||||
tlsConn := tls.Client(conn, &DefaultTlsConfig)
|
||||
stanza.DefaultTlsConfig.ServerName = domain
|
||||
tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig)
|
||||
// We convert existing connection to TLS
|
||||
if s.err = tlsConn.Handshake(); s.err != nil {
|
||||
return tlsConn
|
||||
@ -153,12 +155,12 @@ func (s *Session) bind(o Config) {
|
||||
var resource = o.parsedJid.Resource
|
||||
if resource != "" {
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'><resource>%s</resource></bind></iq>",
|
||||
s.PacketId(), nsBind, resource)
|
||||
s.PacketId(), stanza.NSBind, resource)
|
||||
} else {
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), nsBind)
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><bind xmlns='%s'/></iq>", s.PacketId(), stanza.NSBind)
|
||||
}
|
||||
|
||||
var iq IQ
|
||||
var iq stanza.IQ
|
||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||
s.err = errors.New("error decoding iq bind result: " + s.err.Error())
|
||||
return
|
||||
@ -166,7 +168,7 @@ func (s *Session) bind(o Config) {
|
||||
|
||||
// TODO Check all elements
|
||||
switch payload := iq.Payload.(type) {
|
||||
case *BindBind:
|
||||
case *stanza.BindBind:
|
||||
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
|
||||
default:
|
||||
s.err = errors.New("iq bind result missing")
|
||||
@ -182,9 +184,9 @@ func (s *Session) rfc3921Session(o Config) {
|
||||
return
|
||||
}
|
||||
|
||||
var iq IQ
|
||||
if s.Features.Session.optional.Local != "" {
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), nsSession)
|
||||
var iq stanza.IQ
|
||||
if s.Features.Session.Optional.Local != "" {
|
||||
fmt.Fprintf(s.socketProxy, "<iq type='set' id='%s'><session xmlns='%s'/></iq>", s.PacketId(), stanza.NSSession)
|
||||
if s.err = s.decoder.Decode(&iq); s.err != nil {
|
||||
s.err = errors.New("expecting iq result after session open: " + s.err.Error())
|
||||
return
|
||||
|
76
stanza/README.md
Normal file
76
stanza/README.md
Normal 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
79
stanza/auth_sasl.go
Normal 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
89
stanza/component.go
Normal 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
79
stanza/component_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO support ability to put Raw payload inside IQ
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// XMPP Errors
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
// ErrorType is a Enum of error attribute type
|
||||
type ErrorType string
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@ -24,10 +24,16 @@ type ControlField struct {
|
||||
}
|
||||
|
||||
type ControlSetResponse struct {
|
||||
IQPayload
|
||||
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
|
||||
}
|
||||
|
||||
func (c *ControlSetResponse) Namespace() string {
|
||||
return c.XMLName.Space
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
130
stanza/iq.go
Normal file
130
stanza/iq.go
Normal 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
120
stanza/iq_disco.go
Normal 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
55
stanza/iq_disco_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@ -6,22 +6,22 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestUnmarshalIqs(t *testing.T) {
|
||||
//var cs1 = new(iot.ControlSet)
|
||||
var tests = []struct {
|
||||
iqString string
|
||||
parsedIQ xmpp.IQ
|
||||
parsedIQ stanza.IQ
|
||||
}{
|
||||
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
|
||||
xmpp.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: xmpp.Attrs{Type: xmpp.IQTypeSet, To: "test@localhost", Id: "1"}}},
|
||||
stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.IQTypeSet, To: "test@localhost", Id: "1"}}},
|
||||
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", test.iqString)
|
||||
@ -35,16 +35,16 @@ func TestUnmarshalIqs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerateIq(t *testing.T) {
|
||||
iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
payload := xmpp.DiscoInfo{
|
||||
Identity: xmpp.Identity{
|
||||
Name: "Test Gateway",
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
payload := stanza.DiscoInfo{
|
||||
Identity: []stanza.Identity{
|
||||
{Name: "Test Gateway",
|
||||
Category: "gateway",
|
||||
Type: "mqtt",
|
||||
},
|
||||
Features: []xmpp.Feature{
|
||||
{Var: xmpp.NSDiscoInfo},
|
||||
{Var: xmpp.NSDiscoItems},
|
||||
}},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
},
|
||||
}
|
||||
iq.Payload = &payload
|
||||
@ -58,18 +58,18 @@ func TestGenerateIq(t *testing.T) {
|
||||
t.Error("empty error should not be serialized")
|
||||
}
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
|
||||
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
|
||||
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
|
||||
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorTag(t *testing.T) {
|
||||
xError := xmpp.Err{
|
||||
xError := stanza.Err{
|
||||
XMLName: xml.Name{Local: "error"},
|
||||
Code: 503,
|
||||
Type: "cancel",
|
||||
@ -82,7 +82,7 @@ func TestErrorTag(t *testing.T) {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
}
|
||||
|
||||
parsedError := xmpp.Err{}
|
||||
parsedError := stanza.Err{}
|
||||
if err = xml.Unmarshal(data, &parsedError); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@ -93,8 +93,8 @@ func TestErrorTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDiscoItems(t *testing.T) {
|
||||
iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
||||
payload := xmpp.DiscoItems{
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
|
||||
payload := stanza.DiscoItems{
|
||||
Node: "music",
|
||||
}
|
||||
iq.Payload = &payload
|
||||
@ -104,7 +104,7 @@ func TestDiscoItems(t *testing.T) {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@ -117,7 +117,7 @@ func TestDiscoItems(t *testing.T) {
|
||||
func TestUnmarshalPayload(t *testing.T) {
|
||||
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(query), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", query)
|
||||
@ -142,7 +142,7 @@ func TestPayloadWithError(t *testing.T) {
|
||||
</error>
|
||||
</iq>`
|
||||
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(iq), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal error: %s", iq)
|
||||
@ -158,7 +158,7 @@ func TestUnknownPayload(t *testing.T) {
|
||||
iq := `<iq type="get" to="service.localhost" id="1" >
|
||||
<query xmlns="unknown:ns"/>
|
||||
</iq>`
|
||||
parsedIQ := xmpp.IQ{}
|
||||
parsedIQ := stanza.IQ{}
|
||||
err := xml.Unmarshal([]byte(iq), &parsedIQ)
|
||||
if err != nil {
|
||||
t.Errorf("Unmarshal error: %#v (%s)", err, iq)
|
25
stanza/iq_version.go
Normal file
25
stanza/iq_version.go
Normal 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{})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,15 +1,15 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGenerateMessage(t *testing.T) {
|
||||
message := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
message.Body = "Hi"
|
||||
message.Subject = "Msg Subject"
|
||||
|
||||
@ -18,7 +18,7 @@ func TestGenerateMessage(t *testing.T) {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@ -38,7 +38,7 @@ func TestDecodeError(t *testing.T) {
|
||||
</error>
|
||||
</message>`
|
||||
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message error stanza unmarshall error: %v", err)
|
||||
return
|
||||
@ -50,15 +50,15 @@ func TestDecodeError(t *testing.T) {
|
||||
|
||||
func TestGetOOB(t *testing.T) {
|
||||
image := "https://localhost/image.png"
|
||||
msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost"})
|
||||
ext := xmpp.OOB{
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
ext := stanza.OOB{
|
||||
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
|
||||
URL: image,
|
||||
}
|
||||
msg.Extensions = append(msg.Extensions, &ext)
|
||||
|
||||
// OOB can properly be found
|
||||
var oob xmpp.OOB
|
||||
var oob stanza.OOB
|
||||
// Try to find and
|
||||
if ok := msg.Get(&oob); !ok {
|
||||
t.Error("could not find oob extension")
|
||||
@ -69,7 +69,7 @@ func TestGetOOB(t *testing.T) {
|
||||
}
|
||||
|
||||
// Markable is not found
|
||||
var m xmpp.Markable
|
||||
var m stanza.Markable
|
||||
if ok := msg.Get(&m); ok {
|
||||
t.Error("we should not have found markable extension")
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type HTML struct {
|
||||
MsgExtension
|
@ -1,20 +1,20 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestHTMLGen(t *testing.T) {
|
||||
htmlBody := "<p>Hello <b>World</b></p>"
|
||||
msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost"})
|
||||
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
|
||||
msg.Body = "Hello World"
|
||||
body := xmpp.HTMLBody{
|
||||
body := stanza.HTMLBody{
|
||||
InnerXML: htmlBody,
|
||||
}
|
||||
html := xmpp.HTML{Body: body}
|
||||
html := stanza.HTML{Body: body}
|
||||
msg.Extensions = append(msg.Extensions, html)
|
||||
|
||||
result := msg.XMPPFormat()
|
||||
@ -23,7 +23,7 @@ func TestHTMLGen(t *testing.T) {
|
||||
t.Errorf("incorrect serialize message:\n%s", result)
|
||||
}
|
||||
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message HTML unmarshall error: %v", err)
|
||||
return
|
||||
@ -33,7 +33,7 @@ func TestHTMLGen(t *testing.T) {
|
||||
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
|
||||
}
|
||||
|
||||
var h xmpp.HTML
|
||||
var h stanza.HTML
|
||||
if ok := parsedMessage.Get(&h); !ok {
|
||||
t.Error("could not extract HTML body")
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,10 +1,10 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestDecodeRequest(t *testing.T) {
|
||||
@ -15,7 +15,7 @@ func TestDecodeRequest(t *testing.T) {
|
||||
<body>My lord, dispatch; read o'er these articles.</body>
|
||||
<request xmlns='urn:xmpp:receipts'/>
|
||||
</message>`
|
||||
parsedMessage := xmpp.Message{}
|
||||
parsedMessage := stanza.Message{}
|
||||
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
|
||||
t.Errorf("message receipt unmarshall error: %v", err)
|
||||
return
|
||||
@ -31,7 +31,7 @@ func TestDecodeRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
switch ext := parsedMessage.Extensions[0].(type) {
|
||||
case *xmpp.ReceiptRequest:
|
||||
case *stanza.ReceiptRequest:
|
||||
if ext.XMLName.Local != "request" {
|
||||
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
|
||||
}
|
51
stanza/node.go
Normal file
51
stanza/node.go
Normal 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})
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
const (
|
||||
NSStream = "http://etherx.jabber.org/streams"
|
||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
NSClient = "jabber:client"
|
||||
NSComponent = "jabber:component:accept"
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
type Packet interface {
|
||||
Name() string
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
type StanzaType string
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@ -14,7 +14,7 @@ import (
|
||||
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
|
||||
// getting through the authentication process.
|
||||
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
|
||||
func initStream(p *xml.Decoder) (sessionID string, err error) {
|
||||
func InitStream(p *xml.Decoder) (sessionID string, err error) {
|
||||
for {
|
||||
var t xml.Token
|
||||
t, err = p.Token()
|
||||
@ -41,31 +41,14 @@ func initStream(p *xml.Decoder) (sessionID string, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scan XML token stream to find next StartElement.
|
||||
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||
for {
|
||||
t, err := p.Token()
|
||||
if err == io.EOF {
|
||||
return xml.StartElement{}, errors.New("connection closed")
|
||||
}
|
||||
if err != nil {
|
||||
return xml.StartElement{}, fmt.Errorf("nextStart %s", err)
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case xml.StartElement:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nextPacket scans XML token stream for next complete XMPP stanza.
|
||||
// NextPacket scans XML token stream for next complete XMPP stanza.
|
||||
// Once the type of stanza has been identified, a structure is created to decode
|
||||
// that stanza and returned.
|
||||
// TODO Use an interface to return packets interface xmppDecoder
|
||||
// TODO make auth and bind use nextPacket instead of directly nextStart
|
||||
func nextPacket(p *xml.Decoder) (Packet, error) {
|
||||
// TODO make auth and bind use NextPacket instead of directly NextStart
|
||||
func NextPacket(p *xml.Decoder) (Packet, error) {
|
||||
// Read start element to find out how we want to parse the XMPP packet
|
||||
se, err := nextStart(p)
|
||||
se, err := NextStart(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -74,7 +57,7 @@ func nextPacket(p *xml.Decoder) (Packet, error) {
|
||||
switch se.Name.Space {
|
||||
case NSStream:
|
||||
return decodeStream(p, se)
|
||||
case nsSASL:
|
||||
case NSSASL:
|
||||
return decodeSASL(p, se)
|
||||
case NSClient:
|
||||
return decodeClient(p, se)
|
||||
@ -86,6 +69,23 @@ func nextPacket(p *xml.Decoder) (Packet, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scan XML token stream to find next StartElement.
|
||||
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||
for {
|
||||
t, err := p.Token()
|
||||
if err == io.EOF {
|
||||
return xml.StartElement{}, errors.New("connection closed")
|
||||
}
|
||||
if err != nil {
|
||||
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
|
||||
}
|
||||
switch t := t.(type) {
|
||||
case xml.StartElement:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
|
||||
type.
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,10 +1,10 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// https://xmpp.org/extensions/xep-0045.html#example-27
|
||||
@ -18,12 +18,12 @@ func TestMucPassword(t *testing.T) {
|
||||
</x>
|
||||
</presence>`
|
||||
|
||||
var parsedPresence xmpp.Presence
|
||||
var parsedPresence stanza.Presence
|
||||
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", str)
|
||||
}
|
||||
|
||||
var muc xmpp.MucPresence
|
||||
var muc stanza.MucPresence
|
||||
if ok := parsedPresence.Get(&muc); !ok {
|
||||
t.Error("muc presence extension was not found")
|
||||
}
|
||||
@ -44,13 +44,13 @@ func TestMucHistory(t *testing.T) {
|
||||
</x>
|
||||
</presence>`
|
||||
|
||||
var parsedPresence xmpp.Presence
|
||||
var parsedPresence stanza.Presence
|
||||
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %s", str, err)
|
||||
return
|
||||
}
|
||||
|
||||
var muc xmpp.MucPresence
|
||||
var muc stanza.MucPresence
|
||||
if ok := parsedPresence.Get(&muc); !ok {
|
||||
t.Error("muc presence extension was not found")
|
||||
return
|
||||
@ -74,14 +74,14 @@ func TestMucNoHistory(t *testing.T) {
|
||||
|
||||
maxstanzas := 0
|
||||
|
||||
pres := xmpp.Presence{Attrs: xmpp.Attrs{
|
||||
pres := stanza.Presence{Attrs: stanza.Attrs{
|
||||
From: "hag66@shakespeare.lit/pda",
|
||||
Id: "n13mt3l",
|
||||
To: "coven@chat.shakespeare.lit/thirdwitch",
|
||||
},
|
||||
Extensions: []xmpp.PresExtension{
|
||||
xmpp.MucPresence{
|
||||
History: xmpp.History{MaxStanzas: xmpp.NewNullableInt(maxstanzas)},
|
||||
Extensions: []stanza.PresExtension{
|
||||
stanza.MucPresence{
|
||||
History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)},
|
||||
},
|
||||
},
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
// PresenceShow is a Enum of presence element show
|
||||
type PresenceShow string
|
@ -1,24 +1,23 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestGeneratePresence(t *testing.T) {
|
||||
presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = xmpp.PresenceShowChat
|
||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = stanza.PresenceShowChat
|
||||
|
||||
data, err := xml.Marshal(presence)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure")
|
||||
}
|
||||
|
||||
var parsedPresence xmpp.Presence
|
||||
var parsedPresence stanza.Presence
|
||||
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
@ -32,13 +31,13 @@ func TestPresenceSubElt(t *testing.T) {
|
||||
// Test structure to ensure that show, status and priority are correctly defined as presence
|
||||
// package sub-elements
|
||||
type pres struct {
|
||||
Show xmpp.PresenceShow `xml:"show"`
|
||||
Show stanza.PresenceShow `xml:"show"`
|
||||
Status string `xml:"status"`
|
||||
Priority int8 `xml:"priority"`
|
||||
}
|
||||
|
||||
presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = xmpp.PresenceShowXA
|
||||
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
|
||||
presence.Show = stanza.PresenceShowXA
|
||||
presence.Status = "Coding"
|
||||
presence.Priority = 10
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@ -8,7 +8,7 @@ import (
|
||||
var DefaultTlsConfig tls.Config
|
||||
|
||||
// Used during stream initiation / session establishment
|
||||
type tlsProceed struct {
|
||||
type TLSProceed struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp_test
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@ -10,6 +10,15 @@ import (
|
||||
// marshal / unmarshal. There is no need to manage them on the manually
|
||||
// crafted structure.
|
||||
func xmlEqual(x, y interface{}) bool {
|
||||
return cmp.Equal(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
// xmlDiff compares xml structures ignoring namespace preferences
|
||||
func xmlDiff(x, y interface{}) string {
|
||||
return cmp.Diff(x, y, xmlOpts())
|
||||
}
|
||||
|
||||
func xmlOpts() cmp.Options {
|
||||
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
|
||||
opts := cmp.Options{
|
||||
cmp.FilterValues(func(x, y interface{}) bool {
|
||||
@ -20,10 +29,12 @@ func xmlEqual(x, y interface{}) bool {
|
||||
if xx == zero || yy == zero {
|
||||
return true
|
||||
}
|
||||
if xx.Space == "" || yy.Space == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, alwaysEqual),
|
||||
}
|
||||
|
||||
return cmp.Equal(x, y, opts)
|
||||
return opts
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
// The Fluux XMPP lib can manage client or component XMPP streams.
|
||||
@ -29,7 +30,7 @@ type StreamClient interface {
|
||||
|
||||
// Sender is an interface provided by Stream clients to allow sending XMPP data.
|
||||
type Sender interface {
|
||||
Send(packet Packet) error
|
||||
Send(packet stanza.Packet) error
|
||||
SendRaw(packet string) error
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,14 @@ import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestNoStartTLS(t *testing.T) {
|
||||
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF xmpp.StreamFeatures
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
@ -32,7 +32,7 @@ func TestStartTLS(t *testing.T) {
|
||||
</starttls>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF xmpp.StreamFeatures
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
@ -52,7 +52,7 @@ func TestStreamManagement(t *testing.T) {
|
||||
<sm xmlns='urn:xmpp:sm:3'/>
|
||||
</stream:features>`
|
||||
|
||||
var parsedSF xmpp.StreamFeatures
|
||||
var parsedSF stanza.StreamFeatures
|
||||
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user