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