Merge pull request #79: Stanza package & pattern to help building stanzas
- Move parsing and stanza marshalling / unmarshalling to stanza package - Add pattern & basic helpers to simplify stanza building. This was requested on #61disco_info_form
commit
0fd1bb2483
@ -1,254 +0,0 @@
|
||||
package xmpp
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO support ability to put Raw payload inside IQ
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// IQ Packet
|
||||
|
||||
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
|
||||
type IQ struct { // Info/Query
|
||||
XMLName xml.Name `xml:"iq"`
|
||||
// MUST have a ID
|
||||
Attrs
|
||||
// We can only have one payload on IQ:
|
||||
// "An IQ stanza of type "get" or "set" MUST contain exactly one
|
||||
// child element, which specifies the semantics of the particular
|
||||
// request."
|
||||
Payload IQPayload `xml:",omitempty"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
// Any is used to decode unknown payload as a generique structure
|
||||
Any *Node `xml:",any"`
|
||||
}
|
||||
|
||||
type IQPayload interface {
|
||||
Namespace() string
|
||||
}
|
||||
|
||||
func NewIQ(a Attrs) IQ {
|
||||
// TODO generate IQ ID if not set
|
||||
// TODO ensure that type is set, as it is required
|
||||
return IQ{
|
||||
XMLName: xml.Name{Local: "iq"},
|
||||
Attrs: a,
|
||||
}
|
||||
}
|
||||
|
||||
func (iq IQ) MakeError(xerror Err) IQ {
|
||||
from := iq.From
|
||||
to := iq.To
|
||||
|
||||
iq.Type = "error"
|
||||
iq.From = to
|
||||
iq.To = from
|
||||
iq.Error = xerror
|
||||
|
||||
return iq
|
||||
}
|
||||
|
||||
func (IQ) Name() string {
|
||||
return "iq"
|
||||
}
|
||||
|
||||
type iqDecoder struct{}
|
||||
|
||||
var iq iqDecoder
|
||||
|
||||
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
|
||||
var packet IQ
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
iq.XMLName = start.Name
|
||||
|
||||
// Extract IQ attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "id" {
|
||||
iq.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
iq.Type = StanzaType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
iq.To = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "from" {
|
||||
iq.From = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
case xml.StartElement:
|
||||
if tt.Name.Local == "error" {
|
||||
var xmppError Err
|
||||
err = d.DecodeElement(&xmppError, &tt)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
iq.Error = xmppError
|
||||
continue
|
||||
}
|
||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||
// Decode payload extension
|
||||
err = d.DecodeElement(iqExt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Payload = iqExt
|
||||
continue
|
||||
}
|
||||
// TODO: If unknown decode as generic node
|
||||
node := new(Node)
|
||||
err = d.DecodeElement(node, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Any = node
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Generic / unknown content
|
||||
|
||||
// Node is a generic structure to represent XML data. It is used to parse
|
||||
// unreferenced or custom stanza payload.
|
||||
type Node struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",innerxml"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
func (n *Node) Namespace() string {
|
||||
return n.XMLName.Space
|
||||
}
|
||||
|
||||
// Attr represents generic XML attributes, as used on the generic XML Node
|
||||
// representation.
|
||||
type Attr struct {
|
||||
K string
|
||||
V string
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
|
||||
for _, attr := range start.Attr {
|
||||
// Do not repeat xmlns, it is already in XMLName
|
||||
if attr.Name.Local != "xmlns" {
|
||||
n.Attrs = append(n.Attrs, attr)
|
||||
}
|
||||
}
|
||||
type node Node
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
|
||||
// Node structure to XML.
|
||||
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
start.Attr = n.Attrs
|
||||
start.Name = n.XMLName
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Disco
|
||||
|
||||
const (
|
||||
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
||||
NSDiscoItems = "http://jabber.org/protocol/disco#items"
|
||||
)
|
||||
|
||||
// Disco Info
|
||||
type DiscoInfo struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Identity Identity `xml:"identity"`
|
||||
Features []Feature `xml:"feature"`
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
XMLName xml.Name `xml:"identity,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Category string `xml:"category,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
XMLName xml.Name `xml:"feature"`
|
||||
Var string `xml:"var,attr"`
|
||||
}
|
||||
|
||||
// Disco Items
|
||||
type DiscoItems struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Items []DiscoItem `xml:"item"`
|
||||
}
|
||||
|
||||
func (d *DiscoItems) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
type DiscoItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
JID string `xml:"jid,attr,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Software Version (XEP-0092)
|
||||
|
||||
// Version
|
||||
type Version struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:version query"`
|
||||
Name string `xml:"name,omitempty"`
|
||||
Version string `xml:"version,omitempty"`
|
||||
OS string `xml:"os,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Version) Namespace() string {
|
||||
return v.XMLName.Space
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
# XMPP Stanza
|
||||
|
||||
XMPP `stanza` package is use to parse, marshall and and unmarshal XMPP stanzas and nonzas.
|
||||
|
||||
## Stanza creation
|
||||
|
||||
When creating stanzas, you can use two approaches:
|
||||
|
||||
1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the
|
||||
stanza.
|
||||
2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the
|
||||
final stanza.
|
||||
|
||||
The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of
|
||||
struct that you can build by hand.
|
||||
|
||||
### Composing stanzas manually with structs
|
||||
|
||||
Here is for example how you would generate an IQ discovery result:
|
||||
|
||||
iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"})
|
||||
identity := stanza.Identity{
|
||||
Name: opts.Name,
|
||||
Category: opts.Category,
|
||||
Type: opts.Type,
|
||||
}
|
||||
payload := stanza.DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: stanza.NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
Identity: []stanza.Identity{identity},
|
||||
Features: []stanza.Feature{
|
||||
{Var: stanza.NSDiscoInfo},
|
||||
{Var: stanza.NSDiscoItems},
|
||||
{Var: "jabber:iq:version"},
|
||||
{Var: "urn:xmpp:delegation:1"},
|
||||
},
|
||||
}
|
||||
iqResp.Payload = &payload
|
||||
|
||||
### Using helpers
|
||||
|
||||
Here is for example how you would generate an IQ discovery result using Builder:
|
||||
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
|
||||
## Payload and extensions
|
||||
|
||||
### Message
|
||||
|
||||
### Presence
|
||||
|
||||
### IQ
|
||||
|
||||
IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with
|
||||
Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support
|
||||
having multiple payloads.
|
||||
|
||||
Here is the list of structs implementing IQPayloads:
|
||||
|
||||
- BindBind
|
||||
- Pubsub
|
||||
|
||||
Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic
|
||||
`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases,
|
||||
you may also consider [adding your own custom extensions on stanzas]().
|
||||
|
||||
|
||||
## Adding your own custom extensions on stanzas
|
||||
|
||||
Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may
|
||||
want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas.
|
@ -0,0 +1,79 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// SASLSuccess
|
||||
|
||||
type SASLSuccess struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"`
|
||||
}
|
||||
|
||||
func (SASLSuccess) Name() string {
|
||||
return "sasl:success"
|
||||
}
|
||||
|
||||
type saslSuccessDecoder struct{}
|
||||
|
||||
var saslSuccess saslSuccessDecoder
|
||||
|
||||
func (saslSuccessDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLSuccess, error) {
|
||||
var packet SASLSuccess
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SASLFailure
|
||||
|
||||
type SASLFailure struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"`
|
||||
Any xml.Name // error reason is a subelement
|
||||
}
|
||||
|
||||
func (SASLFailure) Name() string {
|
||||
return "sasl:failure"
|
||||
}
|
||||
|
||||
type saslFailureDecoder struct{}
|
||||
|
||||
var saslFailure saslFailureDecoder
|
||||
|
||||
func (saslFailureDecoder) decode(p *xml.Decoder, se xml.StartElement) (SASLFailure, error) {
|
||||
var packet SASLFailure
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
type Auth struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"`
|
||||
Mechanism string `xml:"mecanism,attr"`
|
||||
Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type BindBind struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
|
||||
Resource string `xml:"resource,omitempty"`
|
||||
Jid string `xml:"jid,omitempty"`
|
||||
}
|
||||
|
||||
func (b *BindBind) Namespace() string {
|
||||
return b.XMLName.Space
|
||||
}
|
||||
|
||||
// Session is obsolete in RFC 6121.
|
||||
// Added for compliance with RFC 3121.
|
||||
// Remove when ejabberd purely conforms to RFC 6121.
|
||||
type sessionSession struct {
|
||||
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"`
|
||||
Optional xml.Name // If it does exist, it mean we are not required to open session
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{})
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Handshake Stanza
|
||||
|
||||
// Handshake is a stanza used by XMPP components to authenticate on XMPP
|
||||
// component port.
|
||||
type Handshake struct {
|
||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||
// TODO Add handshake value with test for proper serialization
|
||||
// Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (Handshake) Name() string {
|
||||
return "component:handshake"
|
||||
}
|
||||
|
||||
// Handshake decoding wrapper
|
||||
|
||||
type handshakeDecoder struct{}
|
||||
|
||||
var handshake handshakeDecoder
|
||||
|
||||
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
|
||||
var packet Handshake
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component delegation
|
||||
// XEP-0355
|
||||
|
||||
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
|
||||
// depending on the context.
|
||||
type Delegation struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
||||
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
||||
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
||||
}
|
||||
|
||||
func (d *Delegation) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
type Forwarded struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
|
||||
Stanza Packet
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Check subelements to extract required field as boolean
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
if packet, err := decodeClient(d, tt); err == nil {
|
||||
f.Stanza = packet
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Delegated struct {
|
||||
XMLName xml.Name `xml:"delegated"`
|
||||
Namespace string `xml:"namespace,attr,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// We should be able to properly parse delegation confirmation messages
|
||||
func TestParsingDelegationMessage(t *testing.T) {
|
||||
packetStr := `<message to='service.localhost' from='localhost'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<delegated namespace='http://jabber.org/protocol/pubsub'/>
|
||||
</delegation>
|
||||
</message>`
|
||||
var msg Message
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &msg); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as MsgExtension
|
||||
var nsDelegated string
|
||||
for _, ext := range msg.Extensions {
|
||||
if delegation, ok := ext.(*Delegation); ok {
|
||||
nsDelegated = delegation.Delegated.Namespace
|
||||
}
|
||||
}
|
||||
if nsDelegated != "http://jabber.org/protocol/pubsub" {
|
||||
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we can parse a delegation IQ.
|
||||
// The most important thing is to be able to
|
||||
func TestParsingDelegationIQ(t *testing.T) {
|
||||
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<publish node='http://jabber.org/protocol/mood'>
|
||||
<item id='current'>
|
||||
<mood xmlns='http://jabber.org/protocol/mood'>
|
||||
<excited/>
|
||||
</mood>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>
|
||||
</forwarded>
|
||||
</delegation>
|
||||
</iq>`
|
||||
var iq IQ
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &iq); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as IQPayload
|
||||
var node string
|
||||
if iq.Payload != nil {
|
||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||
packet := delegation.Forwarded.Stanza
|
||||
forwardedIQ, ok := packet.(IQ)
|
||||
if !ok {
|
||||
t.Errorf("Could not extract packet IQ")
|
||||
return
|
||||
}
|
||||
if forwardedIQ.Payload != nil {
|
||||
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
|
||||
node = pubsub.Publish.Node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if node != "http://jabber.org/protocol/mood" {
|
||||
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
|
||||
}
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO support ability to put Raw payload inside IQ
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// XMPP Errors
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
// ErrorType is a Enum of error attribute type
|
||||
type ErrorType string
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -0,0 +1,130 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
TODO support ability to put Raw payload inside IQ
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// IQ Packet
|
||||
|
||||
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
|
||||
type IQ struct { // Info/Query
|
||||
XMLName xml.Name `xml:"iq"`
|
||||
// MUST have a ID
|
||||
Attrs
|
||||
// We can only have one payload on IQ:
|
||||
// "An IQ stanza of type "get" or "set" MUST contain exactly one
|
||||
// child element, which specifies the semantics of the particular
|
||||
// request."
|
||||
Payload IQPayload `xml:",omitempty"`
|
||||
Error Err `xml:"error,omitempty"`
|
||||
// Any is used to decode unknown payload as a generique structure
|
||||
Any *Node `xml:",any"`
|
||||
}
|
||||
|
||||
type IQPayload interface {
|
||||
Namespace() string
|
||||
}
|
||||
|
||||
func NewIQ(a Attrs) IQ {
|
||||
// TODO generate IQ ID if not set
|
||||
// TODO ensure that type is set, as it is required
|
||||
return IQ{
|
||||
XMLName: xml.Name{Local: "iq"},
|
||||
Attrs: a,
|
||||
}
|
||||
}
|
||||
|
||||
func (iq IQ) MakeError(xerror Err) IQ {
|
||||
from := iq.From
|
||||
to := iq.To
|
||||
|
||||
iq.Type = "error"
|
||||
iq.From = to
|
||||
iq.To = from
|
||||
iq.Error = xerror
|
||||
|
||||
return iq
|
||||
}
|
||||
|
||||
func (IQ) Name() string {
|
||||
return "iq"
|
||||
}
|
||||
|
||||
type iqDecoder struct{}
|
||||
|
||||
var iq iqDecoder
|
||||
|
||||
func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
|
||||
var packet IQ
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// UnmarshalXML implements custom parsing for IQs
|
||||
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
iq.XMLName = start.Name
|
||||
|
||||
// Extract IQ attributes
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "id" {
|
||||
iq.Id = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "type" {
|
||||
iq.Type = StanzaType(attr.Value)
|
||||
}
|
||||
if attr.Name.Local == "to" {
|
||||
iq.To = attr.Value
|
||||
}
|
||||
if attr.Name.Local == "from" {
|
||||
iq.From = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// decode inner elements
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
case xml.StartElement:
|
||||
if tt.Name.Local == "error" {
|
||||
var xmppError Err
|
||||
err = d.DecodeElement(&xmppError, &tt)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
iq.Error = xmppError
|
||||
continue
|
||||
}
|
||||
if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil {
|
||||
// Decode payload extension
|
||||
err = d.DecodeElement(iqExt, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Payload = iqExt
|
||||
continue
|
||||
}
|
||||
// TODO: If unknown decode as generic node
|
||||
node := new(Node)
|
||||
err = d.DecodeElement(node, &tt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iq.Any = node
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Disco Info
|
||||
|
||||
const (
|
||||
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
|
||||
)
|
||||
|
||||
// ----------
|
||||
// Namespaces
|
||||
|
||||
type DiscoInfo struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Identity []Identity `xml:"identity"`
|
||||
Features []Feature `xml:"feature"`
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Builder helpers
|
||||
|
||||
// DiscoInfo builds a default DiscoInfo payload
|
||||
func (iq *IQ) DiscoInfo() *DiscoInfo {
|
||||
d := DiscoInfo{
|
||||
XMLName: xml.Name{
|
||||
Space: NSDiscoInfo,
|
||||
Local: "query",
|
||||
},
|
||||
}
|
||||
iq.Payload = &d
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) AddIdentity(name, category, typ string) {
|
||||
identity := Identity{
|
||||
XMLName: xml.Name{Local: "identity"},
|
||||
Name: name,
|
||||
Category: category,
|
||||
Type: typ,
|
||||
}
|
||||
d.Identity = append(d.Identity, identity)
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) AddFeatures(namespace ...string) {
|
||||
for _, ns := range namespace {
|
||||
d.Features = append(d.Features, Feature{Var: ns})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetNode(node string) {
|
||||
d.Node = node
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo {
|
||||
d.Identity = ident
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo {
|
||||
for _, ns := range namespace {
|
||||
d.Features = append(d.Features, Feature{Var: ns})
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// -----------
|
||||
// SubElements
|
||||
|
||||
type Identity struct {
|
||||
XMLName xml.Name `xml:"identity,omitempty"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Category string `xml:"category,attr,omitempty"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Feature struct {
|
||||
XMLName xml.Name `xml:"feature"`
|
||||
Var string `xml:"var,attr"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Disco Info
|
||||
|
||||
const (
|
||||
NSDiscoItems = "http://jabber.org/protocol/disco#items"
|
||||
)
|
||||
|
||||
type DiscoItems struct {
|
||||
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
Items []DiscoItem `xml:"item"`
|
||||
}
|
||||
|
||||
func (d *DiscoItems) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
type DiscoItem struct {
|
||||
XMLName xml.Name `xml:"item"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
JID string `xml:"jid,attr,omitempty"`
|
||||
Node string `xml:"node,attr,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{})
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package stanza_test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
)
|
||||
|
||||
func TestDiscoInfoBuilder(t *testing.T) {
|
||||
iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"})
|
||||
disco := iq.DiscoInfo()
|
||||
disco.AddIdentity("Test Component", "gateway", "service")
|
||||
disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1")
|
||||
|
||||
// Marshall
|
||||
data, err := xml.Marshal(iq)
|
||||
if err != nil {
|
||||
t.Errorf("cannot marshal xml structure: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshall
|
||||
var parsedIQ stanza.IQ
|
||||
if err = xml.Unmarshal(data, &parsedIQ); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error: %s", data, err)
|
||||
}
|
||||
|
||||
// Check result
|
||||
pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo)
|
||||
if !ok {
|
||||
t.Errorf("Parsed stanza does not contain an IQ payload")
|
||||
}
|
||||
|
||||
// Check features
|
||||
features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"}
|
||||
if len(pp.Features) != len(features) {
|
||||
t.Errorf("Features length mismatch: %#v", pp.Features)
|
||||
} else {
|
||||
for i, f := range pp.Features {
|
||||
if f.Var != features[i] {
|
||||
t.Errorf("Missing feature: %s", features[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check identity
|
||||
if len(pp.Identity) != 1 {
|
||||
t.Errorf("Identity length mismatch: %#v", pp.Identity)
|
||||
} else {
|
||||
if pp.Identity[0].Name != "Test Component" {
|
||||
t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// Software Version (XEP-0092)
|
||||
|
||||
// Version
|
||||
type Version struct {
|
||||
XMLName xml.Name `xml:"jabber:iq:version query"`
|
||||
Name string `xml:"name,omitempty"`
|
||||
Version string `xml:"version,omitempty"`
|
||||
OS string `xml:"os,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Version) Namespace() string {
|
||||
return v.XMLName.Space
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry init
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type HTML struct {
|
||||
MsgExtension
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -1,6 +1,8 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
/*
|
||||
Support for:
|
@ -0,0 +1,51 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// Generic / unknown content
|
||||
|
||||
// Node is a generic structure to represent XML data. It is used to parse
|
||||
// unreferenced or custom stanza payload.
|
||||
type Node struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",innerxml"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
func (n *Node) Namespace() string {
|
||||
return n.XMLName.Space
|
||||
}
|
||||
|
||||
// Attr represents generic XML attributes, as used on the generic XML Node
|
||||
// representation.
|
||||
type Attr struct {
|
||||
K string
|
||||
V string
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
|
||||
for _, attr := range start.Attr {
|
||||
// Do not repeat xmlns, it is already in XMLName
|
||||
if attr.Name.Local != "xmlns" {
|
||||
n.Attrs = append(n.Attrs, attr)
|
||||
}
|
||||
}
|
||||
type node Node
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
|
||||
// Node structure to XML.
|
||||
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
start.Attr = n.Attrs
|
||||
start.Name = n.XMLName
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
const (
|
||||
NSStream = "http://etherx.jabber.org/streams"
|
||||
nsTLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||
nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
NSBind = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
NSSession = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
NSClient = "jabber:client"
|
||||
NSComponent = "jabber:component:accept"
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
type Packet interface {
|
||||
Name() string
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
type StanzaType string
|
||||
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
// PresenceShow is a Enum of presence element show
|
||||
type PresenceShow string
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
@ -1,4 +1,4 @@
|
||||
package xmpp
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
Loading…
Reference in new issue