uhh whats a submodule i have never heard of that bro

This commit is contained in:
2026-07-17 17:18:21 +01:00
parent 006b43a590
commit 8cf0f84ddc
139 changed files with 17151 additions and 1 deletions
+142
View File
@@ -0,0 +1,142 @@
# XMPP Stanza
XMPP `stanza` package is used to parse, marshal 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})
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
Here is the list of implemented message extensions:
- `Delegation`
- `Markable`
- `MarkAcknowledged`
- `MarkDisplayed`
- `MarkReceived`
- `StateActive`
- `StateComposing`
- `StateGone`
- `StateInactive`
- `StatePaused`
- `HTML`
- `OOB`
- `ReceiptReceived`
- `ReceiptRequest`
- `Mood`
### Presence
Here is the list of implemented presence extensions:
- `MucPresence`
### 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:
- `ControlSet`
- `ControlSetResponse`
- `Delegation`
- `DiscoInfo`
- `DiscoItems`
- `Pubsub`
- `Version`
- `Node`
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.
To create an extension you need:
1. to create a struct for that extension. It need to have XMLName for consistency and to tagged at the struct level with
`xml` info.
2. It need to implement one or several extensions interface: stanza.IQPayload, stanza.MsgExtension and / or
stanza.PresExtension
3. Add that custom extension to the stanza.TypeRegistry during the file init.
Here an example code showing how to create a custom IQPayload.
```go
package myclient
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type CustomPayload struct {
XMLName xml.Name `xml:"my:custom:payload query"`
Node string `xml:"node,attr,omitempty"`
}
func (c CustomPayload) Namespace() string {
return c.XMLName.Space
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{})
}
```
+157
View File
@@ -0,0 +1,157 @@
package stanza
import "encoding/xml"
// Implements the XEP-0050 extension
const (
CommandActionCancel = "cancel"
CommandActionComplete = "complete"
CommandActionExecute = "execute"
CommandActionNext = "next"
CommandActionPrevious = "prev"
CommandStatusCancelled = "canceled"
CommandStatusCompleted = "completed"
CommandStatusExecuting = "executing"
CommandNoteTypeErr = "error"
CommandNoteTypeInfo = "info"
CommandNoteTypeWarn = "warn"
)
type Command struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"`
CommandElements []CommandElement
BadAction *struct{} `xml:"bad-action,omitempty"`
BadLocale *struct{} `xml:"bad-locale,omitempty"`
BadPayload *struct{} `xml:"bad-payload,omitempty"`
BadSessionId *struct{} `xml:"bad-sessionid,omitempty"`
MalformedAction *struct{} `xml:"malformed-action,omitempty"`
SessionExpired *struct{} `xml:"session-expired,omitempty"`
// Attributes
Action string `xml:"action,attr,omitempty"`
Node string `xml:"node,attr"`
SessionId string `xml:"sessionid,attr,omitempty"`
Status string `xml:"status,attr,omitempty"`
Lang string `xml:"lang,attr,omitempty"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (c *Command) Namespace() string {
return c.XMLName.Space
}
func (c *Command) GetSet() *ResultSet {
return c.ResultSet
}
type CommandElement interface {
Ref() string
}
type Actions struct {
XMLName xml.Name `xml:"actions"`
Prev *struct{} `xml:"prev,omitempty"`
Next *struct{} `xml:"next,omitempty"`
Complete *struct{} `xml:"complete,omitempty"`
Execute string `xml:"execute,attr,omitempty"`
}
func (a *Actions) Ref() string {
return "actions"
}
type Note struct {
XMLName xml.Name `xml:"note"`
Text string `xml:",cdata"`
Type string `xml:"type,attr,omitempty"`
}
func (n *Note) Ref() string {
return "note"
}
func (f *Form) Ref() string { return "form" }
func (n *Node) Ref() string {
return "node"
}
func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
c.XMLName = start.Name
// Extract packet attributes
for _, attr := range start.Attr {
if attr.Name.Local == "action" {
c.Action = attr.Value
}
if attr.Name.Local == "node" {
c.Node = attr.Value
}
if attr.Name.Local == "sessionid" {
c.SessionId = attr.Value
}
if attr.Name.Local == "status" {
c.Status = attr.Value
}
if attr.Name.Local == "lang" {
c.Lang = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
// Decode sub-elements
var err error
switch tt.Name.Local {
case "actions":
a := Actions{}
err = d.DecodeElement(&a, &tt)
c.CommandElements = append(c.CommandElements, &a)
case "note":
nt := Note{}
err = d.DecodeElement(&nt, &tt)
c.CommandElements = append(c.CommandElements, &nt)
case "x":
f := Form{}
err = d.DecodeElement(&f, &tt)
c.CommandElements = append(c.CommandElements, &f)
default:
n := Node{}
err = d.DecodeElement(&n, &tt)
c.CommandElements = append(c.CommandElements, &n)
if err != nil {
return err
}
}
if err != nil {
return err
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/commands", Local: "command"}, Command{})
}
+41
View File
@@ -0,0 +1,41 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestMarshalCommands(t *testing.T) {
input := "<command xmlns=\"http://jabber.org/protocol/commands\" node=\"list\" " +
"sessionid=\"list:20020923T213616Z-700\" status=\"completed\"><x xmlns=\"jabber:x:data\" " +
"type=\"result\"><title>Available Services</title><reported xmlns=\"jabber:x:data\"><field var=\"service\" " +
"label=\"Service\"></field><field var=\"runlevel-1\" label=\"Single-User mode\">" +
"</field><field var=\"runlevel-2\" label=\"Non-Networked Multi-User mode\"></field><field var=\"runlevel-3\" " +
"label=\"Full Multi-User mode\"></field><field var=\"runlevel-5\" label=\"X-Window mode\"></field></reported>" +
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>httpd</value></field><field var=\"runlevel-1\">" +
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>postgresql</value></field>" +
"<field var=\"runlevel-1\"><value>off</value></field><field var=\"runlevel-2\"><value>off</value></field>" +
"<field var=\"runlevel-3\"><value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item>" +
"<item xmlns=\"jabber:x:data\"><field var=\"service\"><value>jabberd</value></field><field var=\"runlevel-1\">" +
"<value>off</value></field><field var=\"runlevel-2\"><value>off</value></field><field var=\"runlevel-3\">" +
"<value>on</value></field><field var=\"runlevel-5\"><value>on</value></field></item></x></command>"
var c stanza.Command
err := xml.Unmarshal([]byte(input), &c)
if err != nil {
t.Fatalf("failed to unmarshal initial input")
}
data, err := xml.Marshal(c)
if err != nil {
t.Fatalf("failed to marshal unmarshalled input")
}
if err := compareMarshal(input, string(data)); err != nil {
t.Fatal(err)
}
}
+96
View File
@@ -0,0 +1,96 @@
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
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (d *Delegation) Namespace() string {
return d.XMLName.Space
}
func (d *Delegation) GetSet() *ResultSet {
return d.ResultSet
}
// Forwarded is used to wrapped forwarded stanzas.
// TODO: Move it in another file, as it is not limited to components.
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{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{})
}
+79
View File
@@ -0,0 +1,79 @@
package stanza
import (
"encoding/xml"
"testing"
)
// We should be able to properly parse delegation confirmation messages
func TestParsingDelegationMessage(t *testing.T) {
packetStr := `<message to='service.localhost' from='localhost'>
<delegation xmlns='urn:xmpp:delegation:1'>
<delegated namespace='http://jabber.org/protocol/pubsub'/>
</delegation>
</message>`
var msg Message
data := []byte(packetStr)
if err := xml.Unmarshal(data, &msg); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as MsgExtension
var nsDelegated string
for _, ext := range msg.Extensions {
if delegation, ok := ext.(*Delegation); ok {
nsDelegated = delegation.Delegated.Namespace
}
}
if nsDelegated != "http://jabber.org/protocol/pubsub" {
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
}
}
// Check that we can parse a delegation IQ.
// The most important thing is to be able to
func TestParsingDelegationIQ(t *testing.T) {
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
<delegation xmlns='urn:xmpp:delegation:1'>
<forwarded xmlns='urn:xmpp:forward:0'>
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='http://jabber.org/protocol/mood'>
<item id='current'>
<mood xmlns='http://jabber.org/protocol/mood'>
<excited/>
</mood>
</item>
</publish>
</pubsub>
</iq>
</forwarded>
</delegation>
</iq>`
var iq IQ
data := []byte(packetStr)
if err := xml.Unmarshal(data, &iq); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as IQPayload
var node string
if iq.Payload != nil {
if delegation, ok := iq.Payload.(*Delegation); ok {
packet := delegation.Forwarded.Stanza
forwardedIQ, ok := packet.(*IQ)
if !ok {
t.Errorf("Could not extract packet IQ")
return
}
if forwardedIQ.Payload != nil {
if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); 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)
}
}
+70
View File
@@ -0,0 +1,70 @@
package stanza
import (
"errors"
"strings"
"time"
)
// Helper structures and functions to manage dates and timestamps as defined in
// XEP-0082: XMPP Date and Time Profiles (https://xmpp.org/extensions/xep-0082.html)
const dateLayoutXEP0082 = "2006-01-02"
const timeLayoutXEP0082 = "15:04:05+00:00"
var InvalidDateInput = errors.New("could not parse date. Input might not be in a supported format")
var InvalidDateOutput = errors.New("could not format date as desired")
type JabberDate struct {
value time.Time
}
func (d JabberDate) DateToString() string {
return d.value.Format(dateLayoutXEP0082)
}
func (d JabberDate) DateTimeToString(nanos bool) string {
if nanos {
return d.value.Format(time.RFC3339Nano)
}
return d.value.Format(time.RFC3339)
}
func (d JabberDate) TimeToString(nanos bool) (string, error) {
if nanos {
spl := strings.Split(d.value.Format(time.RFC3339Nano), "T")
if len(spl) != 2 {
return "", InvalidDateOutput
}
return spl[1], nil
}
spl := strings.Split(d.value.Format(time.RFC3339), "T")
if len(spl) != 2 {
return "", InvalidDateOutput
}
return spl[1], nil
}
func NewJabberDateFromString(strDate string) (JabberDate, error) {
t, err := time.Parse(time.RFC3339, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
t, err = time.Parse(time.RFC3339Nano, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
t, err = time.Parse(dateLayoutXEP0082, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
t, err = time.Parse(timeLayoutXEP0082, strDate)
if err == nil {
return JabberDate{value: t}, nil
}
return JabberDate{}, InvalidDateInput
}
+191
View File
@@ -0,0 +1,191 @@
package stanza
import (
"testing"
"time"
)
func TestDateToString(t *testing.T) {
t1 := JabberDate{value: time.Now()}
t2 := JabberDate{value: time.Now().Add(24 * time.Hour)}
t1Str := t1.DateToString()
t2Str := t2.DateToString()
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestDateToStringOracle(t *testing.T) {
expected := "2009-11-10"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatal(err)
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str := t1.DateToString()
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestDateTimeToString(t *testing.T) {
t1 := JabberDate{value: time.Now()}
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
t1Str := t1.DateTimeToString(false)
t2Str := t2.DateTimeToString(false)
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestDateTimeToStringOracle(t *testing.T) {
expected := "2009-11-10T23:03:22+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatal(err)
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str := t1.DateTimeToString(false)
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestDateTimeToStringNanos(t *testing.T) {
t1 := JabberDate{value: time.Now()}
time.After(10 * time.Millisecond)
t2 := JabberDate{value: time.Now()}
t1Str := t1.DateTimeToString(true)
t2Str := t2.DateTimeToString(true)
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestDateTimeToStringNanosOracle(t *testing.T) {
expected := "2009-11-10T23:03:22.000000089+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatal(err)
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str := t1.DateTimeToString(true)
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestTimeToString(t *testing.T) {
t1 := JabberDate{value: time.Now()}
t2 := JabberDate{value: time.Now().Add(10 * time.Second)}
t1Str, err := t1.TimeToString(false)
if err != nil {
t.Fatal(err)
}
t2Str, err := t2.TimeToString(false)
if err != nil {
t.Fatal(err)
}
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestTimeToStringOracle(t *testing.T) {
expected := "23:03:22+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatal(err)
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str, err := t1.TimeToString(false)
if err != nil {
t.Fatal(err)
}
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestTimeToStringNanos(t *testing.T) {
t1 := JabberDate{value: time.Now()}
time.Sleep(10 * time.Millisecond)
t2 := JabberDate{value: time.Now()}
t1Str, err := t1.TimeToString(true)
if err != nil {
t.Fatal(err)
}
t2Str, err := t2.TimeToString(true)
if err != nil {
t.Fatal(err)
}
if t1Str == t2Str {
t.Fatalf("time representations should not be identical")
}
}
func TestTimeToStringNanosOracle(t *testing.T) {
expected := "23:03:22.000000089+08:00"
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Fatal(err)
}
t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)}
t1Str, err := t1.TimeToString(true)
if err != nil {
t.Fatal(err)
}
if t1Str != expected {
t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str)
}
}
func TestJabberDateParsing(t *testing.T) {
date := "2009-11-10"
_, err := NewJabberDateFromString(date)
if err != nil {
t.Fatal(err)
}
dateTime := "2009-11-10T23:03:22+08:00"
_, err = NewJabberDateFromString(dateTime)
if err != nil {
t.Fatal(err)
}
dateTimeNanos := "2009-11-10T23:03:22.000000089+08:00"
_, err = NewJabberDateFromString(dateTimeNanos)
if err != nil {
t.Fatal(err)
}
// TODO : fix these. Parsing a time with an offset doesn't work
//time := "23:03:22+08:00"
//_, err = NewJabberDateFromString(time)
//if err != nil {
// t.Fatalf(err.Error())
//}
//timeNanos := "23:03:22.000000089+08:00"
//_, err = NewJabberDateFromString(timeNanos)
//if err != nil {
// t.Fatalf(err.Error())
//}
}
+4
View File
@@ -0,0 +1,4 @@
/*
XMPP stanza package is used to parse, marshal and unmarshal XMPP stanzas and nonzas.
*/
package stanza
+136
View File
@@ -0,0 +1,136 @@
package stanza
import (
"encoding/xml"
"strconv"
"strings"
)
// ============================================================================
// XMPP Errors
// Err is an XMPP stanza payload that is used to report error on message,
// presence or iq stanza.
// It is intended to be added in the payload of the erroneous stanza.
type Err struct {
XMLName xml.Name `xml:"error"`
Code int `xml:"code,attr,omitempty"`
Type ErrorType `xml:"type,attr"` // required
Reason string
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
}
// UnmarshalXML implements custom parsing for XMPP errors
func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
x.XMLName = start.Name
// Extract attributes
for _, attr := range start.Attr {
if attr.Name.Local == "type" {
x.Type = ErrorType(attr.Value)
}
if attr.Name.Local == "code" {
if code, err := strconv.Atoi(attr.Value); err == nil {
x.Code = code
}
}
}
// Check subelements to extract error text and reason (from local namespace).
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
elt := new(Node)
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
// TODO : change the pubsub handling ? It kind of dilutes the information
// Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric)
goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"}
if elt.XMLName == textName || // Regular error text
elt.XMLName == goneName { // Gone text for pubsub
x.Text = elt.Content
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" ||
elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" {
if strings.TrimSpace(x.Reason) != "" {
x.Reason = strings.Join([]string{elt.XMLName.Local}, ":")
} else {
x.Reason = elt.XMLName.Local
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
if x.Code == 0 {
return nil
}
// Encode start element and attributes
start.Name = xml.Name{Local: "error"}
code := xml.Attr{
Name: xml.Name{Local: "code"},
Value: strconv.Itoa(x.Code),
}
start.Attr = append(start.Attr, code)
if len(x.Type) > 0 {
typ := xml.Attr{
Name: xml.Name{Local: "type"},
Value: string(x.Type),
}
start.Attr = append(start.Attr, typ)
}
err = e.EncodeToken(start)
// SubTags
// Reason
if x.Reason != "" {
reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason}
err = e.EncodeToken(xml.StartElement{Name: reason})
if err != nil {
return err
}
err = e.EncodeToken(xml.EndElement{Name: reason})
if err != nil {
return err
}
}
// Text
if x.Text != "" {
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
err = e.EncodeToken(xml.StartElement{Name: text})
if err != nil {
return err
}
err = e.EncodeToken(xml.CharData(x.Text))
if err != nil {
return err
}
err = e.EncodeToken(xml.EndElement{Name: text})
if err != nil {
return err
}
}
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
+13
View File
@@ -0,0 +1,13 @@
package stanza
// ErrorType is a Enum of error attribute type
type ErrorType string
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
const (
ErrorTypeAuth ErrorType = "auth"
ErrorTypeCancel ErrorType = "cancel"
ErrorTypeContinue ErrorType = "continue"
ErrorTypeModify ErrorType = "modify"
ErrorTypeWait ErrorType = "wait"
)
+31
View File
@@ -0,0 +1,31 @@
package stanza
import (
"encoding/xml"
"testing"
)
func TestErr_UnmarshalXML(t *testing.T) {
packet := `
<iq from='pubsub.example.com'
id='kj4vz31m'
to='romeo@example.net/foo'
type='error'>
<error type='wait'>
<resource-constraint
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>System overloaded, please retry</text>
</error>
</iq>`
parsedIQ := IQ{}
data := []byte(packet)
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
xmppError := parsedIQ.Error
if xmppError.Text != "System overloaded, please retry" {
t.Errorf("Could not extract error text: '%s'", xmppError.Text)
}
}
+34
View File
@@ -0,0 +1,34 @@
package stanza
// FIFO queue for string contents
// Implementations have no guarantee regarding thread safety !
type FifoQueue interface {
// Pop returns the first inserted element still in queue and deletes it from queue. If queue is empty, returns nil
// No guarantee regarding thread safety !
Pop() Queueable
// PopN returns the N first inserted elements still in queue and deletes them from queue. If queue is empty or i<=0, returns nil
// If number to pop is greater than queue length, returns all queue elements
// No guarantee regarding thread safety !
PopN(i int) []Queueable
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty, returns nil
// No guarantee regarding thread safety !
Peek() Queueable
// Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty or i<=0, returns nil.
// If number to peek is greater than queue length, returns all queue elements
// No guarantee regarding thread safety !
PeekN() []Queueable
// Push adds an element to the queue
// No guarantee regarding thread safety !
Push(s Queueable) error
// Empty returns true if queue is empty
// No guarantee regarding thread safety !
Empty() bool
}
type Queueable interface {
QueueableName() string
}
+68
View File
@@ -0,0 +1,68 @@
package stanza
import "encoding/xml"
type FormType string
const (
FormTypeCancel = "cancel"
FormTypeForm = "form"
FormTypeResult = "result"
FormTypeSubmit = "submit"
)
// See XEP-0004 and XEP-0068
// Pointer semantics
type Form struct {
XMLName xml.Name `xml:"jabber:x:data x"`
Instructions []string `xml:"instructions"`
Title string `xml:"title,omitempty"`
Fields []*Field `xml:"field,omitempty"`
Reported *FormItem `xml:"reported"`
Items []FormItem `xml:"item,omitempty"`
Type string `xml:"type,attr"`
}
type FormItem struct {
XMLName xml.Name
Fields []Field `xml:"field,omitempty"`
}
type Field struct {
XMLName xml.Name `xml:"field"`
Description string `xml:"desc,omitempty"`
Required *string `xml:"required"`
ValuesList []string `xml:"value"`
Options []Option `xml:"option,omitempty"`
Var string `xml:"var,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Label string `xml:"label,attr,omitempty"`
}
func NewForm(fields []*Field, formType string) *Form {
return &Form{
Type: formType,
Fields: fields,
}
}
type FieldType string
const (
FieldTypeBool = "boolean"
FieldTypeFixed = "fixed"
FieldTypeHidden = "hidden"
FieldTypeJidMulti = "jid-multi"
FieldTypeJidSingle = "jid-single"
FieldTypeListMulti = "list-multi"
FieldTypeListSingle = "list-single"
FieldTypeTextMulti = "text-multi"
FieldTypeTextPrivate = "text-private"
FieldTypeTextSingle = "text-Single"
)
type Option struct {
XMLName xml.Name `xml:"option"`
Label string `xml:"label,attr,omitempty"`
ValuesList []string `xml:"value"`
}
+110
View File
@@ -0,0 +1,110 @@
package stanza
import (
"encoding/xml"
"strings"
"testing"
)
const (
formSubmit = "<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\">" +
"<configure node=\"princely_musings\">" +
"<x xmlns=\"jabber:x:data\" type=\"submit\">" +
"<field var=\"FORM_TYPE\" type=\"hidden\">" +
"<value>http://jabber.org/protocol/pubsub#node_config</value>" +
"</field>" +
"<field var=\"pubsub#title\">" +
"<value>Princely Musings (Atom)</value>" +
"</field>" +
"<field var=\"pubsub#deliver_notifications\">" +
"<value>1</value>" +
"</field>" +
"<field var=\"pubsub#access_model\">" +
"<value>roster</value>" +
"</field>" +
"<field var=\"pubsub#roster_groups_allowed\">" +
"<value>friends</value>" +
"<value>servants</value>" +
"<value>courtiers</value>" +
"</field>" +
"<field var=\"pubsub#type\">" +
"<value>http://www.w3.org/2005/Atom</value>" +
"</field>" +
"<field var=\"pubsub#notification_type\" type=\"list-single\"" +
"label=\"Specify the delivery style for event notifications\">" +
"<value>headline</value>" +
"<option>" +
"<value>normal</value>" +
"</option>" +
"<option>" +
"<value>headline</value>" +
"</option>" +
"</field>" +
"</x>" +
"</configure>" +
"</pubsub>"
clientJid = "hamlet@denmark.lit/elsinore"
serviceJid = "pubsub.shakespeare.lit"
iqId = "config1"
serviceNode = "princely_musings"
)
func TestMarshalFormSubmit(t *testing.T) {
formIQ, err := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet})
if err != nil {
t.Fatalf("failed to create formIQ: %v", err)
}
formIQ.Payload = &PubSubOwner{
OwnerUseCase: &ConfigureOwner{
Node: serviceNode,
Form: &Form{
Type: FormTypeSubmit,
Fields: []*Field{
{Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
{
Var: "pubsub#notification_type",
Type: "list-single",
Label: "Specify the delivery style for event notifications",
ValuesList: []string{"headline"},
Options: []Option{
{ValuesList: []string{"normal"}},
{ValuesList: []string{"headline"}},
},
},
},
},
},
}
b, err := xml.Marshal(formIQ.Payload)
if err != nil {
t.Fatalf("Could not marshal formIQ : %v", err)
}
if strings.ReplaceAll(string(b), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
t.Fatalf("Expected formIQ and marshalled one are different.\nExepected : %s\nMarshalled : %s", formSubmit, string(b))
}
}
func TestUnmarshalFormSubmit(t *testing.T) {
var f PubSubOwner
mErr := xml.Unmarshal([]byte(formSubmit), &f)
if mErr != nil {
t.Fatalf("failed to unmarshal formSubmit ! %s", mErr)
}
data, err := xml.Marshal(&f)
if err != nil {
t.Fatalf("failed to marshal formSubmit")
}
if strings.ReplaceAll(string(data), " ", "") != strings.ReplaceAll(formSubmit, " ", "") {
t.Fatalf("failed unmarshal/marshal for formSubmit : %s\n%s", string(data), formSubmit)
}
}
+48
View File
@@ -0,0 +1,48 @@
package stanza
import (
"encoding/xml"
)
type ControlSet struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
Fields []ControlField `xml:",any"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (c *ControlSet) Namespace() string {
return c.XMLName.Space
}
func (c *ControlSet) GetSet() *ResultSet {
return c.ResultSet
}
type ControlGetForm struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
}
type ControlField struct {
XMLName xml.Name
Name string `xml:"name,attr,omitempty"`
Value string `xml:"value,attr,omitempty"`
}
type ControlSetResponse struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
}
func (c *ControlSetResponse) Namespace() string {
return c.XMLName.Space
}
func (c *ControlSetResponse) GetSet() *ResultSet {
return nil
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:iot:control", Local: "set"}, ControlSet{})
}
+26
View File
@@ -0,0 +1,26 @@
package stanza
import (
"encoding/xml"
"testing"
)
func TestControlSet(t *testing.T) {
packet := `
<iq to='test@localhost/jukebox' from='admin@localhost/mbp' type='set' id='2'>
<set xmlns='urn:xmpp:iot:control' xml:lang='en'>
<string name='action' value='play'/>
<string name='url' value='https://soundcloud.com/radiohead/spectre'/>
</set>
</iq>`
parsedIQ := IQ{}
data := []byte(packet)
if err := xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
t.Errorf("Payload is not an iot control set: %v", cs)
}
}
+192
View File
@@ -0,0 +1,192 @@
package stanza
import (
"encoding/xml"
"errors"
"strings"
"github.com/google/uuid"
)
/*
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 generic structure
Any *Node `xml:",any"`
}
type IQPayload interface {
Namespace() string
GetSet() *ResultSet
}
func NewIQ(a Attrs) (*IQ, error) {
if a.Id == "" {
if id, err := uuid.NewRandom(); err == nil {
a.Id = id.String()
}
}
iq := IQ{
XMLName: xml.Name{Local: "iq"},
Attrs: a,
}
if iq.Type.IsEmpty() {
return nil, IqTypeUnset
}
return &iq, nil
}
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"
}
// NoOp to implement BiDirIteratorElt
func (*IQ) NoOp() {
}
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 {
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
}
}
}
}
var (
IqTypeUnset = errors.New("iq type is not set but is mandatory")
IqIDUnset = errors.New("iq stanza ID is not set but is mandatory")
IqSGetNoPl = errors.New("iq is of type get or set but has no payload")
IqResNoPl = errors.New("iq is of type result but has no payload")
IqErrNoErrPl = errors.New("iq is of type error but has no error payload")
)
// IsValid checks if the IQ is valid. If not, return an error with the reason as a message
// Following RFC-3920 for IQs
func (iq *IQ) IsValid() (bool, error) {
// ID is required
if len(strings.TrimSpace(iq.Id)) == 0 {
return false, IqIDUnset
}
// Type is required
if iq.Type.IsEmpty() {
return false, IqTypeUnset
}
// Type get and set must contain one and only one child element that specifies the semantics
if iq.Type == IQTypeGet || iq.Type == IQTypeSet {
if iq.Payload == nil && iq.Any == nil {
return false, IqSGetNoPl
}
}
// A result must include zero or one child element
if iq.Type == IQTypeResult {
if iq.Payload != nil && iq.Any != nil {
return false, IqResNoPl
}
}
//Error type must contain an "error" child element
if iq.Type == IQTypeError {
if iq.Error == nil {
return false, IqErrNoErrPl
}
}
return true, nil
}
+163
View File
@@ -0,0 +1,163 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Disco Info
const (
// NSDiscoInfo defines the namespace for disco IQ stanzas
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"`
ResultSet *ResultSet `xml:"set,omitempty"`
}
// Namespace lets DiscoInfo implement the IQPayload interface
func (d *DiscoInfo) Namespace() string {
return d.XMLName.Space
}
func (d *DiscoInfo) GetSet() *ResultSet {
return d.ResultSet
}
// ---------------
// 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) *DiscoInfo {
d.Node = node
return d
}
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
d.Identity = ident
return d
}
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
d.Features = []Feature{}
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"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (d *DiscoItems) Namespace() string {
return d.XMLName.Space
}
func (d *DiscoItems) GetSet() *ResultSet {
return d.ResultSet
}
// ---------------
// Builder helpers
// DiscoItems builds a default DiscoItems payload
func (iq *IQ) DiscoItems() *DiscoItems {
d := DiscoItems{
XMLName: xml.Name{Space: NSDiscoItems, Local: "query"},
}
iq.Payload = &d
return &d
}
func (d *DiscoItems) SetNode(node string) *DiscoItems {
d.Node = node
return d
}
func (d *DiscoItems) AddItem(jid, node, name string) *DiscoItems {
item := DiscoItem{
JID: jid,
Node: node,
Name: name,
}
d.Items = append(d.Items, item)
return d
}
type DiscoItem struct {
XMLName xml.Name `xml:"item"`
JID string `xml:"jid,attr,omitempty"`
Node string `xml:"node,attr,omitempty"`
Name string `xml:"name,attr,omitempty"`
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoInfo, Local: "query"}, DiscoInfo{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoItems, Local: "query"}, DiscoItems{})
}
+96
View File
@@ -0,0 +1,96 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// Test DiscoInfo Builder with several features
func TestDiscoInfo_Builder(t *testing.T) {
iq, err := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
disco := iq.DiscoInfo()
disco.AddIdentity("Test Component", "gateway", "service")
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
if !ok {
t.Errorf("Parsed stanza does not contain correct 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)
}
}
}
// Implements XEP-0030 example 17
// https://xmpp.org/extensions/xep-0030.html#example-17
func TestDiscoItems_Builder(t *testing.T) {
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit",
To: "romeo@montague.net/orchard", Id: "items-2"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
iq.DiscoItems().
AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare").
AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride").
AddItem("catalog.shakespeare.lit", "music", "Music from the time of Shakespeare")
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.DiscoItems)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check items
items := []stanza.DiscoItem{{xml.Name{}, "catalog.shakespeare.lit", "books", "Books by and about Shakespeare"},
{xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"},
{xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}}
if len(pp.Items) != len(items) {
t.Errorf("List length mismatch: %#v", pp.Items)
} else {
for i, item := range pp.Items {
if item.JID != items[i].JID {
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
if item.Node != items[i].Node {
t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
if item.Name != items[i].Name {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].JID, item.JID)
}
}
}
}
+126
View File
@@ -0,0 +1,126 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Roster
const (
// NSRoster is the Roster IQ namespace
NSRoster = "jabber:iq:roster"
// SubscriptionNone indicates the user does not have a subscription to
// the contact's presence, and the contact does not have a subscription
// to the user's presence; this is the default value, so if the subscription
// attribute is not included then the state is to be understood as "none"
SubscriptionNone = "none"
// SubscriptionTo indicates the user has a subscription to the contact's
// presence, but the contact does not have a subscription to the user's presence.
SubscriptionTo = "to"
// SubscriptionFrom indicates the contact has a subscription to the user's
// presence, but the user does not have a subscription to the contact's presence
SubscriptionFrom = "from"
// SubscriptionBoth indicates the user and the contact have subscriptions to each
// other's presence (also called a "mutual subscription")
SubscriptionBoth = "both"
)
// ----------
// Namespaces
// Roster struct represents Roster IQs
type Roster struct {
XMLName xml.Name `xml:"jabber:iq:roster query"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
// Namespace defines the namespace for the RosterIQ
func (r *Roster) Namespace() string {
return r.XMLName.Space
}
func (r *Roster) GetSet() *ResultSet {
return r.ResultSet
}
// ---------------
// Builder helpers
// RosterIQ builds a default Roster payload
func (iq *IQ) RosterIQ() *Roster {
r := Roster{
XMLName: xml.Name{
Space: NSRoster,
Local: "query",
},
}
iq.Payload = &r
return &r
}
// -----------
// SubElements
// RosterItems represents the list of items in a roster IQ
type RosterItems struct {
XMLName xml.Name `xml:"jabber:iq:roster query"`
Items []RosterItem `xml:"item"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
// Namespace lets RosterItems implement the IQPayload interface
func (r *RosterItems) Namespace() string {
return r.XMLName.Space
}
func (r *RosterItems) GetSet() *ResultSet {
return r.ResultSet
}
// RosterItem represents an item in the roster iq
type RosterItem struct {
XMLName xml.Name `xml:"jabber:iq:roster item"`
Jid string `xml:"jid,attr"`
Ask string `xml:"ask,attr,omitempty"`
Name string `xml:"name,attr,omitempty"`
Subscription string `xml:"subscription,attr,omitempty"`
Groups []string `xml:"group"`
}
// ---------------
// Builder helpers
// RosterItems builds a default RosterItems payload
func (iq *IQ) RosterItems() *RosterItems {
ri := RosterItems{
XMLName: xml.Name{Space: "jabber:iq:roster", Local: "query"},
}
iq.Payload = &ri
return &ri
}
// AddItem builds an item and ads it to the roster IQ
func (r *RosterItems) AddItem(jid, subscription, ask, name string, groups []string) *RosterItems {
item := RosterItem{
Jid: jid,
Name: name,
Groups: groups,
Subscription: subscription,
Ask: ask,
}
r.Items = append(r.Items, item)
return r
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, Roster{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, RosterItems{})
}
+112
View File
@@ -0,0 +1,112 @@
package stanza
import (
"encoding/xml"
"reflect"
"testing"
)
func TestRosterBuilder(t *testing.T) {
iq, err := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
var noGroup []string
iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com",
SubscriptionBoth,
"",
"xl8ceaw",
[]string{"0flucpm8i2jtrjhxw01uf1nd2",
"bm2bajg9ex4e1swiuju9i9nu5",
"rvjpanomi4ejpx42fpmffoac0"}).
AddItem("9aynsym60zbu78jbdvpho7s68@crypho.com",
SubscriptionBoth,
"",
"9aynsym60",
[]string{"mzaoy73i6ra5k502182zi1t97"}).
AddItem("admin@crypho.com",
SubscriptionBoth,
"",
"admin",
noGroup)
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*RosterItems)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check items
items := []RosterItem{
{
XMLName: xml.Name{},
Name: "xl8ceaw",
Ask: "",
Jid: "xl8ceawrfu8zdneomw1h6h28d@crypho.com",
Subscription: SubscriptionBoth,
Groups: []string{"0flucpm8i2jtrjhxw01uf1nd2",
"bm2bajg9ex4e1swiuju9i9nu5",
"rvjpanomi4ejpx42fpmffoac0"},
},
{
XMLName: xml.Name{},
Name: "9aynsym60",
Ask: "",
Jid: "9aynsym60zbu78jbdvpho7s68@crypho.com",
Subscription: SubscriptionBoth,
Groups: []string{"mzaoy73i6ra5k502182zi1t97"},
},
{
XMLName: xml.Name{},
Name: "admin",
Ask: "",
Jid: "admin@crypho.com",
Subscription: SubscriptionBoth,
Groups: noGroup,
},
}
if len(pp.Items) != len(items) {
t.Errorf("List length mismatch: %#v", pp.Items)
} else {
for i, item := range pp.Items {
if item.Jid != items[i].Jid {
t.Errorf("Jid Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if !reflect.DeepEqual(item.Groups, items[i].Groups) {
t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if item.Name != items[i].Name {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if item.Ask != items[i].Ask {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
if item.Subscription != items[i].Subscription {
t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid)
}
}
}
}
func checkMarshalling(t *testing.T, iq *IQ) (*IQ, error) {
// Marshall
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
return nil, err
}
// Unmarshall
var parsedIQ IQ
err = xml.Unmarshal(data, &parsedIQ)
if err != nil {
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
}
return &parsedIQ, err
}
+240
View File
@@ -0,0 +1,240 @@
package stanza_test
import (
"encoding/xml"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestUnmarshalIqs(t *testing.T) {
//var cs1 = new(iot.ControlSet)
var tests = []struct {
iqString string
parsedIQ stanza.IQ
}{
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.IQTypeSet, To: "test@localhost", Id: "1"}}},
//{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}},
}
for _, test := range tests {
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal(%s) returned error", test.iqString)
}
if !xmlEqual(parsedIQ, test.parsedIQ) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ, test.parsedIQ))
}
}
}
func TestGenerateIqId(t *testing.T) {
t.Parallel()
iq, err := stanza.NewIQ(stanza.Attrs{Id: "1", Type: "dummy type"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
if iq.Id != "1" {
t.Errorf("NewIQ replaced id with %s", iq.Id)
}
iq, err = stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
if iq.Id == "" {
t.Error("NewIQ did not generate an Id")
}
otherIq, err := stanza.NewIQ(stanza.Attrs{Type: "dummy type"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
if iq.Id == otherIq.Id {
t.Errorf("NewIQ generated two identical ids: %s", iq.Id)
}
}
func TestGenerateIq(t *testing.T) {
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
payload := stanza.DiscoInfo{
Identity: []stanza.Identity{
{Name: "Test Gateway",
Category: "gateway",
Type: "mqtt",
}},
Features: []stanza.Feature{
{Var: stanza.NSDiscoInfo},
{Var: stanza.NSDiscoItems},
},
}
iq.Payload = &payload
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
if strings.Contains(string(data), "<error ") {
t.Error("empty error should not be serialized")
}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(iq.Payload, parsedIQ.Payload) {
t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload))
}
}
func TestErrorTag(t *testing.T) {
xError := stanza.Err{
XMLName: xml.Name{Local: "error"},
Code: 503,
Type: "cancel",
Reason: "service-unavailable",
Text: "User session not found",
}
data, err := xml.Marshal(xError)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
}
parsedError := stanza.Err{}
if err = xml.Unmarshal(data, &parsedError); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedError, xError) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedError, xError))
}
}
func TestDiscoItems(t *testing.T) {
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
payload := stanza.DiscoItems{
Node: "music",
}
iq.Payload = &payload
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedIQ.Payload, iq.Payload) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload))
}
}
func TestUnmarshalPayload(t *testing.T) {
query := "<iq to='service.localhost' type='get' id='1'><query xmlns='jabber:iq:version'/></iq>"
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(query), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal(%s) returned error", query)
}
if parsedIQ.Payload == nil {
t.Error("Missing payload")
}
namespace := parsedIQ.Payload.Namespace()
if namespace != "jabber:iq:version" {
t.Errorf("incorrect namespace: %s", namespace)
}
}
func TestPayloadWithError(t *testing.T) {
iq := `<iq xml:lang='en' to='test1@localhost/resource' from='test@localhost' type='error' id='aac1a'>
<query xmlns='jabber:iq:version'/>
<error code='407' type='auth'>
<subscription-required xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
<text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Not subscribed</text>
</error>
</iq>`
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %s", iq)
return
}
if parsedIQ.Error.Reason != "subscription-required" {
t.Errorf("incorrect error value: '%s'", parsedIQ.Error.Reason)
}
}
func TestUnknownPayload(t *testing.T) {
iq := `<iq type="get" to="service.localhost" id="1" >
<query xmlns="unknown:ns"/>
</iq>`
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %#v (%s)", err, iq)
return
}
if parsedIQ.Any.XMLName.Space != "unknown:ns" {
t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space)
}
}
func TestIsValid(t *testing.T) {
type testCase struct {
iq string
shouldErr bool
}
testIQs := make(map[string]testCase)
testIQs["Valid IQ"] = testCase{
`<iq type="get" to="service.localhost" id="1" >
<query xmlns="unknown:ns"/>
</iq>`,
false,
}
testIQs["Invalid IQ"] = testCase{
`<iq type="get" to="service.localhost">
<query xmlns="unknown:ns"/>
</iq>`,
true,
}
for name, tcase := range testIQs {
t.Run(name, func(st *testing.T) {
parsedIQ := stanza.IQ{}
err := xml.Unmarshal([]byte(tcase.iq), &parsedIQ)
if err != nil {
t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq)
return
}
isValid, err := parsedIQ.IsValid()
if !isValid && !tcase.shouldErr {
t.Errorf("failed validation for iq because: %s\nin test case : %s", err, tcase.iq)
}
})
}
}
+51
View File
@@ -0,0 +1,51 @@
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"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (v *Version) Namespace() string {
return v.XMLName.Space
}
func (v *Version) GetSet() *ResultSet {
return v.ResultSet
}
// ---------------
// Builder helpers
// Version builds a default software version payload
func (iq *IQ) Version() *Version {
d := Version{
XMLName: xml.Name{Space: "jabber:iq:version", Local: "query"},
}
iq.Payload = &d
return &d
}
// Set all software version info
func (v *Version) SetInfo(name, version, os string) *Version {
v.Name = name
v.Version = version
v.OS = os
return v
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "jabber:iq:version", Local: "query"}, Version{})
}
+43
View File
@@ -0,0 +1,43 @@
package stanza_test
import (
"testing"
"gosrc.io/xmpp/stanza"
)
// Build a Software Version reply
// https://xmpp.org/extensions/xep-0092.html#example-2
func TestVersion_Builder(t *testing.T) {
name := "Exodus"
version := "0.7.0.4"
os := "Windows-XP 5.01.2600"
iq, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard",
To: "juliet@capulet.com/balcony", Id: "version_1"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
iq.Version().SetInfo(name, version, os)
parsedIQ, err := checkMarshalling(t, iq)
if err != nil {
return
}
// Check result
pp, ok := parsedIQ.Payload.(*stanza.Version)
if !ok {
t.Errorf("Parsed stanza does not contain correct IQ payload")
}
// Check version info
if pp.Name != name {
t.Errorf("Name Mismatch (expected: %s): %s", name, pp.Name)
}
if pp.Version != version {
t.Errorf("Version Mismatch (expected: %s): %s", version, pp.Version)
}
if pp.OS != os {
t.Errorf("OS Mismatch (expected: %s): %s", os, pp.OS)
}
}
+101
View File
@@ -0,0 +1,101 @@
package stanza
import (
"fmt"
"strings"
"unicode"
)
type Jid struct {
Node string
Domain string
Resource string
}
func NewJid(sjid string) (*Jid, error) {
jid := new(Jid)
if sjid == "" {
return jid, fmt.Errorf("jid cannot be empty")
}
s1 := strings.SplitN(sjid, "@", 2)
if len(s1) == 1 { // This is a server or component Jid
jid.Domain = s1[0]
} else { // Jid has a local username part
if s1[0] == "" {
return jid, fmt.Errorf("invalid jid '%s", sjid)
}
jid.Node = s1[0]
if s1[1] == "" {
return jid, fmt.Errorf("domain cannot be empty")
}
jid.Domain = s1[1]
}
// Extract resource from domain field
s2 := strings.SplitN(jid.Domain, "/", 2)
if len(s2) == 2 { // If len = 1, domain is already correct, and resource is already empty
jid.Domain = s2[0]
jid.Resource = s2[1]
}
if !isUsernameValid(jid.Node) {
return jid, fmt.Errorf("invalid Node in Jid '%s'", sjid)
}
if !isDomainValid(jid.Domain) {
return jid, fmt.Errorf("invalid domain in Jid '%s'", sjid)
}
return jid, nil
}
func (j *Jid) Full() string {
if j.Resource == "" {
return j.Bare()
} else if j.Node == "" {
return j.Node + "/" + j.Resource
} else {
return j.Node + "@" + j.Domain + "/" + j.Resource
}
}
func (j *Jid) Bare() string {
if j.Node == "" {
return j.Domain
} else {
return j.Node + "@" + j.Domain
}
}
// ============================================================================
// Helpers, for parsing / validation
func isUsernameValid(username string) bool {
invalidRunes := []rune{'@', '/', '\'', '"', ':', '<', '>'}
return strings.IndexFunc(username, isInvalid(invalidRunes)) < 0
}
func isDomainValid(domain string) bool {
if len(domain) == 0 {
return false
}
invalidRunes := []rune{'@', '/'}
return strings.IndexFunc(domain, isInvalid(invalidRunes)) < 0
}
func isInvalid(invalidRunes []rune) func(c rune) bool {
isInvalid := func(c rune) bool {
if unicode.IsSpace(c) {
return true
}
for _, r := range invalidRunes {
if c == r {
return true
}
}
return false
}
return isInvalid
}
+101
View File
@@ -0,0 +1,101 @@
package stanza
import (
"testing"
)
func TestValidJids(t *testing.T) {
tests := []struct {
jidstr string
expected Jid
}{
{jidstr: "test@domain.com", expected: Jid{"test", "domain.com", ""}},
{jidstr: "test@domain.com/resource", expected: Jid{"test", "domain.com", "resource"}},
// resource can contain '/' or '@'
{jidstr: "test@domain.com/a/b", expected: Jid{"test", "domain.com", "a/b"}},
{jidstr: "test@domain.com/a@b", expected: Jid{"test", "domain.com", "a@b"}},
{jidstr: "domain.com", expected: Jid{"", "domain.com", ""}},
}
for _, tt := range tests {
jid, err := NewJid(tt.jidstr)
if err != nil {
t.Errorf("could not parse correct jid: %s", tt.jidstr)
continue
}
if jid == nil {
t.Error("jid should not be nil")
}
if jid.Node != tt.expected.Node {
t.Errorf("incorrect jid Node (%s): %s", tt.expected.Node, jid.Node)
}
if jid.Node != tt.expected.Node {
t.Errorf("incorrect jid domain (%s): %s", tt.expected.Domain, jid.Domain)
}
if jid.Resource != tt.expected.Resource {
t.Errorf("incorrect jid resource (%s): %s", tt.expected.Resource, jid.Resource)
}
}
}
func TestIncorrectJids(t *testing.T) {
badJids := []string{
"",
"user@",
"@domain.com",
"user:name@domain.com",
"user<name@domain.com",
"test@domain.com@otherdomain.com",
"test@domain com/resource",
}
for _, sjid := range badJids {
if _, err := NewJid(sjid); err == nil {
t.Error("parsing incorrect jid should return error: " + sjid)
}
}
}
func TestFull(t *testing.T) {
fullJids := []string{
"test@domain.com/my resource",
"test@domain.com",
"domain.com",
}
for _, sjid := range fullJids {
parsedJid, err := NewJid(sjid)
if err != nil {
t.Errorf("could not parse jid: %v", err)
}
fullJid := parsedJid.Full()
if fullJid != sjid {
t.Errorf("incorrect full jid: %s", fullJid)
}
}
}
func TestBare(t *testing.T) {
tests := []struct {
jidstr string
expected string
}{
{jidstr: "test@domain.com", expected: "test@domain.com"},
{jidstr: "test@domain.com/resource", expected: "test@domain.com"},
{jidstr: "domain.com", expected: "domain.com"},
}
for _, tt := range tests {
parsedJid, err := NewJid(tt.jidstr)
if err != nil {
t.Errorf("could not parse jid: %v", err)
}
bareJid := parsedJid.Bare()
if bareJid != tt.expected {
t.Errorf("incorrect bare jid: %s", bareJid)
}
}
}
+148
View File
@@ -0,0 +1,148 @@
package stanza
import (
"encoding/xml"
"reflect"
)
// ============================================================================
// Message Packet
// Message implements RFC 6120 - A.5 Client Namespace (a part)
type Message struct {
XMLName xml.Name `xml:"message"`
Attrs
Subject string `xml:"subject,omitempty"`
Body string `xml:"body,omitempty"`
Thread string `xml:"thread,omitempty"`
Error Err `xml:"error,omitempty"`
Extensions []MsgExtension `xml:",omitempty"`
}
func (Message) Name() string {
return "message"
}
func NewMessage(a Attrs) Message {
return Message{
XMLName: xml.Name{Local: "message"},
Attrs: a,
}
}
// Get search and extracts a specific extension on a message.
// It receives a pointer to an MsgExtension. It will panic if the caller
// does not pass a pointer.
// It will return true if the passed extension is found and set the pointer
// to the extension passed as parameter to the found extension.
// It will return false if the extension is not found on the message.
//
// Example usage:
// var oob xmpp.OOB
// if ok := msg.Get(&oob); ok {
// // oob extension has been found
// }
func (msg *Message) Get(ext MsgExtension) bool {
target := reflect.ValueOf(ext)
if target.Kind() != reflect.Ptr {
panic("you must pass a pointer to the message Get method")
}
for _, e := range msg.Extensions {
if reflect.TypeOf(e) == target.Type() {
source := reflect.ValueOf(e)
if source.Kind() != reflect.Ptr {
source = source.Elem()
}
target.Elem().Set(source.Elem())
return true
}
}
return false
}
type messageDecoder struct{}
var message messageDecoder
func (messageDecoder) decode(p *xml.Decoder, se xml.StartElement) (Message, error) {
var packet Message
err := p.DecodeElement(&packet, &se)
return packet, err
}
// XMPPFormat with all Extensions
func (msg *Message) XMPPFormat() string {
out, err := xml.MarshalIndent(msg, "", "")
if err != nil {
return ""
}
return string(out)
}
// UnmarshalXML implements custom parsing for messages
func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
msg.XMLName = start.Name
// Extract packet attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
msg.Id = attr.Value
}
if attr.Name.Local == "type" {
msg.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
msg.To = attr.Value
}
if attr.Name.Local == "from" {
msg.From = attr.Value
}
if attr.Name.Local == "lang" {
msg.Lang = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if msgExt := TypeRegistry.GetMsgExtension(tt.Name); msgExt != nil {
// Decode message extension
err = d.DecodeElement(msgExt, &tt)
if err != nil {
return err
}
msg.Extensions = append(msg.Extensions, msgExt)
} else {
// Decode standard message sub-elements
var err error
switch tt.Name.Local {
case "body":
err = d.DecodeElement(&msg.Body, &tt)
case "thread":
err = d.DecodeElement(&msg.Thread, &tt)
case "subject":
err = d.DecodeElement(&msg.Subject, &tt)
case "error":
err = d.DecodeElement(&msg.Error, &tt)
}
if err != nil {
return err
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestGenerateMessage(t *testing.T) {
message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"})
message.Body = "Hi"
message.Subject = "Msg Subject"
data, err := xml.Marshal(message)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
parsedMessage := stanza.Message{}
if err = xml.Unmarshal(data, &parsedMessage); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedMessage, message) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedMessage, message))
}
}
func TestDecodeError(t *testing.T) {
str := `<message from='juliet@capulet.com'
id='msg_1'
to='romeo@montague.lit'
type='error'>
<error type='cancel'>
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</message>`
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message error stanza unmarshall error: %v", err)
return
}
if parsedMessage.Error.Type != "cancel" {
t.Errorf("incorrect error type: %s", parsedMessage.Error.Type)
}
}
func TestGetOOB(t *testing.T) {
image := "https://localhost/image.png"
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
ext := stanza.OOB{
XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"},
URL: image,
}
msg.Extensions = append(msg.Extensions, &ext)
// OOB can properly be found
var oob stanza.OOB
// Try to find and
if ok := msg.Get(&oob); !ok {
t.Error("could not find oob extension")
return
}
if oob.URL != image {
t.Errorf("OOB URL was not properly extracted: ''%s", oob.URL)
}
// Markable is not found
var m stanza.Markable
if ok := msg.Get(&m); ok {
t.Error("we should not have found markable extension")
}
}
+42
View File
@@ -0,0 +1,42 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0333 - Chat Markers: https://xmpp.org/extensions/xep-0333.html
*/
const NSMsgChatMarkers = "urn:xmpp:chat-markers:0"
type Markable struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 markable"`
}
type MarkReceived struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 received"`
ID string `xml:"id,attr"`
}
type MarkDisplayed struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 displayed"`
ID string `xml:"id,attr"`
}
type MarkAcknowledged struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:chat-markers:0 acknowledged"`
ID string `xml:"id,attr"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "markable"}, Markable{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "received"}, MarkReceived{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "displayed"}, MarkDisplayed{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "acknowledged"}, MarkAcknowledged{})
}
+45
View File
@@ -0,0 +1,45 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0085 - Chat State Notifications: https://xmpp.org/extensions/xep-0085.html
*/
const NSMsgChatStateNotifications = "http://jabber.org/protocol/chatstates"
type StateActive struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates active"`
}
type StateComposing struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates composing"`
}
type StateGone struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates gone"`
}
type StateInactive struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates inactive"`
}
type StatePaused struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/chatstates paused"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "active"}, StateActive{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "composing"}, StateComposing{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "gone"}, StateGone{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "inactive"}, StateInactive{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "paused"}, StatePaused{})
}
+36
View File
@@ -0,0 +1,36 @@
package stanza
import "encoding/xml"
/*
Support for:
- XEP-0334: Message Processing Hints: https://xmpp.org/extensions/xep-0334.html
Pointers should be used to keep consistent with unmarshal. Eg :
msg.Extensions = append(msg.Extensions, &stanza.HintNoCopy{}, &stanza.HintStore{})
*/
type HintNoPermanentStore struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints no-permanent-store"`
}
type HintNoStore struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints no-store"`
}
type HintNoCopy struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints no-copy"`
}
type HintStore struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:hints store"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-permanent-store"}, HintNoPermanentStore{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-store"}, HintNoStore{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-copy"}, HintNoCopy{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "store"}, HintStore{})
}
+72
View File
@@ -0,0 +1,72 @@
package stanza_test
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
"reflect"
"strings"
"testing"
)
const msg_const = `
<message
from="romeo@montague.lit/laptop"
to="juliet@capulet.lit/laptop">
<body>V unir avtugf pybnx gb uvqr zr sebz gurve fvtug</body>
<no-copy xmlns="urn:xmpp:hints"></no-copy>
<no-permanent-store xmlns="urn:xmpp:hints"></no-permanent-store>
<no-store xmlns="urn:xmpp:hints"></no-store>
<store xmlns="urn:xmpp:hints"></store>
</message>`
func TestSerializationHint(t *testing.T) {
msg := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
msg.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
msg.Extensions = append(msg.Extensions, stanza.HintNoCopy{}, stanza.HintNoPermanentStore{}, stanza.HintNoStore{}, stanza.HintStore{})
data, _ := xml.Marshal(msg)
if strings.ReplaceAll(strings.Join(strings.Fields(msg_const), ""), "\n", "") != strings.Join(strings.Fields(string(data)), "") {
t.Fatalf("marshalled message does not match expected message")
}
}
func TestUnmarshalHints(t *testing.T) {
// Init message as in the const value
msgConst := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"})
msgConst.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug"
msgConst.Extensions = append(msgConst.Extensions, &stanza.HintNoCopy{}, &stanza.HintNoPermanentStore{}, &stanza.HintNoStore{}, &stanza.HintStore{})
// Compare message with the const value
msg := stanza.Message{}
err := xml.Unmarshal([]byte(msg_const), &msg)
if err != nil {
t.Fatal(err)
}
if msgConst.XMLName.Local != msg.XMLName.Local {
t.Fatalf("message tags do not match. Expected: %s, Actual: %s", msgConst.XMLName.Local, msg.XMLName.Local)
}
if msgConst.Body != msg.Body {
t.Fatalf("message bodies do not match. Expected: %s, Actual: %s", msgConst.Body, msg.Body)
}
if !reflect.DeepEqual(msgConst.Attrs, msg.Attrs) {
t.Fatalf("attributes do not match")
}
if !reflect.DeepEqual(msgConst.Error, msg.Error) {
t.Fatalf("attributes do not match")
}
var found bool
for _, ext := range msgConst.Extensions {
for _, strExt := range msg.Extensions {
if reflect.TypeOf(ext) == reflect.TypeOf(strExt) {
found = true
break
}
}
if !found {
t.Fatalf("extensions do not match")
}
found = false
}
}
+22
View File
@@ -0,0 +1,22 @@
package stanza
import (
"encoding/xml"
)
type HTML struct {
MsgExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/xhtml-im html"`
Body HTMLBody
Lang string `xml:"xml:lang,attr,omitempty"`
}
type HTMLBody struct {
XMLName xml.Name `xml:"http://www.w3.org/1999/xhtml body"`
// InnerXML MUST be valid xhtml. We do not check if it is valid when generating the XMPP stanza.
InnerXML string `xml:",innerxml"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/xhtml-im", Local: "html"}, HTML{})
}
+44
View File
@@ -0,0 +1,44 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestHTMLGen(t *testing.T) {
htmlBody := "<p>Hello <b>World</b></p>"
msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"})
msg.Body = "Hello World"
body := stanza.HTMLBody{
InnerXML: htmlBody,
}
html := stanza.HTML{Body: body}
msg.Extensions = append(msg.Extensions, html)
result := msg.XMPPFormat()
str := `<message to="test@localhost"><body>Hello World</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>Hello <b>World</b></p></body></html></message>`
if result != str {
t.Errorf("incorrect serialize message:\n%s", result)
}
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message HTML unmarshall error: %v", err)
return
}
if parsedMessage.Body != msg.Body {
t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body)
}
var h stanza.HTML
if ok := parsedMessage.Get(&h); !ok {
t.Error("could not extract HTML body")
}
if h.Body.InnerXML != htmlBody {
t.Errorf("could not extract html body: '%s'", h.Body.InnerXML)
}
}
+21
View File
@@ -0,0 +1,21 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0066 - Out of Band Data: https://xmpp.org/extensions/xep-0066.html
*/
type OOB struct {
MsgExtension
XMLName xml.Name `xml:"jabber:x:oob x"`
URL string `xml:"url"`
Desc string `xml:"desc,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "jabber:x:oob", Local: "x"}, OOB{})
}
+213
View File
@@ -0,0 +1,213 @@
package stanza
import (
"encoding/xml"
)
// Implementation of the http://jabber.org/protocol/pubsub#event namespace
type PubSubEvent struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#event event"`
MsgExtension
EventElement EventElement
//List ItemsEvent
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, PubSubEvent{})
}
type EventElement interface {
Name() string
}
// *********************
// Collection
// *********************
const PubSubCollectionEventName = "Collection"
type CollectionEvent struct {
AssocDisassoc AssocDisassoc
Node string `xml:"node,attr,omitempty"`
}
func (c CollectionEvent) Name() string {
return PubSubCollectionEventName
}
// *********************
// Associate/Disassociate
// *********************
type AssocDisassoc interface {
GetAssocDisassoc() string
}
// *********************
// Associate
// *********************
const Assoc = "Associate"
type AssociateEvent struct {
XMLName xml.Name `xml:"associate"`
Node string `xml:"node,attr"`
}
func (a *AssociateEvent) GetAssocDisassoc() string {
return Assoc
}
// *********************
// Disassociate
// *********************
const Disassoc = "Disassociate"
type DisassociateEvent struct {
XMLName xml.Name `xml:"disassociate"`
Node string `xml:"node,attr"`
}
func (e *DisassociateEvent) GetAssocDisassoc() string {
return Disassoc
}
// *********************
// Configuration
// *********************
const PubSubConfigEventName = "Configuration"
type ConfigurationEvent struct {
Node string `xml:"node,attr,omitempty"`
Form *Form
}
func (c ConfigurationEvent) Name() string {
return PubSubConfigEventName
}
// *********************
// Delete
// *********************
const PubSubDeleteEventName = "Delete"
type DeleteEvent struct {
Node string `xml:"node,attr"`
Redirect *RedirectEvent `xml:"redirect"`
}
func (c DeleteEvent) Name() string {
return PubSubConfigEventName
}
// *********************
// Redirect
// *********************
type RedirectEvent struct {
URI string `xml:"uri,attr"`
}
// *********************
// List
// *********************
const PubSubItemsEventName = "List"
type ItemsEvent struct {
XMLName xml.Name `xml:"items"`
Items []ItemEvent `xml:"item,omitempty"`
Node string `xml:"node,attr"`
Retract *RetractEvent `xml:"retract"`
}
type ItemEvent struct {
XMLName xml.Name `xml:"item"`
Id string `xml:"id,attr,omitempty"`
Publisher string `xml:"publisher,attr,omitempty"`
Any *Node `xml:",any"`
}
func (i ItemsEvent) Name() string {
return PubSubItemsEventName
}
// *********************
// List
// *********************
type RetractEvent struct {
XMLName xml.Name `xml:"retract"`
ID string `xml:"node,attr"`
}
// *********************
// Purge
// *********************
const PubSubPurgeEventName = "Purge"
type PurgeEvent struct {
XMLName xml.Name `xml:"purge"`
Node string `xml:"node,attr"`
}
func (p PurgeEvent) Name() string {
return PubSubPurgeEventName
}
// *********************
// Subscription
// *********************
const PubSubSubscriptionEventName = "Subscription"
type SubscriptionEvent struct {
SubStatus string `xml:"subscription,attr,omitempty"`
Expiry string `xml:"expiry,attr,omitempty"`
SubInfo `xml:",omitempty"`
}
func (s SubscriptionEvent) Name() string {
return PubSubSubscriptionEventName
}
func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
pse.XMLName = start.Name
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
var ee EventElement
switch tt := t.(type) {
case xml.StartElement:
switch tt.Name.Local {
case "collection":
ee = &CollectionEvent{}
case "configuration":
ee = &ConfigurationEvent{}
case "delete":
ee = &DeleteEvent{}
case "items":
ee = &ItemsEvent{}
case "purge":
ee = &PurgeEvent{}
case "subscription":
ee = &SubscriptionEvent{}
default:
ee = nil
}
// known child element found, decode it
if ee != nil {
err = d.DecodeElement(ee, &tt)
if err != nil {
return err
}
pse.EventElement = ee
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
+162
View File
@@ -0,0 +1,162 @@
package stanza_test
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
"strings"
"testing"
)
func TestDecodeMsgEvent(t *testing.T) {
str := `<message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo'>
<event xmlns='http://jabber.org/protocol/pubsub#event'>
<items node='princely_musings'>
<item id='ae890ac52d0df67ed7cfdf51b644e901'>
<entry xmlns='http://www.w3.org/2005/Atom'>
<title>Soliloquy</title>
<summary>
To be, or not to be: that is the question:
Whether 'tis nobler in the mind to suffer
The slings and arrows of outrageous fortune,
Or to take arms against a sea of troubles,
And by opposing end them?
</summary>
<link rel='alternate' type='text/html'
href='http://denmark.lit/2003/12/13/atom03'/>
<id>tag:denmark.lit,2003:entry-32397</id>
<published>2003-12-13T18:30:02Z</published>
<updated>2003-12-13T18:30:02Z</updated>
</entry>
</item>
</items>
</event>
</message>
`
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message receipt unmarshall error: %v", err)
return
}
if parsedMessage.Body != "" {
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
}
if len(parsedMessage.Extensions) < 1 {
t.Errorf("no extension found on parsed message")
return
}
switch ext := parsedMessage.Extensions[0].(type) {
case *stanza.PubSubEvent:
if ext.XMLName.Local != "event" {
t.Fatalf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
}
tmp, ok := parsedMessage.Extensions[0].(*stanza.PubSubEvent)
if !ok {
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
}
ie, ok := tmp.EventElement.(*stanza.ItemsEvent)
if !ok {
t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
}
if ie.Items[0].Any.Nodes[0].Content != "Soliloquy" {
t.Fatalf("could not read title ! Read this : %s", ie.Items[0].Any.Nodes[0].Content)
}
if len(ie.Items[0].Any.Nodes) != 6 {
t.Fatalf("some nodes were not correctly parsed")
}
default:
t.Fatalf("could not find pubsub event extension")
}
}
func TestEncodeEvent(t *testing.T) {
expected := "<message><event xmlns=\"http://jabber.org/protocol/pubsub#event\">" +
"<items node=\"princely_musings\"><item id=\"ae890ac52d0df67ed7cfdf51b644e901\">" +
"<entry xmlns=\"http://www.w3.org/2005/Atom\"><title>My pub item title</title>" +
"<summary>My pub item content summary</summary><link rel=\"alternate\" " +
"type=\"text/html\" href=\"http://denmark.lit/2003/12/13/atom03\">" +
"</link><id>My pub item content ID</id><published>2003-12-13T18:30:02Z</published>" +
"<updated>2003-12-13T18:30:02Z</updated></entry></item></items></event></message>"
message := stanza.Message{
Extensions: []stanza.MsgExtension{
stanza.PubSubEvent{
EventElement: stanza.ItemsEvent{
Items: []stanza.ItemEvent{
{
Id: "ae890ac52d0df67ed7cfdf51b644e901",
Any: &stanza.Node{
XMLName: xml.Name{
Space: "http://www.w3.org/2005/Atom",
Local: "entry",
},
Attrs: nil,
Content: "",
Nodes: []stanza.Node{
{
XMLName: xml.Name{Space: "", Local: "title"},
Attrs: nil,
Content: "My pub item title",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "summary"},
Attrs: nil,
Content: "My pub item content summary",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "link"},
Attrs: []xml.Attr{
{
Name: xml.Name{Space: "", Local: "rel"},
Value: "alternate",
},
{
Name: xml.Name{Space: "", Local: "type"},
Value: "text/html",
},
{
Name: xml.Name{Space: "", Local: "href"},
Value: "http://denmark.lit/2003/12/13/atom03",
},
},
},
{
XMLName: xml.Name{Space: "", Local: "id"},
Attrs: nil,
Content: "My pub item content ID",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "published"},
Attrs: nil,
Content: "2003-12-13T18:30:02Z",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "updated"},
Attrs: nil,
Content: "2003-12-13T18:30:02Z",
Nodes: nil,
},
},
},
},
},
Node: "princely_musings",
Retract: nil,
},
},
},
}
data, _ := xml.Marshal(message)
if strings.TrimSpace(string(data)) != strings.TrimSpace(expected) {
t.Errorf("event was not encoded properly : \nexpected:%s \ngot: %s", expected, string(data))
}
}
+29
View File
@@ -0,0 +1,29 @@
package stanza
import (
"encoding/xml"
)
/*
Support for:
- XEP-0184 - Message Delivery Receipts: https://xmpp.org/extensions/xep-0184.html
*/
const NSMsgReceipts = "urn:xmpp:receipts"
// Used on outgoing message, to tell the recipient that you are requesting a message receipt / ack.
type ReceiptRequest struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:receipts request"`
}
type ReceiptReceived struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:receipts received"`
ID string `xml:"id,attr"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "request"}, ReceiptRequest{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "received"}, ReceiptReceived{})
}
+42
View File
@@ -0,0 +1,42 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestDecodeRequest(t *testing.T) {
str := `<message
from='northumberland@shakespeare.lit/westminster'
id='richard2-4.1.247'
to='kingrichard@royalty.england.lit/throne'>
<body>My lord, dispatch; read o'er these articles.</body>
<request xmlns='urn:xmpp:receipts'/>
</message>`
parsedMessage := stanza.Message{}
if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil {
t.Errorf("message receipt unmarshall error: %v", err)
return
}
if parsedMessage.Body != "My lord, dispatch; read o'er these articles." {
t.Errorf("Unexpected body: '%s'", parsedMessage.Body)
}
if len(parsedMessage.Extensions) < 1 {
t.Errorf("no extension found on parsed message")
return
}
switch ext := parsedMessage.Extensions[0].(type) {
case *stanza.ReceiptRequest:
if ext.XMLName.Local != "request" {
t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local)
}
default:
t.Errorf("could not find receipts extension")
}
}
+63
View File
@@ -0,0 +1,63 @@
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:",cdata"`
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)
if err != nil {
return err
}
err = e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
if err != nil {
return err
}
if n.Content != "" {
err = e.EncodeToken(xml.CharData(n.Content))
if err != nil {
return err
}
}
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
+33
View File
@@ -0,0 +1,33 @@
package stanza
import (
"encoding/xml"
"testing"
)
func TestNode_Marshal(t *testing.T) {
jsonData := []byte("{\"key\":\"value\"}")
iqResp, err := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
iqResp.Any = &Node{
XMLName: xml.Name{Space: "myNS", Local: "space"},
Content: string(jsonData),
}
bytes, err := xml.Marshal(iqResp)
if err != nil {
t.Errorf("Could not marshal XML: %v", err)
}
parsedIQ := IQ{}
if err := xml.Unmarshal(bytes, &parsedIQ); err != nil {
t.Errorf("Unmarshal returned error: %v", err)
}
if parsedIQ.Any.Content != string(jsonData) {
t.Errorf("Cannot find generic any payload in parsedIQ: '%s'", parsedIQ.Any.Content)
}
}
+12
View File
@@ -0,0 +1,12 @@
package stanza
const (
NSStream = "http://etherx.jabber.org/streams"
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
NSFraming = "urn:ietf:params:xml:ns:xmpp-framing"
NSClient = "jabber:client"
NSComponent = "jabber:component:accept"
)
+13
View File
@@ -0,0 +1,13 @@
package stanza
import "encoding/xml"
// Open Packet
// Reference: WebSocket connections must start with this element
// https://tools.ietf.org/html/rfc7395#section-3.4
type WebsocketOpen struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-framing open"`
From string `xml:"from,attr"`
Id string `xml:"id,attr"`
Version string `xml:"version,attr"`
}
+18
View File
@@ -0,0 +1,18 @@
package stanza
type Packet interface {
Name() string
}
// Attrs represents the common structure for base XMPP packets.
type Attrs struct {
Type StanzaType `xml:"type,attr,omitempty"`
Id string `xml:"id,attr,omitempty"`
From string `xml:"from,attr,omitempty"`
To string `xml:"to,attr,omitempty"`
Lang string `xml:"lang,attr,omitempty"`
}
type packetFormatter interface {
XMPPFormat() string
}
+31
View File
@@ -0,0 +1,31 @@
package stanza
import "strings"
type StanzaType string
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
const (
IQTypeError StanzaType = "error"
IQTypeGet StanzaType = "get"
IQTypeResult StanzaType = "result"
IQTypeSet StanzaType = "set"
MessageTypeChat StanzaType = "chat"
MessageTypeError StanzaType = "error"
MessageTypeGroupchat StanzaType = "groupchat"
MessageTypeHeadline StanzaType = "headline"
MessageTypeNormal StanzaType = "normal" // Default
PresenceTypeError StanzaType = "error"
PresenceTypeProbe StanzaType = "probe"
PresenceTypeSubscribe StanzaType = "subscribe"
PresenceTypeSubscribed StanzaType = "subscribed"
PresenceTypeUnavailable StanzaType = "unavailable"
PresenceTypeUnsubscribe StanzaType = "unsubscribe"
PresenceTypeUnsubscribed StanzaType = "unsubscribed"
)
func (s StanzaType) IsEmpty() bool {
return len(strings.TrimSpace(string(s))) == 0
}
+199
View File
@@ -0,0 +1,199 @@
package stanza
import (
"encoding/xml"
"errors"
"fmt"
"io"
)
// Reads and checks the opening XMPP stream element.
// TODO It returns a stream structure containing:
// - Host: You can check the host against the host you were expecting to connect to
// - Id: the Stream ID is a temporary shared secret used for some hash calculation. It is also used by ProcessOne
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
// getting through the authentication process.
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
func InitStream(p *xml.Decoder) (sessionID string, err error) {
for {
var t xml.Token
t, err = p.Token()
if err != nil {
return sessionID, err
}
switch elem := t.(type) {
case xml.StartElement:
isStreamOpen := elem.Name.Space == NSStream && elem.Name.Local == "stream"
isFrameOpen := elem.Name.Space == NSFraming && elem.Name.Local == "open"
if !isStreamOpen && !isFrameOpen {
err = errors.New("xmpp: expected <stream> or <open> but got <" + elem.Name.Local + "> in " + elem.Name.Space)
return sessionID, err
}
// Parse XMPP stream attributes
for _, attrs := range elem.Attr {
switch attrs.Name.Local {
case "id":
sessionID = attrs.Value
}
}
return sessionID, err
}
}
}
// NextPacket scans XML token stream for next complete XMPP stanza.
// Once the type of stanza has been identified, a structure is created to decode
// that stanza and returned.
// TODO Use an interface to return packets interface xmppDecoder
// TODO make auth and bind use NextPacket instead of directly NextStart
func NextPacket(p *xml.Decoder) (Packet, error) {
// Read start element to find out how we want to parse the XMPP packet
t, err := NextXmppToken(p)
if err != nil {
return nil, err
}
if ee, ok := t.(xml.EndElement); ok {
return decodeStream(p, ee)
}
// If not an end element, then must be a start
se, ok := t.(xml.StartElement)
if !ok {
return nil, errors.New("unknown token ")
}
// Decode one of the top level XMPP namespace
switch se.Name.Space {
case NSStream:
return decodeStream(p, se)
case NSSASL:
return decodeSASL(p, se)
case NSClient:
return decodeClient(p, se)
case NSComponent:
return decodeComponent(p, se)
case NSStreamManagement:
return sm.decode(p, se)
default:
return nil, errors.New("unknown namespace " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// NextXmppToken scans XML token stream to find next StartElement or stream EndElement.
// We need the EndElement scan, because we must register stream close tags
func NextXmppToken(p *xml.Decoder) (xml.Token, 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
case xml.EndElement:
if t.Name.Space == NSStream && t.Name.Local == "stream" {
return t, nil
}
}
}
}
// NextStart scans XML token stream to find next StartElement.
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err == io.EOF {
return xml.StartElement{}, errors.New("connection closed")
}
if err != nil {
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
/*
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
type.
That way, we have a consistent way to do type assertion, always matching against pointers.
*/
// decodeStream will fully decode a stream packet
func decodeStream(p *xml.Decoder, t xml.Token) (Packet, error) {
if se, ok := t.(xml.StartElement); ok {
switch se.Name.Local {
case "error":
return streamError.decode(p, se)
case "features":
return streamFeatures.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
if ee, ok := t.(xml.EndElement); ok {
if ee.Name.Local == "stream" {
return streamClose.decode(ee), nil
}
return nil, errors.New("unexpected XMPP packet " +
ee.Name.Space + " <" + ee.Name.Local + "/>")
}
// Should not happen
return nil, errors.New("unexpected XML token ")
}
// decodeSASL decodes a packet related to SASL authentication.
func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "success":
return saslSuccess.decode(p, se)
case "failure":
return saslFailure.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// decodeClient decodes all known packets in the client namespace.
func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "message":
return message.decode(p, se)
case "presence":
return presence.decode(p, se)
case "iq":
return iq.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// decodeComponent decodes all known packets in the component namespace.
func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "handshake": // handshake is used to authenticate components
return handshake.decode(p, se)
case "message":
return message.decode(p, se)
case "presence":
return presence.decode(p, se)
case "iq":
return iq.decode(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
+27
View File
@@ -0,0 +1,27 @@
package stanza
import (
"encoding/xml"
)
type Tune struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/tune tune"`
Artist string `xml:"artist,omitempty"`
Length int `xml:"length,omitempty"`
Rating int `xml:"rating,omitempty"`
Source string `xml:"source,omitempty"`
Title string `xml:"title,omitempty"`
Track string `xml:"track,omitempty"`
Uri string `xml:"uri,omitempty"`
}
// Mood defines data model for XEP-0107 - User Mood
// See: https://xmpp.org/extensions/xep-0107.html
type Mood struct {
MsgExtension // Mood can be added as a message extension
XMLName xml.Name `xml:"http://jabber.org/protocol/mood mood"`
// TODO: Custom parsing to extract mood type from tag name.
// Note: the list is predefined.
// Mood type
Text string `xml:"text,omitempty"`
}
+148
View File
@@ -0,0 +1,148 @@
package stanza
import (
"encoding/xml"
"strconv"
"time"
)
// ============================================================================
// MUC Presence extension
// MucPresence implements XEP-0045: Multi-User Chat - 19.1
type MucPresence struct {
PresExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/muc x"`
Password string `xml:"password,omitempty"`
History History `xml:"history,omitempty"`
}
const timeLayout = "2006-01-02T15:04:05Z"
// History implements XEP-0045: Multi-User Chat - 19.1
type History struct {
XMLName xml.Name
MaxChars NullableInt `xml:"maxchars,attr,omitempty"`
MaxStanzas NullableInt `xml:"maxstanzas,attr,omitempty"`
Seconds NullableInt `xml:"seconds,attr,omitempty"`
Since time.Time `xml:"since,attr,omitempty"`
}
type NullableInt struct {
Value int
isSet bool
}
func NewNullableInt(val int) NullableInt {
return NullableInt{val, true}
}
func (n NullableInt) Get() (v int, ok bool) {
return n.Value, n.isSet
}
// UnmarshalXML implements custom parsing for history element
func (h *History) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
h.XMLName = start.Name
// Extract attributes
for _, attr := range start.Attr {
switch attr.Name.Local {
case "maxchars":
v, err := strconv.Atoi(attr.Value)
if err != nil {
return err
}
h.MaxChars = NewNullableInt(v)
case "maxstanzas":
v, err := strconv.Atoi(attr.Value)
if err != nil {
return err
}
h.MaxStanzas = NewNullableInt(v)
case "seconds":
v, err := strconv.Atoi(attr.Value)
if err != nil {
return err
}
h.Seconds = NewNullableInt(v)
case "since":
t, err := time.Parse(timeLayout, attr.Value)
if err != nil {
return err
}
h.Since = t
}
}
// Consume remaining data until element end
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
mc, isMcSet := h.MaxChars.Get()
ms, isMsSet := h.MaxStanzas.Get()
s, isSSet := h.Seconds.Get()
// We do not have any value, ignore history element
if h.Since.IsZero() && !isMcSet && !isMsSet && !isSSet {
return nil
}
// Encode start element and attributes
start.Name = xml.Name{Local: "history"}
if isMcSet {
attr := xml.Attr{
Name: xml.Name{Local: "maxchars"},
Value: strconv.Itoa(mc),
}
start.Attr = append(start.Attr, attr)
}
if isMsSet {
attr := xml.Attr{
Name: xml.Name{Local: "maxstanzas"},
Value: strconv.Itoa(ms),
}
start.Attr = append(start.Attr, attr)
}
if isSSet {
attr := xml.Attr{
Name: xml.Name{Local: "seconds"},
Value: strconv.Itoa(s),
}
start.Attr = append(start.Attr, attr)
}
if !h.Since.IsZero() {
attr := xml.Attr{
Name: xml.Name{Local: "since"},
Value: h.Since.Format(timeLayout),
}
start.Attr = append(start.Attr, attr)
}
if err := e.EncodeToken(start); err != nil {
return err
}
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
func init() {
TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc", Local: "x"}, MucPresence{})
}
+97
View File
@@ -0,0 +1,97 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// https://xmpp.org/extensions/xep-0045.html#example-27
func TestMucPassword(t *testing.T) {
str := `<presence
from='hag66@shakespeare.lit/pda'
id='djn4714'
to='coven@chat.shakespeare.lit/thirdwitch'>
<x xmlns='http://jabber.org/protocol/muc'>
<password>cauldronburn</password>
</x>
</presence>`
var parsedPresence stanza.Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", str)
}
var muc stanza.MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
}
if muc.Password != "cauldronburn" {
t.Errorf("incorrect password: '%s'", muc.Password)
}
}
// https://xmpp.org/extensions/xep-0045.html#example-37
func TestMucHistory(t *testing.T) {
str := `<presence
from='hag66@shakespeare.lit/pda'
id='n13mt3l'
to='coven@chat.shakespeare.lit/thirdwitch'>
<x xmlns='http://jabber.org/protocol/muc'>
<history maxstanzas='20'/>
</x>
</presence>`
var parsedPresence stanza.Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", str, err)
return
}
var muc stanza.MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
return
}
if v, ok := muc.History.MaxStanzas.Get(); !ok || v != 20 {
t.Errorf("incorrect MaxStanzas: '%#v'", muc.History.MaxStanzas)
}
}
// https://xmpp.org/extensions/xep-0045.html#example-37
func TestMucNoHistory(t *testing.T) {
str := "<presence" +
" id=\"n13mt3l\"" +
" from=\"hag66@shakespeare.lit/pda\"" +
" to=\"coven@chat.shakespeare.lit/thirdwitch\">" +
"<x xmlns=\"http://jabber.org/protocol/muc\">" +
"<history maxstanzas=\"0\"></history>" +
"</x>" +
"</presence>"
maxstanzas := 0
pres := stanza.Presence{Attrs: stanza.Attrs{
From: "hag66@shakespeare.lit/pda",
Id: "n13mt3l",
To: "coven@chat.shakespeare.lit/thirdwitch",
},
Extensions: []stanza.PresExtension{
stanza.MucPresence{
History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)},
},
},
}
data, err := xml.Marshal(&pres)
if err != nil {
t.Error("error on encode:", err)
return
}
if string(data) != str {
t.Errorf("incorrect stanza: \n%s\n%s", str, data)
}
}
+139
View File
@@ -0,0 +1,139 @@
package stanza
import (
"encoding/xml"
"reflect"
)
// ============================================================================
// Presence Packet
// Presence implements RFC 6120 - A.5 Client Namespace (a part)
type Presence struct {
XMLName xml.Name `xml:"presence"`
Attrs
Show PresenceShow `xml:"show,omitempty"`
Status string `xml:"status,omitempty"`
Priority int8 `xml:"priority,omitempty"` // default: 0
Error Err `xml:"error,omitempty"`
Extensions []PresExtension `xml:",omitempty"`
}
func (Presence) Name() string {
return "presence"
}
func NewPresence(a Attrs) Presence {
return Presence{
XMLName: xml.Name{Local: "presence"},
Attrs: a,
}
}
// Get search and extracts a specific extension on a presence stanza.
// It receives a pointer to an PresExtension. It will panic if the caller
// does not pass a pointer.
// It will return true if the passed extension is found and set the pointer
// to the extension passed as parameter to the found extension.
// It will return false if the extension is not found on the presence.
//
// Example usage:
// var muc xmpp.MucPresence
// if ok := msg.Get(&muc); ok {
// // muc presence extension has been found
// }
func (pres *Presence) Get(ext PresExtension) bool {
target := reflect.ValueOf(ext)
if target.Kind() != reflect.Ptr {
panic("you must pass a pointer to the message Get method")
}
for _, e := range pres.Extensions {
if reflect.TypeOf(e) == target.Type() {
source := reflect.ValueOf(e)
if source.Kind() != reflect.Ptr {
source = source.Elem()
}
target.Elem().Set(source.Elem())
return true
}
}
return false
}
type presenceDecoder struct{}
var presence presenceDecoder
func (presenceDecoder) decode(p *xml.Decoder, se xml.StartElement) (Presence, error) {
var packet Presence
err := p.DecodeElement(&packet, &se)
// TODO Add default presence type (when omitted)
return packet, err
}
// UnmarshalXML implements custom parsing for presence stanza
func (pres *Presence) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
pres.XMLName = start.Name
// Extract packet attributes
for _, attr := range start.Attr {
if attr.Name.Local == "id" {
pres.Id = attr.Value
}
if attr.Name.Local == "type" {
pres.Type = StanzaType(attr.Value)
}
if attr.Name.Local == "to" {
pres.To = attr.Value
}
if attr.Name.Local == "from" {
pres.From = attr.Value
}
if attr.Name.Local == "lang" {
pres.Lang = attr.Value
}
}
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if presExt := TypeRegistry.GetPresExtension(tt.Name); presExt != nil {
// Decode message extension
err = d.DecodeElement(presExt, &tt)
if err != nil {
return err
}
pres.Extensions = append(pres.Extensions, presExt)
} else {
// Decode standard message sub-elements
var err error
switch tt.Name.Local {
case "show":
err = d.DecodeElement(&pres.Show, &tt)
case "status":
err = d.DecodeElement(&pres.Status, &tt)
case "priority":
err = d.DecodeElement(&pres.Priority, &tt)
case "error":
err = d.DecodeElement(&pres.Error, &tt)
}
if err != nil {
return err
}
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
package stanza
// PresenceShow is a Enum of presence element show
type PresenceShow string
// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace
const (
PresenceShowAway PresenceShow = "away"
PresenceShowChat PresenceShow = "chat"
PresenceShowDND PresenceShow = "dnd"
PresenceShowXA PresenceShow = "xa"
)
+63
View File
@@ -0,0 +1,63 @@
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
func TestGeneratePresence(t *testing.T) {
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = stanza.PresenceShowChat
data, err := xml.Marshal(presence)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
var parsedPresence stanza.Presence
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if !xmlEqual(parsedPresence, presence) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedPresence, presence))
}
}
func TestPresenceSubElt(t *testing.T) {
// Test structure to ensure that show, status and priority are correctly defined as presence
// package sub-elements
type pres struct {
Show stanza.PresenceShow `xml:"show"`
Status string `xml:"status"`
Priority int8 `xml:"priority"`
}
presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = stanza.PresenceShowXA
presence.Status = "Coding"
presence.Priority = 10
data, err := xml.Marshal(presence)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
var parsedPresence pres
if err = xml.Unmarshal(data, &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
if parsedPresence.Show != presence.Show {
t.Errorf("cannot read 'show' as presence subelement (%s)", parsedPresence.Show)
}
if parsedPresence.Status != presence.Status {
t.Errorf("cannot read 'status' as presence subelement (%s)", parsedPresence.Status)
}
if parsedPresence.Priority != presence.Priority {
t.Errorf("cannot read 'priority' as presence subelement (%d)", parsedPresence.Priority)
}
}
+433
View File
@@ -0,0 +1,433 @@
package stanza
import (
"encoding/xml"
"errors"
"strings"
)
type PubSubGeneric struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
Create *Create `xml:"create,omitempty"`
Configure *Configure `xml:"configure,omitempty"`
Subscribe *SubInfo `xml:"subscribe,omitempty"`
SubOptions *SubOptions `xml:"options,omitempty"`
Publish *Publish `xml:"publish,omitempty"`
PublishOptions *PublishOptions `xml:"publish-options"`
Affiliations *Affiliations `xml:"affiliations,omitempty"`
Default *Default `xml:"default,omitempty"`
Items *Items `xml:"items,omitempty"`
Retract *Retract `xml:"retract,omitempty"`
Subscription *Subscription `xml:"subscription,omitempty"`
Subscriptions *Subscriptions `xml:"subscriptions,omitempty"`
// To use in responses to sub/unsub for instance
// Subscription options
Unsubscribe *SubInfo `xml:"unsubscribe,omitempty"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (p *PubSubGeneric) Namespace() string {
return p.XMLName.Space
}
func (p *PubSubGeneric) GetSet() *ResultSet {
return p.ResultSet
}
type Affiliations struct {
List []Affiliation `xml:"affiliation"`
Node string `xml:"node,attr,omitempty"`
}
type Affiliation struct {
AffiliationStatus string `xml:"affiliation"`
Node string `xml:"node,attr"`
}
type Create struct {
Node string `xml:"node,attr,omitempty"`
}
type SubOptions struct {
SubInfo
Form *Form `xml:"x"`
}
type Configure struct {
Form *Form `xml:"x"`
}
type Default struct {
Node string `xml:"node,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Form *Form `xml:"x"`
}
type Subscribe struct {
XMLName xml.Name `xml:"subscribe"`
SubInfo
}
type Unsubscribe struct {
XMLName xml.Name `xml:"unsubscribe"`
SubInfo
}
// SubInfo represents information about a subscription
// Node is the node related to the subscription
// Jid is the subscription JID of the subscribed entity
// SubID is the subscription ID
type SubInfo struct {
Node string `xml:"node,attr,omitempty"`
Jid string `xml:"jid,attr,omitempty"`
// Sub ID is optional
SubId *string `xml:"subid,attr,omitempty"`
}
// validate checks if a node and a jid are present in the sub info, and if this jid is valid.
func (si *SubInfo) validate() error {
// Requests MUST contain a valid JID
if _, err := NewJid(si.Jid); err != nil {
return err
}
// SubInfo must contain both a valid JID and a node. See XEP-0060
if strings.TrimSpace(si.Node) == "" {
return errors.New("SubInfo must contain the node AND the subscriber JID in subscription config options requests")
}
return nil
}
// Handles the "5.6 Retrieve Subscriptions" of XEP-0060
type Subscriptions struct {
XMLName xml.Name `xml:"subscriptions"`
List []Subscription `xml:"subscription,omitempty"`
}
// Handles the "5.6 Retrieve Subscriptions" and the 6.1 Subscribe to a Node and so on of XEP-0060
type Subscription struct {
SubStatus string `xml:"subscription,attr,omitempty"`
SubInfo `xml:",omitempty"`
// Seems like we can't marshal a self-closing tag for now : https://github.com/golang/go/issues/21399
// subscribe-options should be like this as per XEP-0060:
// <subscribe-options>
// <required/>
// </subscribe-options>
// Used to indicate if configuration options is required.
Required *struct{}
}
type PublishOptions struct {
XMLName xml.Name `xml:"publish-options"`
Form *Form
}
type Publish struct {
XMLName xml.Name `xml:"publish"`
Node string `xml:"node,attr"`
Items []Item `xml:"item,omitempty"` // xsd says there can be many. See also 12.10 Batch Processing of XEP-0060
}
type Items struct {
List []Item `xml:"item,omitempty"`
MaxItems int `xml:"max_items,attr,omitempty"`
Node string `xml:"node,attr"`
SubId string `xml:"subid,attr,omitempty"`
}
type Item struct {
XMLName xml.Name `xml:"item"`
Id string `xml:"id,attr,omitempty"`
Publisher string `xml:"publisher,attr,omitempty"`
Any *Node `xml:",any"`
}
type Retract struct {
XMLName xml.Name `xml:"retract"`
Node string `xml:"node,attr"`
Notify *bool `xml:"notify,attr,omitempty"`
Items []Item `xml:"item"`
}
type PubSubOption struct {
XMLName xml.Name `xml:"jabber:x:data options"`
Form `xml:"x"`
}
// NewSubRq builds a subscription request to a node at the given service.
// It's a Set type IQ.
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
// 6.1 Subscribe to a Node
func NewSubRq(serviceId string, subInfo SubInfo) (*IQ, error) {
if e := subInfo.validate(); e != nil {
return nil, e
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Subscribe: &subInfo,
}
return iq, nil
}
// NewUnsubRq builds an unsub request to a node at the given service.
// It's a Set type IQ
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
// 6.2 Unsubscribe from a Node
func NewUnsubRq(serviceId string, subInfo SubInfo) (*IQ, error) {
if e := subInfo.validate(); e != nil {
return nil, e
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Unsubscribe: &subInfo,
}
return iq, nil
}
// NewSubOptsRq builds a request for the subscription options.
// It's a Get type IQ
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
// 6.3 Configure Subscription Options
func NewSubOptsRq(serviceId string, subInfo SubInfo) (*IQ, error) {
if e := subInfo.validate(); e != nil {
return nil, e
}
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
SubOptions: &SubOptions{
SubInfo: subInfo,
},
}
return iq, nil
}
// NewFormSubmission builds a form submission pubsub IQ
// Information about the subscription and the requester are separated. subInfo contains information about the subscription.
// 6.3.5 Form Submission
func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) {
if e := subInfo.validate(); e != nil {
return nil, e
}
if form.Type != FormTypeSubmit {
return nil, errors.New("form type was expected to be submit but was : " + form.Type)
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
SubOptions: &SubOptions{
SubInfo: subInfo,
Form: form,
},
}
return iq, nil
}
// NewSubAndConfig builds a subscribe request that contains configuration options for the service
// From XEP-0060 : The <options/> element MUST follow the <subscribe/> element and
// MUST NOT possess a 'node' attribute or 'jid' attribute,
// since the value of the <subscribe/> element's 'node' attribute specifies the desired NodeID and
// the value of the <subscribe/> element's 'jid' attribute specifies the subscriber's JID
// 6.3.7 Subscribe and Configure
func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) {
if e := subInfo.validate(); e != nil {
return nil, e
}
if form.Type != FormTypeSubmit {
return nil, errors.New("form type was expected to be submit but was : " + form.Type)
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Subscribe: &subInfo,
SubOptions: &SubOptions{
SubInfo: SubInfo{SubId: subInfo.SubId},
Form: form,
},
}
return iq, nil
}
// NewItemsRequest creates a request to query existing items from a node.
// Specify a "maxItems" value to request only the last maxItems items. If 0, requests all items.
// 6.5.2 Requesting All List AND 6.5.7 Requesting the Most Recent List
func NewItemsRequest(serviceId string, node string, maxItems int) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Items: &Items{Node: node},
}
if maxItems != 0 {
ps, _ := iq.Payload.(*PubSubGeneric)
ps.Items.MaxItems = maxItems
}
return iq, nil
}
// NewItemsRequest creates a request to get a specific item from a node.
// 6.5.8 Requesting a Particular Item
func NewSpecificItemRequest(serviceId, node, itemId string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Items: &Items{Node: node,
List: []Item{
{
Id: itemId,
},
},
},
}
return iq, nil
}
// NewPublishItemRq creates a request to publish a single item to a node identified by its provided ID
func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (*IQ, error) {
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
if strings.TrimSpace(nodeID) == "" {
return nil, errors.New("cannot publish without a target node ID")
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Publish: &Publish{Node: nodeID, Items: []Item{item}},
}
// "The <item/> element provided by the publisher MAY possess an 'id' attribute,
// specifying a unique ItemID for the item.
// If an ItemID is not provided in the publish request,
// the pubsub service MUST generate one and MUST ensure that it is unique for that node."
if strings.TrimSpace(pubItemID) != "" {
ps, _ := iq.Payload.(*PubSubGeneric)
ps.Publish.Items[0].Id = pubItemID
}
return iq, nil
}
// NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options
// A pubsub service MAY support the ability to specify options along with a publish request
//(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature).
func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (*IQ, error) {
// "The <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
if strings.TrimSpace(nodeID) == "" {
return nil, errors.New("cannot publish without a target node ID")
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Publish: &Publish{Node: nodeID, Items: items},
PublishOptions: options,
}
return iq, nil
}
// NewDelItemFromNode creates a request to delete and item from a node, given its id.
// To delete an item, the publisher sends a retract request.
// This helper function follows 7.2 Delete an Item from a Node
func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (*IQ, error) {
// "The <retract/> element MUST possess a 'node' attribute, specifying the NodeID of the node."
if strings.TrimSpace(nodeID) == "" {
return nil, errors.New("cannot delete item without a target node ID")
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Retract: &Retract{Node: nodeID, Items: []Item{{Id: itemId}}, Notify: notify},
}
return iq, nil
}
// NewCreateAndConfigNode makes a request for node creation that has the desired node configuration.
// See 8.1.3 Create and Configure a Node
func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Create: &Create{Node: nodeID},
Configure: &Configure{Form: confForm},
}
return iq, nil
}
// NewCreateNode builds a request to create a node on the service referenced by "serviceId"
// See 8.1 Create a Node
func NewCreateNode(serviceId, nodeName string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Create: &Create{Node: nodeName},
}
return iq, nil
}
// NewRetrieveAllSubsRequest builds a request to retrieve all subscriptions from all nodes
// In order to make the request, the requesting entity MUST send an IQ-get whose <pubsub/>
// child contains an empty <subscriptions/> element with no attributes.
func NewRetrieveAllSubsRequest(serviceId string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Subscriptions: &Subscriptions{},
}
return iq, nil
}
// NewRetrieveAllAffilsRequest builds a request to retrieve all affiliations from all nodes
// In order to make the request of the service, the requesting entity includes an empty <affiliations/> element with no attributes.
func NewRetrieveAllAffilsRequest(serviceId string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubGeneric{
Affiliations: &Affiliations{},
}
return iq, nil
}
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{})
}
+458
View File
@@ -0,0 +1,458 @@
package stanza
import (
"encoding/xml"
"errors"
"strings"
)
type PubSubOwner struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"`
OwnerUseCase OwnerUseCase
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (pso *PubSubOwner) Namespace() string {
return pso.XMLName.Space
}
func (pso *PubSubOwner) GetSet() *ResultSet {
return pso.ResultSet
}
type OwnerUseCase interface {
UseCase() string
}
type AffiliationsOwner struct {
XMLName xml.Name `xml:"affiliations"`
Affiliations []AffiliationOwner `xml:"affiliation,omitempty"`
Node string `xml:"node,attr"`
}
func (AffiliationsOwner) UseCase() string {
return "affiliations"
}
type AffiliationOwner struct {
XMLName xml.Name `xml:"affiliation"`
AffiliationStatus string `xml:"affiliation,attr"`
Jid string `xml:"jid,attr"`
}
const (
AffiliationStatusMember = "member"
AffiliationStatusNone = "none"
AffiliationStatusOutcast = "outcast"
AffiliationStatusOwner = "owner"
AffiliationStatusPublisher = "publisher"
AffiliationStatusPublishOnly = "publish-only"
)
type ConfigureOwner struct {
XMLName xml.Name `xml:"configure"`
Node string `xml:"node,attr,omitempty"`
Form *Form `xml:"x,omitempty"`
}
func (*ConfigureOwner) UseCase() string {
return "configure"
}
type DefaultOwner struct {
XMLName xml.Name `xml:"default"`
Form *Form `xml:"x,omitempty"`
}
func (*DefaultOwner) UseCase() string {
return "default"
}
type DeleteOwner struct {
XMLName xml.Name `xml:"delete"`
RedirectOwner *RedirectOwner `xml:"redirect,omitempty"`
Node string `xml:"node,attr,omitempty"`
}
func (*DeleteOwner) UseCase() string {
return "delete"
}
type RedirectOwner struct {
XMLName xml.Name `xml:"redirect"`
URI string `xml:"uri,attr"`
}
type PurgeOwner struct {
XMLName xml.Name `xml:"purge"`
Node string `xml:"node,attr"`
}
func (*PurgeOwner) UseCase() string {
return "purge"
}
type SubscriptionsOwner struct {
XMLName xml.Name `xml:"subscriptions"`
Subscriptions []SubscriptionOwner `xml:"subscription"`
Node string `xml:"node,attr"`
}
func (*SubscriptionsOwner) UseCase() string {
return "subscriptions"
}
type SubscriptionOwner struct {
SubscriptionStatus string `xml:"subscription"`
Jid string `xml:"jid,attr"`
}
const (
SubscriptionStatusNone = "none"
SubscriptionStatusPending = "pending"
SubscriptionStatusSubscribed = "subscribed"
SubscriptionStatusUnconfigured = "unconfigured"
)
// NewConfigureNode creates a request to configure a node on the given service.
// A form will be returned by the service, to which the user must respond using for instance the NewFormSubmission function.
// See 8.2 Configure a Node
func NewConfigureNode(serviceId, nodeName string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &ConfigureOwner{Node: nodeName},
}
return iq, nil
}
// NewDelNode creates a request to delete node "nodeID" from the "serviceId" service
// See 8.4 Delete a Node
func NewDelNode(serviceId, nodeID string) (*IQ, error) {
if strings.TrimSpace(nodeID) == "" {
return nil, errors.New("cannot delete a node without a target node ID")
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &DeleteOwner{Node: nodeID},
}
return iq, nil
}
// NewPurgeAllItems creates a new purge request for the "nodeId" node, at "serviceId" service
// See 8.5 Purge All Node Items
func NewPurgeAllItems(serviceId, nodeId string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &PurgeOwner{Node: nodeId},
}
return iq, nil
}
// NewRequestDefaultConfig build a request to ask the service for the default config of its nodes
// See 8.3 Request Default Node Configuration Options
func NewRequestDefaultConfig(serviceId string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &DefaultOwner{},
}
return iq, nil
}
// NewApproveSubRequest creates a new sub approval response to a request from the service to the owner of the node
// In order to approve the request, the owner shall submit the form and set the "pubsub#allow" field to a value of "1" or "true"
// For tracking purposes the message MUST reflect the 'id' attribute originally provided in the request.
// See 8.6 Manage Subscription Requests
func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, error) {
if serviceId == "" {
return Message{}, errors.New("need a target service serviceId send approval serviceId")
}
if reqID == "" {
return Message{}, errors.New("the request ID is empty but must be used for the approval")
}
if apprForm == nil {
return Message{}, errors.New("approval form is nil")
}
apprMess := NewMessage(Attrs{To: serviceId})
apprMess.Extensions = []MsgExtension{apprForm}
apprMess.Id = reqID
return apprMess, nil
}
// NewGetPendingSubRequests creates a new request for all pending subscriptions to all their nodes at a service
// This feature MUST be implemented using the Ad-Hoc Commands (XEP-0050) protocol
// 8.7 Process Pending Subscription Requests
func NewGetPendingSubRequests(serviceId string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &Command{
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
Node: "http://jabber.org/protocol/pubsub#get-pending",
Action: CommandActionExecute,
}
return iq, nil
}
// NewGetPendingSubRequests creates a new request for all pending subscriptions to be approved on a given node
// Upon receiving the data form for managing subscription requests, the owner then MAY request pending subscription
// approval requests for a given node.
// See 8.7.4 Per-Node Request
func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (*IQ, error) {
if sessionId == "" {
return nil, errors.New("the sessionId must be maintained for the command")
}
form := &Form{
Type: FormTypeSubmit,
Fields: []*Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}},
}
data, err := xml.Marshal(form)
if err != nil {
return nil, err
}
var n Node
err = xml.Unmarshal(data, &n)
if err != nil {
return nil, err
}
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &Command{
// the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending"
Node: "http://jabber.org/protocol/pubsub#get-pending",
Action: CommandActionExecute,
SessionId: sessionId,
CommandElements: []CommandElement{&n},
}
return iq, nil
}
// NewSubListRequest creates a request to list subscriptions of the client, for all nodes at the service.
// It's a Get type IQ
// 8.8.1 Retrieve Subscriptions
func NewSubListRqPl(serviceId, nodeID string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &SubscriptionsOwner{Node: nodeID},
}
return iq, nil
}
func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs},
}
return iq, nil
}
// NewModifAffiliationRequest creates a request to either modify one or more affiliations, or delete one or more affiliations
// 8.9.2 Modify Affiliation & 8.9.2.4 Multiple Simultaneous Modifications & 8.9.3 Delete an Entity (just set the status to "none")
func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &AffiliationsOwner{
Node: nodeID,
Affiliations: newAffils,
},
}
return iq, nil
}
// NewAffiliationListRequest creates a request to list all affiliated entities
// See 8.9.1 Retrieve List List
func NewAffiliationListRequest(serviceId, nodeID string) (*IQ, error) {
iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId})
if err != nil {
return nil, err
}
iq.Payload = &PubSubOwner{
OwnerUseCase: &AffiliationsOwner{
Node: nodeID,
},
}
return iq, nil
}
// NewFormSubmission builds a form submission pubsub IQ, in the Owner namespace
// This is typically used to respond to a form issued by the server when configuring a node.
// See 8.2.4 Form Submission
func NewFormSubmissionOwner(serviceId, nodeName string, fields []*Field) (*IQ, error) {
if serviceId == "" || nodeName == "" {
return nil, errors.New("serviceId and nodeName must be filled for this request to be valid")
}
submitConf, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId})
if err != nil {
return nil, err
}
submitConf.Payload = &PubSubOwner{
OwnerUseCase: &ConfigureOwner{
Node: nodeName,
Form: NewForm(fields,
FormTypeSubmit)},
}
return submitConf, nil
}
// GetFormFields gets the fields from a form in a IQ stanza of type result, as a map.
// Key is the "var" attribute of the field, and field is the value.
// The user can then select and modify the fields they want to alter, and submit a new form to the service using the
// NewFormSubmission function to build the IQ.
// TODO : remove restriction on IQ type ?
func (iq *IQ) GetFormFields() (map[string]*Field, error) {
if iq.Type != IQTypeResult {
return nil, errors.New("this IQ is not a result type IQ. Cannot extract the form from it")
}
switch payload := iq.Payload.(type) {
// We support IOT Control IQ
case *PubSubGeneric:
fieldMap := make(map[string]*Field)
for _, elt := range payload.Configure.Form.Fields {
fieldMap[elt.Var] = elt
}
return fieldMap, nil
case *PubSubOwner:
fieldMap := make(map[string]*Field)
co, ok := payload.OwnerUseCase.(*ConfigureOwner)
if !ok {
return nil, errors.New("this IQ does not contain a PubSub payload with a configure tag for the owner namespace")
}
for _, elt := range co.Form.Fields {
fieldMap[elt.Var] = elt
}
return fieldMap, nil
case *Command:
fieldMap := make(map[string]*Field)
var form *Form
for _, ce := range payload.CommandElements {
fo, ok := ce.(*Form)
if ok {
form = fo
break
}
}
if form == nil {
return nil, errors.New("this IQ does not contain a command payload with a form")
}
for _, elt := range form.Fields {
fieldMap[elt.Var] = elt
}
return fieldMap, nil
default:
if iq.Any != nil {
fieldMap := make(map[string]*Field)
if iq.Any.XMLName.Local != "command" {
return nil, errors.New("this IQ does not contain a form")
}
for _, nde := range iq.Any.Nodes {
if nde.XMLName.Local == "x" {
for _, n := range nde.Nodes {
if n.XMLName.Local == "field" {
f := Field{}
data, err := xml.Marshal(n)
if err != nil {
continue
}
err = xml.Unmarshal(data, &f)
if err == nil {
fieldMap[f.Var] = &f
}
}
}
}
}
return fieldMap, nil
}
return nil, errors.New("this IQ does not contain a form")
}
}
func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
pso.XMLName = start.Name
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
// Decode sub-elements
var err error
switch tt.Name.Local {
case "affiliations":
aff := AffiliationsOwner{}
err = d.DecodeElement(&aff, &tt)
pso.OwnerUseCase = &aff
case "configure":
co := ConfigureOwner{}
err = d.DecodeElement(&co, &tt)
pso.OwnerUseCase = &co
case "default":
def := DefaultOwner{}
err = d.DecodeElement(&def, &tt)
pso.OwnerUseCase = &def
case "delete":
del := DeleteOwner{}
err = d.DecodeElement(&del, &tt)
pso.OwnerUseCase = &del
case "purge":
pu := PurgeOwner{}
err = d.DecodeElement(&pu, &tt)
pso.OwnerUseCase = &pu
case "subscriptions":
subs := SubscriptionsOwner{}
err = d.DecodeElement(&subs, &tt)
pso.OwnerUseCase = &subs
if err != nil {
return err
}
}
if err != nil {
return err
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub#owner", Local: "pubsub"}, PubSubOwner{})
}
+886
View File
@@ -0,0 +1,886 @@
package stanza_test
import (
"encoding/xml"
"errors"
"testing"
"gosrc.io/xmpp/stanza"
)
// ******************************
// * 8.2 Configure a Node
// ******************************
func TestNewConfigureNode(t *testing.T) {
expectedReq := "<iq type=\"get\" id=\"config1\" to=\"pubsub.shakespeare.lit\" > " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"></configure> " +
"</pubsub> </iq>"
subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings")
if err != nil {
t.Fatalf("failed to create a configure node request: %v", err)
}
subR.Id = "config1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
if !ok {
t.Fatalf("owner use case is not a configure tag")
}
if ownrUsecase.Node == "" {
t.Fatalf("could not parse node from config tag")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewConfigureNodeResp(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<configure node="princely_musings">
<x type="form" xmlns="jabber:x:data">
<field type="hidden" var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#node_config</value>
</field>
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
<value>0</value>
</field>
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
<value>1028</value>
</field>
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
<option label="Never">
<value>never</value>
</option>
<option label="When a new subscription is processed">
<value>on_sub</value>
</option>
<option label="When a new subscription is processed and whenever a subscriber comes online">
<value>on_sub_and_presence</value>
</option>
<value>never</value>
</field>
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
<value>0</value>
</field>
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
<option>
<value>normal</value>
</option>
<option>
<value>headline</value>
</option>
<value>headline</value>
</field>
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
<value>http://www.w3.org/2005/Atom</value>
</field>
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
</x>
</configure>
</pubsub>
</iq>
`
pubsub, err := getPubSubOwnerPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
if !ok {
t.Fatalf("owner use case is not a configure tag")
}
if ownrUsecase.Form == nil {
t.Fatalf("form is nil in the parsed config tag")
}
if len(ownrUsecase.Form.Fields) != 8 {
t.Fatalf("one or more fields in the response form could not be parsed correctly")
}
}
// *************************************************
// * 8.3 Request Default Node Configuration Options
// *************************************************
func TestNewRequestDefaultConfig(t *testing.T) {
expectedReq := "<iq type=\"get\" id=\"def1\" to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <default></default> </pubsub> </iq>"
subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit")
if err != nil {
t.Fatalf("failed to create a default config request: %v", err)
}
subR.Id = "def1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
_, ok = pubsub.OwnerUseCase.(*stanza.DefaultOwner)
if !ok {
t.Fatalf("owner use case is not a default tag")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewRequestDefaultConfigResp(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<configure node="princely_musings">
<x type="form" xmlns="jabber:x:data">
<field type="hidden" var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#node_config</value>
</field>
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
<value>0</value>
</field>
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
<value>1028</value>
</field>
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
<option label="Never">
<value>never</value>
</option>
<option label="When a new subscription is processed">
<value>on_sub</value>
</option>
<option label="When a new subscription is processed and whenever a subscriber comes online">
<value>on_sub_and_presence</value>
</option>
<value>never</value>
</field>
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
<value>0</value>
</field>
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
<option>
<value>normal</value>
</option>
<option>
<value>headline</value>
</option>
<value>headline</value>
</field>
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
<value>http://www.w3.org/2005/Atom</value>
</field>
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
</x>
</configure>
</pubsub>
</iq>
`
pubsub, err := getPubSubOwnerPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
if !ok {
t.Fatalf("owner use case is not a configure tag")
}
if ownrUsecase.Form == nil {
t.Fatalf("form is nil in the parsed config tag")
}
if len(ownrUsecase.Form.Fields) != 8 {
t.Fatalf("one or more fields in the response form could not be parsed correctly")
}
}
// ***********************
// * 8.4 Delete a Node
// ***********************
func TestNewDelNode(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"delete1\" to=\"pubsub.shakespeare.lit\" >" +
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
"<delete node=\"princely_musings\"></delete> </pubsub> </iq>"
subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings")
if err != nil {
t.Fatalf("failed to create a node delete request: %v", err)
}
subR.Id = "delete1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
_, ok = pubsub.OwnerUseCase.(*stanza.DeleteOwner)
if !ok {
t.Fatalf("owner use case is not a delete tag")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewDelNodeResp(t *testing.T) {
response := `
<iq id="delete1" to="pubsub.shakespeare.lit" type="set">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<delete node="princely_musings">
<redirect uri="xmpp:hamlet@denmark.lit"/>
</delete>
</pubsub>
</iq>
`
pubsub, err := getPubSubOwnerPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.DeleteOwner)
if !ok {
t.Fatalf("owner use case is not a configure tag")
}
if ownrUsecase.RedirectOwner == nil {
t.Fatalf("redirect is nil in the delete tag")
}
if ownrUsecase.RedirectOwner.URI == "" {
t.Fatalf("could not parse redirect uri")
}
}
// ****************************
// * 8.5 Purge All Node Items
// ****************************
func TestNewPurgeAllItems(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"purge1\" to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
"<purge node=\"princely_musings\"></purge> </pubsub> </iq>"
subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings")
if err != nil {
t.Fatalf("failed to create a purge all items request: %v", err)
}
subR.Id = "purge1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.OwnerUseCase == nil {
t.Fatalf("owner use case is nil")
}
purge, ok := pubsub.OwnerUseCase.(*stanza.PurgeOwner)
if !ok {
t.Fatalf("owner use case is not a delete tag")
}
if purge.Node == "" {
t.Fatalf("could not parse purge targer node")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ************************************
// * 8.6 Manage Subscription Requests
// ************************************
func TestNewApproveSubRequest(t *testing.T) {
expectedReq := "<message id=\"approve1\" to=\"pubsub.shakespeare.lit\"> " +
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
"<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value> </field> <field var=\"pubsub#subid\">" +
" <value>123-abc</value> </field> <field var=\"pubsub#node\"> <value>princely_musings</value> </field> " +
"<field var=\"pubsub#subscriber_jid\"> <value>horatio@denmark.lit</value> </field> <field var=\"pubsub#allow\"> " +
"<value>true</value> </field> </x> </message>"
apprForm := &stanza.Form{
Type: stanza.FormTypeSubmit,
Fields: []*stanza.Field{
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}},
{Var: "pubsub#subid", ValuesList: []string{"123-abc"}},
{Var: "pubsub#node", ValuesList: []string{"princely_musings"}},
{Var: "pubsub#subscriber_jid", ValuesList: []string{"horatio@denmark.lit"}},
{Var: "pubsub#allow", ValuesList: []string{"true"}},
},
}
subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm)
if err != nil {
t.Fatalf("failed to create a sub approval request: %v", err)
}
subR.Id = "approve1"
frm, ok := subR.Extensions[0].(*stanza.Form)
if !ok {
t.Fatalf("extension is not a from !")
}
var allowField *stanza.Field
for _, f := range frm.Fields {
if f.Var == "pubsub#allow" {
allowField = f
}
}
if allowField == nil || allowField.ValuesList[0] != "true" {
t.Fatalf("could not correctly parse the allow field in the response from")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ********************************************
// * 8.7 Process Pending Subscription Requests
// ********************************************
func TestNewGetPendingSubRequests(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"pending1\" to=\"pubsub.shakespeare.lit\" > " +
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\" node=\"http://jabber.org/protocol/pubsub#get-pending\" >" +
"</command> </iq>"
subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit")
if err != nil {
t.Fatalf("failed to create a get pending subs request: %v", err)
}
subR.Id = "pending1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
command, ok := subR.Payload.(*stanza.Command)
if !ok {
t.Fatalf("payload is not a command !")
}
if command.Action != stanza.CommandActionExecute {
t.Fatalf("command should be execute !")
}
if command.Node != "http://jabber.org/protocol/pubsub#get-pending" {
t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewGetPendingSubRequestsResp(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
<x type="form" xmlns="jabber:x:data">
<field type="hidden" var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
</field>
<field type="list-single" var="pubsub#node">
<option>
<value>princely_musings</value>
</option>
<option>
<value>news_from_elsinore</value>
</option>
</field>
</x>
</command>
</iq>
`
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
t.Fatalf("could not parse iq")
}
_, ok := respIQ.Payload.(*stanza.Command)
if !ok {
t.Fatal("this iq payload is not a command")
}
fMap, err := respIQ.GetFormFields()
if err != nil || len(fMap) != 2 {
t.Fatal("could not parse command form fields")
}
}
// ********************************************
// * 8.7 Process Pending Subscription Requests
// ********************************************
func TestNewApprovePendingSubRequest(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"pending2\" to=\"pubsub.shakespeare.lit\"> " +
"<command xmlns=\"http://jabber.org/protocol/commands\" action=\"execute\"" +
"node=\"http://jabber.org/protocol/pubsub#get-pending\"sessionid=\"pubsub-get-pending:20031021T150901Z-600\"> " +
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field xmlns=\"jabber:x:data\" var=\"pubsub#node\"> " +
"<value xmlns=\"jabber:x:data\">princely_musings</value> </field> </x> </command> </iq>"
subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit",
"pubsub-get-pending:20031021T150901Z-600",
"princely_musings")
if err != nil {
t.Fatalf("failed to create a approve pending sub request: %v", err)
}
subR.Id = "pending2"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
command, ok := subR.Payload.(*stanza.Command)
if !ok {
t.Fatalf("payload is not a command !")
}
if command.Action != stanza.CommandActionExecute {
t.Fatalf("command should be execute !")
}
//if command.Node != "http://jabber.org/protocol/pubsub#get-pending"{
// t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !")
//}
//
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ********************************************
// * 8.8.1 Retrieve Subscriptions List
// ********************************************
func TestNewSubListRqPl(t *testing.T) {
expectedReq := "<iq type=\"get\" id=\"subman1\" to=\"pubsub.shakespeare.lit\" > " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
"<subscriptions node=\"princely_musings\"></subscriptions> </pubsub> </iq>"
subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings")
if err != nil {
t.Fatalf("failed to create a sub list request: %v", err)
}
subR.Id = "subman1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub in namespace owner !")
}
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
if !ok {
t.Fatalf("pubsub doesn not contain a subscriptions node !")
}
if subs.Node != "princely_musings" {
t.Fatalf("subs node attribute should be princely_musings. Found %s", subs.Node)
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewSubListRqPlResp(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="subman1" to="hamlet@denmark.lit/elsinore" type="result">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<subscriptions node="princely_musings">
<subscription jid="hamlet@denmark.lit" subscription="subscribed"></subscription>
<subscription jid="polonius@denmark.lit" subscription="unconfigured"></subscription>
<subscription jid="bernardo@denmark.lit" subid="123-abc" subscription="subscribed"></subscription>
<subscription jid="bernardo@denmark.lit" subid="004-yyy" subscription="subscribed"></subscription>
</subscriptions>
</pubsub>
</iq>
`
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
t.Fatalf("could not parse iq")
}
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatal("this iq payload is not a command")
}
subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner)
if !ok {
t.Fatalf("pubsub doesn not contain a subscriptions node !")
}
if len(subs.Subscriptions) != 4 {
t.Fatalf("expected to find 4 subscriptions but got %d", len(subs.Subscriptions))
}
}
// ********************************************
// * 8.9.1 Retrieve Affiliations List
// ********************************************
func TestNewAffiliationListRequest(t *testing.T) {
expectedReq := "<iq type=\"get\" id=\"ent1\" to=\"pubsub.shakespeare.lit\" > " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> " +
"<affiliations node=\"princely_musings\"></affiliations> </pubsub> </iq>"
subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings")
if err != nil {
t.Fatalf("failed to create an affiliations list request: %v", err)
}
subR.Id = "ent1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub in namespace owner !")
}
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
if !ok {
t.Fatalf("pubsub doesn not contain an affiliations node !")
}
if affils.Node != "princely_musings" {
t.Fatalf("affils node attribute should be princely_musings. Found %s", affils.Node)
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewAffiliationListRequestResp(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="ent1" to="hamlet@denmark.lit/elsinore" type="result">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<affiliations node="princely_musings">
<affiliation affiliation="owner" jid="hamlet@denmark.lit"/>
<affiliation affiliation="outcast" jid="polonius@denmark.lit"/>
</affiliations>
</pubsub>
</iq>
`
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
t.Fatalf("could not parse iq")
}
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatal("this iq payload is not a command")
}
affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
if !ok {
t.Fatalf("pubsub doesn not contain an affiliations node !")
}
if len(affils.Affiliations) != 2 {
t.Fatalf("expected to find 2 subscriptions but got %d", len(affils.Affiliations))
}
}
// ********************************************
// * 8.9.2 Modify Affiliation
// ********************************************
func TestNewModifAffiliationRequest(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"ent3\" to=\"pubsub.shakespeare.lit\" > " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <affiliations node=\"princely_musings\"> " +
"<affiliation affiliation=\"none\" jid=\"hamlet@denmark.lit\"></affiliation> " +
"<affiliation affiliation=\"none\" jid=\"polonius@denmark.lit\"></affiliation> " +
"<affiliation affiliation=\"publisher\" jid=\"bard@shakespeare.lit\"></affiliation> </affiliations> </pubsub> " +
"</iq>"
affils := []stanza.AffiliationOwner{
{
AffiliationStatus: stanza.AffiliationStatusNone,
Jid: "hamlet@denmark.lit",
},
{
AffiliationStatus: stanza.AffiliationStatusNone,
Jid: "polonius@denmark.lit",
},
{
AffiliationStatus: stanza.AffiliationStatusPublisher,
Jid: "bard@shakespeare.lit",
},
}
subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils)
if err != nil {
t.Fatalf("failed to create a modif affiliation request: %v", err)
}
subR.Id = "ent3"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub in namespace owner !")
}
as, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner)
if !ok {
t.Fatalf("pubsub doesn not contain an affiliations node !")
}
if as.Node != "princely_musings" {
t.Fatalf("affils node attribute should be princely_musings. Found %s", as.Node)
}
if len(as.Affiliations) != 3 {
t.Fatalf("expected 3 affiliations, found %d", len(as.Affiliations))
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestGetFormFields(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="config1" to="hamlet@denmark.lit/elsinore" type="result">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<configure node="princely_musings">
<x type="form" xmlns="jabber:x:data">
<field type="hidden" var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#node_config</value>
</field>
<field label="Purge all items when the relevant publisher goes offline?" type="boolean" var="pubsub#purge_offline">
<value>0</value>
</field>
<field label="Max Payload size in bytes" type="text-single" var="pubsub#max_payload_size">
<value>1028</value>
</field>
<field label="When to send the last published item" type="list-single" var="pubsub#send_last_published_item">
<option label="Never">
<value>never</value>
</option>
<option label="When a new subscription is processed">
<value>on_sub</value>
</option>
<option label="When a new subscription is processed and whenever a subscriber comes online">
<value>on_sub_and_presence</value>
</option>
<value>never</value>
</field>
<field label="Deliver event notifications only to available users" type="boolean" var="pubsub#presence_based_delivery">
<value>0</value>
</field>
<field label="Specify the delivery style for event notifications" type="list-single" var="pubsub#notification_type">
<option>
<value>normal</value>
</option>
<option>
<value>headline</value>
</option>
<value>headline</value>
</field>
<field label="Specify the type of payload data to be provided at this node" type="text-single" var="pubsub#type">
<value>http://www.w3.org/2005/Atom</value>
</field>
<field label="Payload XSLT" type="text-single" var="pubsub#dataform_xslt"/>
</x>
</configure>
</pubsub>
</iq>
`
var iq stanza.IQ
err := xml.Unmarshal([]byte(response), &iq)
if err != nil {
t.Fatalf("could not parse IQ")
}
fields, err := iq.GetFormFields()
if len(fields) != 8 {
t.Fatalf("could not correctly parse fields. Expected 8, found : %v", len(fields))
}
}
func TestGetFormFieldsCmd(t *testing.T) {
response := `
<iq from="pubsub.shakespeare.lit" id="pending1" to="hamlet@denmark.lit/elsinore" type="result">
<command action="execute" node="http://jabber.org/protocol/pubsub#get-pending" sessionid="pubsub-get-pending:20031021T150901Z-600" status="executing" xmlns="http://jabber.org/protocol/commands">
<x type="form" xmlns="jabber:x:data">
<field type="hidden" var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#subscribe_authorization</value>
</field>
<field type="list-single" var="pubsub#node">
<option>
<value>princely_musings</value>
</option>
<option>
<value>news_from_elsinore</value>
</option>
</field>
</x>
</command>
</iq>
`
var iq stanza.IQ
err := xml.Unmarshal([]byte(response), &iq)
if err != nil {
t.Fatalf("could not parse IQ")
}
fields, err := iq.GetFormFields()
if len(fields) != 2 {
t.Fatalf("could not correctly parse fields. Expected 2, found : %v", len(fields))
}
}
func TestNewFormSubmissionOwner(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"config2\" to=\"pubsub.shakespeare.lit\">" +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\"> <configure node=\"princely_musings\"> " +
"<x xmlns=\"jabber:x:data\" type=\"submit\" > <field var=\"FORM_TYPE\" type=\"hidden\"> " +
"<value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#item_expire\"> " +
"<value>604800</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value> " +
"<value>courtiers</value> </field> </x> </configure> </pubsub> </iq>"
subR, err := stanza.NewFormSubmissionOwner("pubsub.shakespeare.lit",
"princely_musings",
[]*stanza.Field{
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
{Var: "pubsub#item_expire", ValuesList: []string{"604800"}},
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
})
if err != nil {
t.Fatalf("failed to create a form submission request: %v", err)
}
subR.Id = "config2"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubOwner)
if !ok {
t.Fatalf("payload is not a pubsub in namespace owner !")
}
conf, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner)
if !ok {
t.Fatalf("pubsub does not contain a configure node !")
}
if conf.Form == nil {
t.Fatalf("the form is absent from the configuration submission !")
}
if len(conf.Form.Fields) != 4 {
t.Fatalf("expected 4 fields, found %d", len(conf.Form.Fields))
}
if len(conf.Form.Fields[3].ValuesList) != 3 {
t.Fatalf("expected 3 values in fourth field, found %d", len(conf.Form.Fields[3].ValuesList))
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) {
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
return &stanza.PubSubOwner{}, err
}
pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner)
if !ok {
return nil, errors.New("this iq payload is not a pubsub of the owner namespace")
}
return pubsub, nil
}
+923
View File
@@ -0,0 +1,923 @@
package stanza_test
import (
"encoding/xml"
"errors"
"strings"
"testing"
"gosrc.io/xmpp/stanza"
)
var submitFormExample = stanza.NewForm([]*stanza.Field{
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
{Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}},
{Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}},
{Var: "pubsub#access_model", ValuesList: []string{"roster"}},
{Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}},
{Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}},
{
Var: "pubsub#notification_type",
Type: "list-single",
Label: "Specify the delivery style for event notifications",
ValuesList: []string{"headline"},
Options: []stanza.Option{
{ValuesList: []string{"normal"}},
{ValuesList: []string{"headline"}},
},
},
}, stanza.FormTypeSubmit)
// ***********************************
// * 6.1 Subscribe to a Node
// ***********************************
func TestNewSubRequest(t *testing.T) {
expectedReq := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></subscribe>" +
" </pubsub> </iq>"
subInfo := stanza.SubInfo{
Node: "princely_musings", Jid: "francisco@denmark.lit",
}
subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo)
if err != nil {
t.Fatalf("failed to create a sub request: %v", err)
}
subR.Id = "sub1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewSubResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscription node="princely_musings" jid="francisco@denmark.lit"
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
</pubsub>
</iq>
`
pubsub, err := getPubSubGenericPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.Subscription == nil {
t.Fatalf("subscription node is nil")
}
if pubsub.Subscription.Node == "" ||
pubsub.Subscription.Jid == "" ||
pubsub.Subscription.SubId == nil ||
pubsub.Subscription.SubStatus == "" {
t.Fatalf("one or more of the subscription attributes was not successfully decoded")
}
}
// ***********************************
// * 6.2 Unsubscribe from a Node
// ***********************************
func TestNewUnSubRequest(t *testing.T) {
expectedReq := "<iq type=\"set\"id=\"unsub1\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
"<unsubscribe node=\"princely_musings\"jid=\"francisco@denmark.lit\"></unsubscribe> </pubsub> </iq>"
subInfo := stanza.SubInfo{
Node: "princely_musings", Jid: "francisco@denmark.lit",
}
subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo)
if err != nil {
t.Fatalf("failed to create an unsub request: %v", err)
}
subR.Id = "unsub1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Unsubscribe == nil {
t.Fatalf("Unsubscribe tag should be present in sub config options request")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewUnsubResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="unsub1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscription node="princely_musings" jid="francisco@denmark.lit" subscription="none"
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3"/>
</pubsub>
</iq>
`
pubsub, err := getPubSubGenericPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.Subscription == nil {
t.Fatalf("subscription node is nil")
}
if pubsub.Subscription.Node == "" ||
pubsub.Subscription.Jid == "" ||
pubsub.Subscription.SubId == nil ||
pubsub.Subscription.SubStatus == "" {
t.Fatalf("one or more of the subscription attributes was not successfully decoded")
}
}
// ***************************************
// * 6.3 Configure Subscription Options
// ***************************************
func TestNewSubOptsRq(t *testing.T) {
expectedReq := "<iq type=\"get\"id=\"options1\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
"<options node=\"princely_musings\" jid=\"francisco@denmark.lit\"></options> </pubsub> </iq>"
subInfo := stanza.SubInfo{
Node: "princely_musings", Jid: "francisco@denmark.lit",
}
subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo)
if err != nil {
t.Fatalf("failed to create a sub options request: %v", err)
}
subR.Id = "options1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.SubOptions == nil {
t.Fatalf("Options tag should be present in sub config options request")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewNewConfOptsRsp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="options1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<options node="princely_musings" jid="francisco@denmark.lit">
<x xmlns="jabber:x:data" type="form">
<field var="FORM_TYPE" type="hidden">
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
</field>
<field var="pubsub#deliver" type="boolean" label="Enable delivery?">
<value>1</value>
</field>
<field var="pubsub#digest" type="boolean"
label="Receive digest notifications (approx. one per day)?">
<value>0</value>
</field>
<field var="pubsub#include_body" type="boolean"
label="Receive message body in addition to payload?">
<value>false</value>
</field>
<field var="pubsub#show-values" type="list-multi"
label="Select the presence types which are
allowed to receive event notifications">
<option label="Want to Chat">
<value>chat</value>
</option>
<option label="Available">
<value>online</value>
</option>
<option label="Away">
<value>away</value>
</option>
<option label="Extended Away">
<value>xa</value>
</option>
<option label="Do Not Disturb">
<value>dnd</value>
</option>
<value>chat</value>
<value>online</value>
</field>
</x>
</options>
</pubsub>
</iq>
`
pubsub, err := getPubSubGenericPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.SubOptions == nil {
t.Fatalf("sub options node is nil")
}
if pubsub.SubOptions.Form == nil {
t.Fatalf("the response form is nil")
}
if len(pubsub.SubOptions.Form.Fields) != 5 {
t.Fatalf("one or more fields in the response form could not be parsed correctly")
}
}
// ***************************************
// * 6.3.5 Form Submission
// ***************************************
func TestNewFormSubmission(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"options2\" to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <options node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\">" +
" <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#title\"> " +
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
"<value>1</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value>" +
" <value>courtiers</value> </field> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
"</field> </x> </options> </pubsub> </iq>"
subInfo := stanza.SubInfo{
Node: "princely_musings", Jid: "francisco@denmark.lit",
}
subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample)
if err != nil {
t.Fatalf("failed to create a form submission request: %v", err)
}
subR.Id = "options2"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.SubOptions == nil {
t.Fatalf("Options tag should be present in sub config options request")
}
if pubsub.SubOptions.Form == nil {
t.Fatalf("No form in form submit request !")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ***************************************
// * 6.3.7 Subscribe and Configure
// ***************************************
func TestNewSubAndConfig(t *testing.T) {
expectedReq := "<iq type=\"set\"id=\"sub1\"to=\"pubsub.shakespeare.lit\">" +
" <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscribe node=\"princely_musings\" jid=\"francisco@denmark.lit\"> " +
"</subscribe>" +
"<options> <x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\">" +
" <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#title\"> " +
"<value>Princely Musings (Atom)</value> </field> <field var=\"pubsub#deliver_notifications\"> " +
"<value>1</value> </field> <field var=\"pubsub#access_model\"> <value>roster</value> </field> " +
"<field var=\"pubsub#roster_groups_allowed\"> <value>friends</value> <value>servants</value>" +
" <value>courtiers</value> </field> <field var=\"pubsub#type\"> <value>http://www.w3.org/2005/Atom</value> " +
"</field> <field var=\"pubsub#notification_type\" type=\"list-single\"label=\"Specify the delivery style for event notifications\"> " +
"<value>headline</value> <option> <value>normal</value> </option> <option> <value>headline</value> </option> " +
"</field> </x> </options> </pubsub> </iq>"
subInfo := stanza.SubInfo{
Node: "princely_musings", Jid: "francisco@denmark.lit",
}
subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample)
if err != nil {
t.Fatalf("failed to create a sub and config request: %v", err)
}
subR.Id = "sub1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.SubOptions == nil {
t.Fatalf("Options tag should be present in sub config options request")
}
if pubsub.SubOptions.Form == nil {
t.Fatalf("No form in form submit request !")
}
// The <options/> element MUST NOT possess a 'node' attribute or 'jid' attribute
// See XEP-0060
if pubsub.SubOptions.SubInfo.Node != "" || pubsub.SubOptions.SubInfo.Jid != "" {
t.Fatalf("SubInfo node and jid should be empty for the options tag !")
}
if pubsub.Subscribe.Node == "" || pubsub.Subscribe.Jid == "" {
t.Fatalf("SubInfo node and jid should NOT be empty for the subscribe tag !")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewSubAndConfigResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="sub1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscription node="princely_musings" jid="francisco@denmark.lit"
subid="ba49252aaa4f5d320c24d3766f0bdcade78c78d3" subscription="subscribed"/>
<options>
<x xmlns="jabber:x:data" type="result">
<field var="FORM_TYPE" type="hidden">
<value>http://jabber.org/protocol/pubsub#subscribe_options</value>
</field>
<field var="pubsub#deliver">
<value>1</value>
</field>
<field var="pubsub#digest">
<value>0</value>
</field>
<field var="pubsub#include_body">
<value>false</value>
</field>
<field var="pubsub#show-values">
<value>chat</value>
<value>online</value>
<value>away</value>
</field>
</x>
</options>
</pubsub>
</iq>
`
pubsub, err := getPubSubGenericPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.Subscription == nil {
t.Fatalf("sub node is nil")
}
if pubsub.SubOptions == nil {
t.Fatalf("sub options node is nil")
}
if pubsub.SubOptions.Form == nil {
t.Fatalf("the response form is nil")
}
if len(pubsub.SubOptions.Form.Fields) != 5 {
t.Fatalf("one or more fields in the response form could not be parsed correctly")
}
}
// ***************************************
// * 6.5.2 Requesting All List
// ***************************************
func TestNewItemsRequest(t *testing.T) {
subR, err := stanza.NewItemsRequest("pubsub.shakespeare.lit", "princely_musings", 0)
if err != nil {
t.Fatalf("Could not create an items request : %s", err)
}
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Items == nil {
t.Fatalf("List tag should be present to request items from a service")
}
if len(pubsub.Items.List) != 0 {
t.Fatalf("There should be no items in the <items> tag to request all items from a service")
}
}
func TestNewItemsResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit/barracks" id="items2">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<items node="princely_musings">
<item id="4e30f35051b7b8b42abe083742187228">
<entry xmlns="http://www.w3.org/2005/Atom">
<title>Alone</title>
<summary> Now I am alone. O, what a rogue and peasant slave am I! </summary>
<link rel="alternate" type="text/html"
href="http://denmark.lit/2003/12/13/atom03"/>
<id>tag:denmark.lit,2003:entry-32396</id>
<published>2003-12-13T11:09:53Z</published>
<updated>2003-12-13T11:09:53Z</updated>
</entry>
</item>
<item id="ae890ac52d0df67ed7cfdf51b644e901">
<entry xmlns="http://www.w3.org/2005/Atom">
<title>Soliloquy</title>
<summary> To be, or not to be: that is the question: Whether 'tis nobler in the
mind to suffer The slings and arrows of outrageous fortune, Or to take arms
against a sea of troubles, And by opposing end them? </summary>
<link rel="alternate" type="text/html"
href="http://denmark.lit/2003/12/13/atom03"/>
<id>tag:denmark.lit,2003:entry-32397</id>
<published>2003-12-13T18:30:02Z</published>
<updated>2003-12-13T18:30:02Z</updated>
</entry>
</item>
</items>
</pubsub>
</iq>
`
pubsub, err := getPubSubGenericPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.Items == nil {
t.Fatalf("sub options node is nil")
}
if pubsub.Items.List == nil {
t.Fatalf("the response form is nil")
}
if len(pubsub.Items.List) != 2 {
t.Fatalf("one or more items in the response could not be parsed correctly")
}
}
// ***************************************
// * 6.5.8 Requesting a Particular Item
// ***************************************
func TestNewSpecificItemRequest(t *testing.T) {
expectedReq := "<iq type=\"get\" id=\"items3\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <items node=\"princely_musings\"> " +
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </items> </pubsub> </iq>"
subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901")
if err != nil {
t.Fatalf("failed to create a specific item request: %v", err)
}
subR.Id = "items3"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Items == nil {
t.Fatalf("List tag should be present to request items from a service")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ***************************************
// * 7.1 Publish an Item to a Node
// ***************************************
func TestNewPublishItemRq(t *testing.T) {
item := stanza.Item{
XMLName: xml.Name{},
Id: "",
Publisher: "",
Any: &stanza.Node{
XMLName: xml.Name{
Space: "http://www.w3.org/2005/Atom",
Local: "entry",
},
Attrs: nil,
Content: "",
Nodes: []stanza.Node{
{
XMLName: xml.Name{Space: "", Local: "title"},
Attrs: nil,
Content: "My pub item title",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "summary"},
Attrs: nil,
Content: "My pub item content summary",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "link"},
Attrs: []xml.Attr{
{
Name: xml.Name{Space: "", Local: "rel"},
Value: "alternate",
},
{
Name: xml.Name{Space: "", Local: "type"},
Value: "text/html",
},
{
Name: xml.Name{Space: "", Local: "href"},
Value: "http://denmark.lit/2003/12/13/atom03",
},
},
},
{
XMLName: xml.Name{Space: "", Local: "id"},
Attrs: nil,
Content: "My pub item content ID",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "published"},
Attrs: nil,
Content: "2003-12-13T18:30:02Z",
Nodes: nil,
},
{
XMLName: xml.Name{Space: "", Local: "updated"},
Attrs: nil,
Content: "2003-12-13T18:30:02Z",
Nodes: nil,
},
},
},
}
subR, err := stanza.NewPublishItemRq("pubsub.shakespeare.lit", "princely_musings", "bnd81g37d61f49fgn581", item)
if err != nil {
t.Fatalf("Could not create an item pub request : %s", err)
}
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated sub request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if strings.TrimSpace(pubsub.Publish.Node) == "" {
t.Fatalf("the <publish/> element MUST possess a 'node' attribute, specifying the NodeID of the node.")
}
if pubsub.Publish.Items[0].Id == "" {
t.Fatalf("an id was provided for the item and it should be used")
}
}
// ***************************************
// * 7.1.5 Publishing Options
// ***************************************
func TestNewPublishItemOptsRq(t *testing.T) {
expectedReq := "<iq type=\"set\"id=\"pub1\"to=\"pubsub.shakespeare.lit\"> <pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> " +
"<publish node=\"princely_musings\"> <item id=\"ae890ac52d0df67ed7cfdf51b644e901\"> " +
"<entry xmlns=\"http://www.w3.org/2005/Atom\"> <title>Soliloquy</title> " +
"<summary> To be, or not to be: that is the question: Whether \"tis nobler in the mind to suffer The " +
"slings and arrows of outrageous fortune, Or to take arms against a sea of troubles, And by opposing end them? " +
"</summary> <link rel=\"alternate\" type=\"text/html\"href=\"http://denmark.lit/2003/12/13/atom03\"></link> " +
"<id>tag:denmark.lit,2003:entry-32397</id> <published>2003-12-13T18:30:02Z</published> " +
"<updated>2003-12-13T18:30:02Z</updated> </entry> </item> </publish> <publish-options> " +
"<x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\"> " +
"<value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var=\"pubsub#access_model\"> " +
"<value>presence</value> </field> </x> </publish-options> </pubsub> </iq>"
var iq stanza.IQ
err := xml.Unmarshal([]byte(expectedReq), &iq)
if err != nil {
t.Fatalf("could not unmarshal example request : %s", err)
}
pubsub, ok := iq.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Publish == nil {
t.Fatalf("Publish tag is empty")
}
if len(pubsub.Publish.Items) != 1 {
t.Fatalf("could not parse item properly")
}
}
// ***************************************
// * 7.2 Delete an Item from a Node
// ***************************************
func TestNewDelItemFromNode(t *testing.T) {
expectedReq := "<iq type=\"set\"id=\"retract1\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <retract node=\"princely_musings\"> " +
"<item id=\"ae890ac52d0df67ed7cfdf51b644e901\"></item> </retract> </pubsub> </iq>"
subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil)
if err != nil {
t.Fatalf("failed to create a delete item from node request: %v", err)
}
subR.Id = "retract1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Retract == nil {
t.Fatalf("Retract tag should be present to del an item from a service")
}
if strings.TrimSpace(pubsub.Retract.Items[0].Id) == "" {
t.Fatalf("Item id, for the item to delete, should be non empty")
}
if pubsub.Retract.Items[0].Any != nil {
t.Fatalf("Item node must be empty")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ***************************************
// * 8.1 Create a Node
// ***************************************
func TestNewCreateNode(t *testing.T) {
expectedReq := "<iq type=\"set\"id=\"create1\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> </pubsub> </iq>"
subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings")
if err != nil {
t.Fatalf("failed to create a create node request: %v", err)
}
subR.Id = "create1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Create == nil {
t.Fatalf("Create tag should be present to create a node on a service")
}
if strings.TrimSpace(pubsub.Create.Node) == "" {
t.Fatalf("Expected node name to be present")
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
func TestNewCreateNodeResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="hamlet@denmark.lit/elsinore" id="create2">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<create node="25e3d37dabbab9541f7523321421edc5bfeb2dae"/>
</pubsub>
</iq>
`
pubsub, err := getPubSubGenericPayload(response)
if err != nil {
t.Fatal(err)
}
if pubsub.Create == nil {
t.Fatalf("create segment is nil")
}
if pubsub.Create.Node == "" {
t.Fatalf("could not parse generated nodeId")
}
}
// ***************************************
// * 8.1.3 Create and Configure a Node
// ***************************************
func TestNewCreateAndConfigNode(t *testing.T) {
expectedReq := "<iq type=\"set\" id=\"create1\" to=\"pubsub.shakespeare.lit\" > " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <create node=\"princely_musings\"></create> " +
"<configure> <x xmlns=\"jabber:x:data\" type=\"submit\"> <field var=\"FORM_TYPE\" type=\"hidden\" > " +
"<value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var=\"pubsub#notify_retract\"> " +
"<value>0</value> </field> <field var=\"pubsub#notify_sub\"> <value>0</value> </field> " +
"<field var=\"pubsub#max_payload_size\"> <value>1028</value> </field> </x> </configure> </pubsub> </iq>"
subR, err := stanza.NewCreateAndConfigNode("pubsub.shakespeare.lit",
"princely_musings",
&stanza.Form{
Type: stanza.FormTypeSubmit,
Fields: []*stanza.Field{
{Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}},
{Var: "pubsub#notify_retract", ValuesList: []string{"0"}},
{Var: "pubsub#notify_sub", ValuesList: []string{"0"}},
{Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}},
},
})
if err != nil {
t.Fatalf("failed to create a create and config node request: %v", err)
}
subR.Id = "create1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
}
pubsub, ok := subR.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("payload is not a pubsub !")
}
if pubsub.Create == nil {
t.Fatalf("Create tag should be present to create a node on a service")
}
if strings.TrimSpace(pubsub.Create.Node) == "" {
t.Fatalf("Expected node name to be present")
}
if pubsub.Configure == nil {
t.Fatalf("Configure tag should be present to configure a node during its creation on a service")
}
if pubsub.Configure.Form == nil {
t.Fatalf("Expected a form to be present, to configure the node")
}
if len(pubsub.Configure.Form.Fields) != 4 {
t.Fatalf("Expected 4 elements to be present in the config form but got : %v", len(pubsub.Configure.Form.Fields))
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expectedReq, string(data)); err != nil {
t.Fatal(err)
}
}
// ********************************
// * 5.7 Retrieve Subscriptions
// ********************************
func TestNewRetrieveAllSubsRequest(t *testing.T) {
expected := "<iq type=\"get\" id=\"subscriptions1\" to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <subscriptions></subscriptions> </pubsub> </iq>"
subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit")
if err != nil {
t.Fatalf("failed to create a get all subs request: %v", err)
}
subR.Id = "subscriptions1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated del item request : %s", e)
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expected, string(data)); err != nil {
t.Fatal(err)
}
}
func TestRetrieveAllSubsResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="subscriptions1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscriptions>
<subscription node="node1" jid="francisco@denmark.lit" subscription="subscribed"/>
<subscription node="node2" jid="francisco@denmark.lit" subscription="subscribed"/>
<subscription node="node5" jid="francisco@denmark.lit" subscription="unconfigured"/>
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
subid="123-abc"/>
<subscription node="node6" jid="francisco@denmark.lit" subscription="subscribed"
subid="004-yyy"/>
</subscriptions>
</pubsub>
</iq>
`
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
t.Fatalf("could not unmarshal response: %s", err)
}
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("umarshalled payload is not a pubsub")
}
if pubsub.Subscriptions == nil {
t.Fatalf("subscriptions node is nil")
}
if len(pubsub.Subscriptions.List) != 5 {
t.Fatalf("incorrect number of decoded subscriptions")
}
}
// ********************************
// * 5.7 Retrieve Affiliations
// ********************************
func TestNewRetrieveAllAffilsRequest(t *testing.T) {
expected := "<iq type=\"get\"id=\"affil1\"to=\"pubsub.shakespeare.lit\"> " +
"<pubsub xmlns=\"http://jabber.org/protocol/pubsub\"> <affiliations></affiliations> </pubsub> </iq>"
subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit")
if err != nil {
t.Fatalf("failed to create a get all affiliations request: %v", err)
}
subR.Id = "affil1"
if _, e := checkMarshalling(t, subR); e != nil {
t.Fatalf("Failed to check marshalling for generated retreive all affiliations request : %s", e)
}
data, err := xml.Marshal(subR)
if err := compareMarshal(expected, string(data)); err != nil {
t.Fatal(err)
}
}
func TestRetrieveAllAffilsResp(t *testing.T) {
response := `
<iq type="result" from="pubsub.shakespeare.lit" to="francisco@denmark.lit" id="affil1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<affiliations>
<affiliation node="node1" affiliation="owner"/>
<affiliation node="node2" affiliation="publisher"/>
<affiliation node="node5" affiliation="outcast"/>
<affiliation node="node6" affiliation="owner"/>
</affiliations>
</pubsub>
</iq>
`
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
t.Fatalf("could not unmarshal response: %s", err)
}
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
if !ok {
t.Fatalf("umarshalled payload is not a pubsub")
}
if pubsub.Affiliations == nil {
t.Fatalf("subscriptions node is nil")
}
if len(pubsub.Affiliations.List) != 4 {
t.Fatalf("incorrect number of decoded subscriptions")
}
}
func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) {
var respIQ stanza.IQ
err := xml.Unmarshal([]byte(response), &respIQ)
if err != nil {
return &stanza.PubSubGeneric{}, err
}
pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric)
if !ok {
return nil, errors.New("this iq payload is not a pubsub")
}
return pubsub, nil
}
+119
View File
@@ -0,0 +1,119 @@
package stanza
import (
"encoding/xml"
"reflect"
"sync"
)
type MsgExtension interface{}
type PresExtension interface{}
// The Registry for msg and IQ types is a global variable.
// TODO: Move to the client init process to remove the dependency on a global variable.
// That should make it possible to be able to share the decoder.
// TODO: Ensure that a client can add its own custom namespace to the registry (or overload existing ones).
type PacketType uint8
const (
PKTPresence PacketType = iota
PKTMessage
PKTIQ
)
var TypeRegistry = newRegistry()
// We store different registries per packet type and namespace.
type registryKey struct {
packetType PacketType
namespace string
}
type registryForNamespace map[string]reflect.Type
type registry struct {
// We store different registries per packet type and namespace.
msgTypes map[registryKey]registryForNamespace
// Handle concurrent access
msgTypesLock *sync.RWMutex
}
func newRegistry() *registry {
return &registry{
msgTypes: make(map[registryKey]registryForNamespace),
msgTypesLock: &sync.RWMutex{},
}
}
// MapExtension stores extension type for packet payload.
// The match is done per PacketType (iq, message, or presence) and XML tag name.
// You can use the alias "*" as local XML name to be able to match all unknown tag name for that
// packet type and namespace.
func (r *registry) MapExtension(pktType PacketType, name xml.Name, extension MsgExtension) {
key := registryKey{pktType, name.Space}
r.msgTypesLock.RLock()
store := r.msgTypes[key]
r.msgTypesLock.RUnlock()
r.msgTypesLock.Lock()
defer r.msgTypesLock.Unlock()
if store == nil {
store = make(map[string]reflect.Type)
}
store[name.Local] = reflect.TypeOf(extension)
r.msgTypes[key] = store
}
// GetExtensionType returns extension type for packet payload, based on packet type and tag name.
func (r *registry) GetExtensionType(pktType PacketType, name xml.Name) reflect.Type {
key := registryKey{pktType, name.Space}
r.msgTypesLock.RLock()
defer r.msgTypesLock.RUnlock()
store := r.msgTypes[key]
result := store[name.Local]
if result == nil && name.Local != "*" {
return store["*"]
}
return result
}
// GetPresExtension returns an instance of PresExtension, by matching packet type and XML
// tag name against the registry.
func (r *registry) GetPresExtension(name xml.Name) PresExtension {
if extensionType := r.GetExtensionType(PKTPresence, name); extensionType != nil {
val := reflect.New(extensionType)
elt := val.Interface()
if presExt, ok := elt.(PresExtension); ok {
return presExt
}
}
return nil
}
// GetMsgExtension returns an instance of MsgExtension, by matching packet type and XML
// tag name against the registry.
func (r *registry) GetMsgExtension(name xml.Name) MsgExtension {
if extensionType := r.GetExtensionType(PKTMessage, name); extensionType != nil {
val := reflect.New(extensionType)
elt := val.Interface()
if msgExt, ok := elt.(MsgExtension); ok {
return msgExt
}
}
return nil
}
// GetIQExtension returns an instance of IQPayload, by matching packet type and XML
// tag name against the registry.
func (r *registry) GetIQExtension(name xml.Name) IQPayload {
if extensionType := r.GetExtensionType(PKTIQ, name); extensionType != nil {
val := reflect.New(extensionType)
elt := val.Interface()
if iqExt, ok := elt.(IQPayload); ok {
return iqExt
}
}
return nil
}
+47
View File
@@ -0,0 +1,47 @@
package stanza
import (
"encoding/xml"
"reflect"
"testing"
)
func TestRegistry_RegisterMsgExt(t *testing.T) {
// Setup registry
typeRegistry := newRegistry()
// Register an element
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
// Match that element
receipt := typeRegistry.GetMsgExtension(name)
if receipt == nil {
t.Error("cannot read element type from registry")
return
}
switch r := receipt.(type) {
case *ReceiptRequest:
default:
t.Errorf("Registry did not return expected type ReceiptRequest: %v", reflect.TypeOf(r))
}
}
func BenchmarkRegistryGet(b *testing.B) {
// Setup registry
typeRegistry := newRegistry()
// Register an element
name := xml.Name{Space: "urn:xmpp:receipts", Local: "request"}
typeRegistry.MapExtension(PKTMessage, name, ReceiptRequest{})
for i := 0; i < b.N; i++ {
// Match that element
receipt := typeRegistry.GetExtensionType(PKTMessage, name)
if receipt == nil {
b.Error("cannot read element type from registry")
return
}
}
}
+29
View File
@@ -0,0 +1,29 @@
package stanza
import (
"encoding/xml"
)
// Support for XEP-0059
// See https://xmpp.org/extensions/xep-0059
const (
// Common but not only possible namespace for query blocks in a result set context
NSQuerySet = "jabber:iq:search"
)
type ResultSet struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"`
After *string `xml:"after,omitempty"`
Before *string `xml:"before,omitempty"`
Count *int `xml:"count,omitempty"`
First *First `xml:"first,omitempty"`
Index *int `xml:"index,omitempty"`
Last *string `xml:"last,omitempty"`
Max *int `xml:"max,omitempty"`
}
type First struct {
XMLName xml.Name `xml:"first"`
Content string `xml:",chardata"`
Index *int `xml:"index,attr,omitempty"`
}
+28
View File
@@ -0,0 +1,28 @@
package stanza_test
import (
"gosrc.io/xmpp/stanza"
"testing"
)
// Limiting the number of items
func TestNewResultSetReq(t *testing.T) {
expectedRq := "<iq id=\"q29302\" type=\"set\"> <query xmlns=\"urn:xmpp:mam:2\"> " +
"<x type=\"submit\" xmlns=\"jabber:x:data\"> <field type=\"hidden\" var=\"FORM_TYPE\"> " +
"<value>urn:xmpp:mam:2</value> </field> <field var=\"start\"> <value>2010-08-07T00:00:00Z</value> </field> </x> " +
"<set xmlns=\"http://jabber.org/protocol/rsm\"> <max>10</max> </set> </query> </iq>"
maxVal := 10
rs := &stanza.ResultSet{
Max: &maxVal,
}
// TODO when Mam is implemented
_ = expectedRq
_ = rs
}
func TestUnmarshalResultSeqReq(t *testing.T) {
// TODO when Mam is implemented
}
+124
View File
@@ -0,0 +1,124 @@
package stanza
import "encoding/xml"
// ============================================================================
// SASLAuth implements SASL Authentication initiation.
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.2
type SASLAuth struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
Mechanism string `xml:"mechanism,attr"`
Value string `xml:",innerxml"`
}
// ============================================================================
// SASLSuccess implements SASL Success nonza, sent by server as a result of the
// SASL auth negotiation.
// Reference: https://tools.ietf.org/html/rfc6120#section-6.4.6
type SASLSuccess struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
}
func (SASLSuccess) Name() string {
return "sasl:success"
}
// SASLSuccess decoding
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"
}
// SASLFailure decoding
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
}
// ===========================================================================
// Resource binding
// Bind is an IQ payload used during session negotiation to bind user resource
// to the current XMPP stream.
// Reference: https://tools.ietf.org/html/rfc6120#section-7
type Bind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string `xml:"resource,omitempty"`
Jid string `xml:"jid,omitempty"`
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (b *Bind) Namespace() string {
return b.XMLName.Space
}
func (b *Bind) GetSet() *ResultSet {
return b.ResultSet
}
// ============================================================================
// Session (Obsolete)
// Session is both a stream feature and an obsolete IQ Payload, used to bind a
// resource to the current XMPP stream on RFC 3121 only XMPP servers.
// Session is obsolete in RFC 6121. It is added to Fluux XMPP for compliance
// with RFC 3121.
// Reference: https://xmpp.org/rfcs/rfc3921.html#session
//
// This is the draft defining how to handle the transition:
// https://tools.ietf.org/html/draft-cridland-xmpp-session-01
type StreamSession struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
Optional *struct{} // If element does exist, it mean we are not required to open session
// Result sets
ResultSet *ResultSet `xml:"set,omitempty"`
}
func (s *StreamSession) Namespace() string {
return s.XMLName.Space
}
func (s *StreamSession) GetSet() *ResultSet {
return s.ResultSet
}
func (s *StreamSession) IsOptional() bool {
if s.XMLName.Local == "session" {
return s.Optional != nil
}
// If session element is missing, then we should not use session
return true
}
// ============================================================================
// Registry init
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-bind", Local: "bind"}, Bind{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-session", Local: "session"}, StreamSession{})
}
+60
View File
@@ -0,0 +1,60 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
// Check that we can detect optional session from advertised stream features
func TestSessionFeatures(t *testing.T) {
streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: &struct{}{}}}
data, err := xml.Marshal(streamFeatures)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
}
parsedStream := stanza.StreamFeatures{}
if err = xml.Unmarshal(data, &parsedStream); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
}
if !parsedStream.Session.IsOptional() {
t.Error("Session should be optional")
}
}
// Check that the Session tag can be used in IQ decoding
func TestSessionIQ(t *testing.T) {
iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"})
if err != nil {
t.Fatalf("failed to create IQ: %v", err)
}
iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: &struct{}{}}
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure: %s", err)
return
}
parsedIQ := stanza.IQ{}
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
return
}
session, ok := parsedIQ.Payload.(*stanza.StreamSession)
if !ok {
t.Error("Missing session payload")
return
}
if !session.IsOptional() {
t.Error("Session should be optional")
}
}
// TODO Test Sasl mechanism
+171
View File
@@ -0,0 +1,171 @@
package stanza
import (
"encoding/xml"
)
type StanzaErrorGroup interface {
GroupErrorName() string
}
type BadFormat struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-format"`
}
func (e *BadFormat) GroupErrorName() string { return "bad-format" }
type BadNamespacePrefix struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-namespace-prefix"`
}
func (e *BadNamespacePrefix) GroupErrorName() string { return "bad-namespace-prefix" }
type Conflict struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas conflict"`
}
func (e *Conflict) GroupErrorName() string { return "conflict" }
type ConnectionTimeout struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas connection-timeout"`
}
func (e *ConnectionTimeout) GroupErrorName() string { return "connection-timeout" }
type HostGone struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-gone"`
}
func (e *HostGone) GroupErrorName() string { return "host-gone" }
type HostUnknown struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-unknown"`
}
func (e *HostUnknown) GroupErrorName() string { return "host-unknown" }
type ImproperAddressing struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas improper-addressing"`
}
func (e *ImproperAddressing) GroupErrorName() string { return "improper-addressing" }
type InternalServerError struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error"`
}
func (e *InternalServerError) GroupErrorName() string { return "internal-server-error" }
type InvalidForm struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-from"`
}
func (e *InvalidForm) GroupErrorName() string { return "invalid-from" }
type InvalidId struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-id"`
}
func (e *InvalidId) GroupErrorName() string { return "invalid-id" }
type InvalidNamespace struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-namespace"`
}
func (e *InvalidNamespace) GroupErrorName() string { return "invalid-namespace" }
type InvalidXML struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-xml"`
}
func (e *InvalidXML) GroupErrorName() string { return "invalid-xml" }
type NotAuthorized struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-authorized"`
}
func (e *NotAuthorized) GroupErrorName() string { return "not-authorized" }
type NotWellFormed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-well-formed"`
}
func (e *NotWellFormed) GroupErrorName() string { return "not-well-formed" }
type PolicyViolation struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas policy-violation"`
}
func (e *PolicyViolation) GroupErrorName() string { return "policy-violation" }
type RemoteConnectionFailed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas remote-connection-failed"`
}
func (e *RemoteConnectionFailed) GroupErrorName() string { return "remote-connection-failed" }
type Reset struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas reset"`
}
func (e *Reset) GroupErrorName() string { return "reset" }
type ResourceConstraint struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint"`
}
func (e *ResourceConstraint) GroupErrorName() string { return "resource-constraint" }
type RestrictedXML struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas restricted-xml"`
}
func (e *RestrictedXML) GroupErrorName() string { return "restricted-xml" }
type SeeOtherHost struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas see-other-host"`
}
func (e *SeeOtherHost) GroupErrorName() string { return "see-other-host" }
type SystemShutdown struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas system-shutdown"`
}
func (e *SystemShutdown) GroupErrorName() string { return "system-shutdown" }
type UndefinedCondition struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition"`
}
func (e *UndefinedCondition) GroupErrorName() string { return "undefined-condition" }
type UnsupportedEncoding struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-encoding"`
}
type UnexpectedRequest struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request"`
}
func (e *UnexpectedRequest) GroupErrorName() string { return "unexpected-request" }
func (e *UnsupportedEncoding) GroupErrorName() string { return "unsupported-encoding" }
type UnsupportedStanzaType struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-stanza-type"`
}
func (e *UnsupportedStanzaType) GroupErrorName() string { return "unsupported-stanza-type" }
type UnsupportedVersion struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-version"`
}
func (e *UnsupportedVersion) GroupErrorName() string { return "unsupported-version" }
type XMLNotWellFormed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas xml-not-well-formed"`
}
func (e *XMLNotWellFormed) GroupErrorName() string { return "xml-not-well-formed" }
+14
View File
@@ -0,0 +1,14 @@
package stanza
import (
"encoding/xml"
)
// Used during stream initiation / session establishment
type TLSProceed struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
}
type tlsFailure struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls failure"`
}
+16
View File
@@ -0,0 +1,16 @@
package stanza
import "encoding/xml"
// Start of stream
// Reference: XMPP Core stream open
// https://tools.ietf.org/html/rfc6120#section-4.2
type Stream struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"`
From string `xml:"from,attr"`
To string `xml:"to,attr"`
Id string `xml:"id,attr"`
Version string `xml:"version,attr"`
}
const StreamClose = "</stream:stream>"
+189
View File
@@ -0,0 +1,189 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// StreamFeatures Packet
// Reference: The active stream features are published on
// https://xmpp.org/registrar/stream-features.html
// Note: That page misses draft and experimental XEP (i.e CSI, etc)
type StreamFeatures struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
// Server capabilities hash
Caps Caps
// Stream features
StartTLS TlsStartTLS
Mechanisms saslMechanisms
Bind Bind
StreamManagement streamManagement
// Obsolete
Session StreamSession
// ProcessOne Stream Features
P1Push p1Push
P1Rebind p1Rebind
p1Ack p1Ack
Any []xml.Name `xml:",any"`
}
func (StreamFeatures) Name() string {
return "stream:features"
}
type streamFeatureDecoder struct{}
var streamFeatures streamFeatureDecoder
func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamFeatures, error) {
var packet StreamFeatures
err := p.DecodeElement(&packet, &se)
return packet, err
}
// Capabilities
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
// and peer servers do not need to send service discovery requests each time they connect."
// This is not a stream feature but a way to let client cache server disco info.
type Caps struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
Hash string `xml:"hash,attr"`
Node string `xml:"node,attr"`
Ver string `xml:"ver,attr"`
Ext string `xml:"ext,attr,omitempty"`
}
// ============================================================================
// Supported Stream Features
// StartTLS feature
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4
type TlsStartTLS struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"`
Required bool
}
// UnmarshalXML implements custom parsing startTLS required flag
func (stls *TlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
stls.XMLName = start.Name
// 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:
elt := new(Node)
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
if elt.XMLName.Local == "required" {
stls.Required = true
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
func (sf *StreamFeatures) DoesStartTLS() (feature TlsStartTLS, isSupported bool) {
if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" {
return sf.StartTLS, true
}
return feature, false
}
// Mechanisms
// Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-6.4.1
type saslMechanisms struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"`
Mechanism []string `xml:"mechanism"`
}
// StreamManagement
// Reference: XEP-0198 - https://xmpp.org/extensions/xep-0198.html#feature
type streamManagement struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"`
}
func (streamManagement) Name() string {
return "streamManagement"
}
func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) {
if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" {
return true
}
return false
}
// P1 extensions
// Reference: https://docs.ejabberd.im/developer/mobile/core-features/
// p1:push support
type p1Push struct {
XMLName xml.Name `xml:"p1:push push"`
}
// p1:rebind suppor
type p1Rebind struct {
XMLName xml.Name `xml:"p1:rebind rebind"`
}
// p1:ack support
type p1Ack struct {
XMLName xml.Name `xml:"p1:ack ack"`
}
// ============================================================================
// StreamError Packet
type StreamError struct {
XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"`
Error xml.Name `xml:",any"`
Text string `xml:"urn:ietf:params:xml:ns:xmpp-streams text"`
}
func (StreamError) Name() string {
return "stream:error"
}
type streamErrorDecoder struct{}
var streamError streamErrorDecoder
func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamError, error) {
var packet StreamError
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// StreamClose "Packet"
// This is just a closing tag and hold no information
type StreamClosePacket struct{}
func (StreamClosePacket) Name() string {
return "stream:stream"
}
type streamCloseDecoder struct{}
var streamClose streamCloseDecoder
func (streamCloseDecoder) decode(_ xml.EndElement) StreamClosePacket {
return StreamClosePacket{}
}
+64
View File
@@ -0,0 +1,64 @@
package stanza_test
import (
"encoding/xml"
"testing"
"gosrc.io/xmpp/stanza"
)
func TestNoStartTLS(t *testing.T) {
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
</stream:features>`
var parsedSF stanza.StreamFeatures
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
}
startTLS, ok := parsedSF.DoesStartTLS()
if ok {
t.Error("StartTLS feature should not be enabled")
}
if startTLS.Required {
t.Error("StartTLS cannot be required as default")
}
}
func TestStartTLS(t *testing.T) {
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
<required/>
</starttls>
</stream:features>`
var parsedSF stanza.StreamFeatures
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
}
startTLS, ok := parsedSF.DoesStartTLS()
if !ok {
t.Error("StartTLS feature should be enabled")
}
if !startTLS.Required {
t.Error("StartTLS feature should be required")
}
}
// TODO: Ability to support / detect previous version of stream management feature
func TestStreamManagement(t *testing.T) {
streamFeatures := `<stream:features xmlns:stream='http://etherx.jabber.org/streams'>
<sm xmlns='urn:xmpp:sm:3'/>
</stream:features>`
var parsedSF stanza.StreamFeatures
if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil {
t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err)
}
ok := parsedSF.DoesStreamManagement()
if !ok {
t.Error("Stream Management feature should have been detected")
}
}
+392
View File
@@ -0,0 +1,392 @@
package stanza
import (
"encoding/xml"
"errors"
"sync"
)
const (
NSStreamManagement = "urn:xmpp:sm:3"
)
type SMEnable struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 enable"`
Max *uint `xml:"max,attr,omitempty"`
Resume *bool `xml:"resume,attr,omitempty"`
}
// Enabled as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#enable
type SMEnabled struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 enabled"`
Id string `xml:"id,attr,omitempty"`
Location string `xml:"location,attr,omitempty"`
Resume string `xml:"resume,attr,omitempty"`
Max uint `xml:"max,attr,omitempty"`
}
func (SMEnabled) Name() string {
return "Stream Management: enabled"
}
type UnAckQueue struct {
Uslice []*UnAckedStz
sync.RWMutex
}
type UnAckedStz struct {
Id int
Stz string
}
func NewUnAckQueue() *UnAckQueue {
return &UnAckQueue{
Uslice: make([]*UnAckedStz, 0, 10), // Capacity is 0 to comply with "Push" implementation (so that no reachable element is nil)
RWMutex: sync.RWMutex{},
}
}
func (u *UnAckedStz) QueueableName() string {
return "Un-acknowledged stanza"
}
func (uaq *UnAckQueue) PeekN(n int) []Queueable {
if uaq == nil {
return nil
}
if n <= 0 {
return nil
}
if len(uaq.Uslice) < n {
n = len(uaq.Uslice)
}
if len(uaq.Uslice) == 0 {
return nil
}
var r []Queueable
for i := 0; i < n; i++ {
r = append(r, uaq.Uslice[i])
}
return r
}
// No guarantee regarding thread safety !
func (uaq *UnAckQueue) Pop() Queueable {
if uaq == nil {
return nil
}
r := uaq.Peek()
if r != nil {
uaq.Uslice = uaq.Uslice[1:]
}
return r
}
// No guarantee regarding thread safety !
func (uaq *UnAckQueue) PopN(n int) []Queueable {
if uaq == nil {
return nil
}
r := uaq.PeekN(n)
uaq.Uslice = uaq.Uslice[len(r):]
return r
}
func (uaq *UnAckQueue) Peek() Queueable {
if uaq == nil {
return nil
}
if len(uaq.Uslice) == 0 {
return nil
}
r := uaq.Uslice[0]
return r
}
func (uaq *UnAckQueue) Push(s Queueable) error {
if uaq == nil {
return nil
}
pushIdx := 1
if len(uaq.Uslice) != 0 {
pushIdx = uaq.Uslice[len(uaq.Uslice)-1].Id + 1
}
sStz, ok := s.(*UnAckedStz)
if !ok {
return errors.New("element in not compatible with this queue. expected an UnAckedStz")
}
e := UnAckedStz{
Id: pushIdx,
Stz: sStz.Stz,
}
uaq.Uslice = append(uaq.Uslice, &e)
return nil
}
func (uaq *UnAckQueue) Empty() bool {
if uaq == nil {
return true
}
r := len(uaq.Uslice)
return r == 0
}
// Request as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMRequest struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 r"`
}
func (SMRequest) Name() string {
return "Stream Management: request"
}
// Answer as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMAnswer struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 a"`
H uint `xml:"h,attr"`
}
func (SMAnswer) Name() string {
return "Stream Management: answer"
}
// Resumed as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMResumed struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"`
PrevId string `xml:"previd,attr,omitempty"`
H *uint `xml:"h,attr,omitempty"`
}
func (SMResumed) Name() string {
return "Stream Management: resumed"
}
// Resume as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMResume struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 resume"`
PrevId string `xml:"previd,attr,omitempty"`
H *uint `xml:"h,attr,omitempty"`
}
func (SMResume) Name() string {
return "Stream Management: resume"
}
// Failed as defined in Stream Management spec
// Reference: https://xmpp.org/extensions/xep-0198.html#acking
type SMFailed struct {
XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"`
H *uint `xml:"h,attr,omitempty"`
StreamErrorGroup StanzaErrorGroup
}
func (SMFailed) Name() string {
return "Stream Management: failed"
}
func (smf *SMFailed) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
smf.XMLName = start.Name
// According to https://xmpp.org/rfcs/rfc3920.html#def we should have no attributes aside from the namespace
// which we don't use internally
// decode inner elements
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
// Decode sub-elements
var err error
switch tt.Name.Local {
case "bad-format":
bf := BadFormat{}
err = d.DecodeElement(&bf, &tt)
smf.StreamErrorGroup = &bf
case "bad-namespace-prefix":
bnp := BadNamespacePrefix{}
err = d.DecodeElement(&bnp, &tt)
smf.StreamErrorGroup = &bnp
case "conflict":
c := Conflict{}
err = d.DecodeElement(&c, &tt)
smf.StreamErrorGroup = &c
case "connection-timeout":
ct := ConnectionTimeout{}
err = d.DecodeElement(&ct, &tt)
smf.StreamErrorGroup = &ct
case "host-gone":
hg := HostGone{}
err = d.DecodeElement(&hg, &tt)
smf.StreamErrorGroup = &hg
case "host-unknown":
hu := HostUnknown{}
err = d.DecodeElement(&hu, &tt)
smf.StreamErrorGroup = &hu
case "improper-addressing":
ia := ImproperAddressing{}
err = d.DecodeElement(&ia, &tt)
smf.StreamErrorGroup = &ia
case "internal-server-error":
ise := InternalServerError{}
err = d.DecodeElement(&ise, &tt)
smf.StreamErrorGroup = &ise
case "invalid-from":
ifrm := InvalidForm{}
err = d.DecodeElement(&ifrm, &tt)
smf.StreamErrorGroup = &ifrm
case "invalid-id":
id := InvalidId{}
err = d.DecodeElement(&id, &tt)
smf.StreamErrorGroup = &id
case "invalid-namespace":
ins := InvalidNamespace{}
err = d.DecodeElement(&ins, &tt)
smf.StreamErrorGroup = &ins
case "invalid-xml":
ix := InvalidXML{}
err = d.DecodeElement(&ix, &tt)
smf.StreamErrorGroup = &ix
case "not-authorized":
na := NotAuthorized{}
err = d.DecodeElement(&na, &tt)
smf.StreamErrorGroup = &na
case "not-well-formed":
nwf := NotWellFormed{}
err = d.DecodeElement(&nwf, &tt)
smf.StreamErrorGroup = &nwf
case "policy-violation":
pv := PolicyViolation{}
err = d.DecodeElement(&pv, &tt)
smf.StreamErrorGroup = &pv
case "remote-connection-failed":
rcf := RemoteConnectionFailed{}
err = d.DecodeElement(&rcf, &tt)
smf.StreamErrorGroup = &rcf
case "resource-constraint":
rc := ResourceConstraint{}
err = d.DecodeElement(&rc, &tt)
smf.StreamErrorGroup = &rc
case "restricted-xml":
rx := RestrictedXML{}
err = d.DecodeElement(&rx, &tt)
smf.StreamErrorGroup = &rx
case "see-other-host":
soh := SeeOtherHost{}
err = d.DecodeElement(&soh, &tt)
smf.StreamErrorGroup = &soh
case "system-shutdown":
ss := SystemShutdown{}
err = d.DecodeElement(&ss, &tt)
smf.StreamErrorGroup = &ss
case "undefined-condition":
uc := UndefinedCondition{}
err = d.DecodeElement(&uc, &tt)
smf.StreamErrorGroup = &uc
case "unexpected-request":
ur := UnexpectedRequest{}
err = d.DecodeElement(&ur, &tt)
smf.StreamErrorGroup = &ur
case "unsupported-encoding":
ue := UnsupportedEncoding{}
err = d.DecodeElement(&ue, &tt)
smf.StreamErrorGroup = &ue
case "unsupported-stanza-type":
ust := UnsupportedStanzaType{}
err = d.DecodeElement(&ust, &tt)
smf.StreamErrorGroup = &ust
case "unsupported-version":
uv := UnsupportedVersion{}
err = d.DecodeElement(&uv, &tt)
smf.StreamErrorGroup = &uv
case "xml-not-well-formed":
xnwf := XMLNotWellFormed{}
err = d.DecodeElement(&xnwf, &tt)
smf.StreamErrorGroup = &xnwf
default:
return errors.New("error is unknown")
}
if err != nil {
return err
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
type smDecoder struct{}
var sm smDecoder
// decode decodes all known nonza in the stream management namespace.
func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local {
case "enabled":
return s.decodeEnabled(p, se)
case "resumed":
return s.decodeResumed(p, se)
case "resume":
return s.decodeResume(p, se)
case "r":
return s.decodeRequest(p, se)
case "a":
return s.decodeAnswer(p, se)
case "failed":
return s.decodeFailed(p, se)
default:
return nil, errors.New("unexpected XMPP packet " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
func (smDecoder) decodeEnabled(p *xml.Decoder, se xml.StartElement) (SMEnabled, error) {
var packet SMEnabled
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed, error) {
var packet SMResumed
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeResume(p *xml.Decoder, se xml.StartElement) (SMResume, error) {
var packet SMResume
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) {
var packet SMRequest
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeAnswer(p *xml.Decoder, se xml.StartElement) (SMAnswer, error) {
var packet SMAnswer
err := p.DecodeElement(&packet, &se)
return packet, err
}
func (smDecoder) decodeFailed(p *xml.Decoder, se xml.StartElement) (SMFailed, error) {
var packet SMFailed
err := p.DecodeElement(&packet, &se)
return packet, err
}
+226
View File
@@ -0,0 +1,226 @@
package stanza_test
import (
"gosrc.io/xmpp/stanza"
"math/rand"
"reflect"
"testing"
"time"
)
func TestPopEmptyQueue(t *testing.T) {
var uaq stanza.UnAckQueue
popped := uaq.Pop()
if popped != nil {
t.Fatalf("queue is empty but something was popped !")
}
}
func TestPushUnack(t *testing.T) {
uaq := initUnAckQueue()
toPush := stanza.UnAckedStz{
Id: 3,
Stz: `<iq type='submit'
from='confucius@scholars.lit/home'
to='registrar.scholars.lit'
id='kj3b157n'
xml:lang='en'>
<query xmlns='jabber:iq:register'>
<username>confucius</username>
<first>Qui</first>
<last>Kong</last>
</query>
</iq>`,
}
err := uaq.Push(&toPush)
if err != nil {
t.Fatalf("could not push element to the queue : %v", err)
}
if len(uaq.Uslice) != 4 {
t.Fatalf("push to the non-acked queue failed")
}
for i := 0; i < 4; i++ {
if uaq.Uslice[i].Id != i+1 {
t.Fatalf("indexes were not updated correctly. Expected %d got %d", i, uaq.Uslice[i].Id)
}
}
// Check that the queue is a fifo : popped element should not be the one we just pushed.
popped := uaq.Pop()
poppedElt, ok := popped.(*stanza.UnAckedStz)
if !ok {
t.Fatalf("popped element is not a *stanza.UnAckedStz")
}
if reflect.DeepEqual(*poppedElt, toPush) {
t.Fatalf("pushed element is at the top of the fifo queue when it should be at the bottom")
}
}
func TestPeekUnack(t *testing.T) {
uaq := initUnAckQueue()
expectedPeek := stanza.UnAckedStz{
Id: 1,
Stz: `<iq type='set'
from='romeo@montague.net/home'
to='characters.shakespeare.lit'
id='search2'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<last>Capulet</last>
</query>
</iq>`,
}
if !reflect.DeepEqual(expectedPeek, *uaq.Uslice[0]) {
t.Fatalf("peek failed to return the correct stanza")
}
}
func TestPeekNUnack(t *testing.T) {
uaq := initUnAckQueue()
initLen := len(uaq.Uslice)
randPop := rand.Int31n(int32(initLen))
peeked := uaq.PeekN(int(randPop))
if len(uaq.Uslice) != initLen {
t.Fatalf("queue length changed whith peek n operation : had %d found %d after peek", initLen, len(uaq.Uslice))
}
if len(peeked) != int(randPop) {
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", randPop, len(peeked))
}
}
func TestPeekNUnackTooLong(t *testing.T) {
uaq := initUnAckQueue()
initLen := len(uaq.Uslice)
// Have a random number of elements to peek that's greater than the queue size
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
peeked := uaq.PeekN(int(randPop))
if len(uaq.Uslice) != initLen {
t.Fatalf("total length changed whith peek n operation : had %d found %d after pop", initLen, len(uaq.Uslice))
}
if len(peeked) != initLen {
t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", initLen, len(peeked))
}
}
func TestPopNUnack(t *testing.T) {
uaq := initUnAckQueue()
initLen := len(uaq.Uslice)
randPop := rand.Int31n(int32(initLen))
popped := uaq.PopN(int(randPop))
if len(uaq.Uslice)+len(popped) != initLen {
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
}
for _, elt := range popped {
for _, oldElt := range uaq.Uslice {
if reflect.DeepEqual(elt, oldElt) {
t.Fatalf("pop n operation duplicated some elements")
}
}
}
}
func TestPopNUnackTooLong(t *testing.T) {
uaq := initUnAckQueue()
initLen := len(uaq.Uslice)
// Have a random number of elements to pop that's greater than the queue size
randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen)
popped := uaq.PopN(int(randPop))
if len(uaq.Uslice)+len(popped) != initLen {
t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped))
}
for _, elt := range popped {
for _, oldElt := range uaq.Uslice {
if reflect.DeepEqual(elt, oldElt) {
t.Fatalf("pop n operation duplicated some elements")
}
}
}
}
func TestPopUnack(t *testing.T) {
uaq := initUnAckQueue()
initLen := len(uaq.Uslice)
popped := uaq.Pop()
if len(uaq.Uslice)+1 != initLen {
t.Fatalf("total length changed whith pop operation : had %d found %d after pop", initLen, len(uaq.Uslice)+1)
}
for _, oldElt := range uaq.Uslice {
if reflect.DeepEqual(popped, oldElt) {
t.Fatalf("pop n operation duplicated some elements")
}
}
}
func initUnAckQueue() stanza.UnAckQueue {
q := []*stanza.UnAckedStz{
{
Id: 1,
Stz: `<iq type='set'
from='romeo@montague.net/home'
to='characters.shakespeare.lit'
id='search2'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<last>Capulet</last>
</query>
</iq>`,
},
{Id: 2,
Stz: `<iq type='get'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'
id='search3'
xml:lang='en'>
<query xmlns='jabber:iq:search'/>
</iq>`},
{Id: 3,
Stz: `<iq type='set'
from='juliet@capulet.com/balcony'
to='characters.shakespeare.lit'
id='search4'
xml:lang='en'>
<query xmlns='jabber:iq:search'>
<x xmlns='jabber:x:data' type='submit'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:search</value>
</field>
<field var='x-gender'>
<value>male</value>
</field>
</x>
</query>
</iq>`},
}
return stanza.UnAckQueue{Uslice: q}
}
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
+81
View File
@@ -0,0 +1,81 @@
package stanza_test
import (
"encoding/xml"
"errors"
"regexp"
"testing"
"github.com/google/go-cmp/cmp"
"gosrc.io/xmpp/stanza"
)
var reLeadcloseWhtsp = regexp.MustCompile(`^[\s\p{Zs}]+|[\s\p{Zs}]+$`)
var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`)
// ============================================================================
// Marshaller / unmarshaller test
func checkMarshalling(t *testing.T, iq *stanza.IQ) (*stanza.IQ, error) {
// Marshall
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal iq: %s\n%#v", err, iq)
return nil, err
}
// Unmarshall
var parsedIQ stanza.IQ
err = xml.Unmarshal(data, &parsedIQ)
if err != nil {
t.Errorf("Unmarshal returned error: %s\n%s", err, data)
}
return &parsedIQ, err
}
// ============================================================================
// XML structs comparison
// Compare iq structure but ignore empty namespace as they are set properly on
// marshal / unmarshal. There is no need to manage them on the manually
// crafted structure.
func xmlEqual(x, y interface{}) bool {
return cmp.Equal(x, y, xmlOpts())
}
// xmlDiff compares xml structures ignoring namespace preferences
func xmlDiff(x, y interface{}) string {
return cmp.Diff(x, y, xmlOpts())
}
func xmlOpts() cmp.Options {
alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true })
opts := cmp.Options{
cmp.FilterValues(func(x, y interface{}) bool {
xx, xok := x.(xml.Name)
yy, yok := y.(xml.Name)
if xok && yok {
zero := xml.Name{}
if xx == zero || yy == zero {
return true
}
if xx.Space == "" || yy.Space == "" {
return true
}
}
return false
}, alwaysEqual),
}
return opts
}
func delSpaces(s string) string {
return reInsideWhtsp.ReplaceAllString(reLeadcloseWhtsp.ReplaceAllString(s, ""), "")
}
func compareMarshal(expected, data string) error {
if delSpaces(expected) != delSpaces(data) {
return errors.New("failed to verify unmarshal->marshal. Expected :" + expected + "\ngot: " + data)
}
return nil
}