Refactor and move parsing and stanza to a separate package

This commit is contained in:
Mickael Remond
2019-06-26 17:14:52 +02:00
parent 0acf824217
commit 428787d7ab
51 changed files with 614 additions and 580 deletions

72
stanza/auth_sasl.go Normal file
View File

@@ -0,0 +1,72 @@
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
}

89
stanza/component.go Normal file
View File

@@ -0,0 +1,89 @@
package stanza
import (
"encoding/xml"
)
// ============================================================================
// Handshake Stanza
// Handshake is a stanza used by XMPP components to authenticate on XMPP
// component port.
type Handshake struct {
XMLName xml.Name `xml:"jabber:component:accept handshake"`
// TODO Add handshake value with test for proper serialization
// Value string `xml:",innerxml"`
}
func (Handshake) Name() string {
return "component:handshake"
}
// Handshake decoding wrapper
type handshakeDecoder struct{}
var handshake handshakeDecoder
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
var packet Handshake
err := p.DecodeElement(&packet, &se)
return packet, err
}
// ============================================================================
// Component delegation
// XEP-0355
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
// depending on the context.
type Delegation struct {
MsgExtension
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
Delegated *Delegated // This is used in a message to confirm delegated namespace
}
func (d *Delegation) Namespace() string {
return d.XMLName.Space
}
type Forwarded struct {
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
Stanza Packet
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Check subelements to extract required field as boolean
for {
t, err := d.Token()
if err != nil {
return err
}
switch tt := t.(type) {
case xml.StartElement:
if packet, err := decodeClient(d, tt); err == nil {
f.Stanza = packet
}
case xml.EndElement:
if tt == start.End() {
return nil
}
}
}
}
type Delegated struct {
XMLName xml.Name `xml:"delegated"`
Namespace string `xml:"namespace,attr,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
}

79
stanza/component_test.go Normal file
View File

@@ -0,0 +1,79 @@
package stanza
import (
"encoding/xml"
"testing"
)
// We should be able to properly parse delegation confirmation messages
func TestParsingDelegationMessage(t *testing.T) {
packetStr := `<message to='service.localhost' from='localhost'>
<delegation xmlns='urn:xmpp:delegation:1'>
<delegated namespace='http://jabber.org/protocol/pubsub'/>
</delegation>
</message>`
var msg Message
data := []byte(packetStr)
if err := xml.Unmarshal(data, &msg); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as MsgExtension
var nsDelegated string
for _, ext := range msg.Extensions {
if delegation, ok := ext.(*Delegation); ok {
nsDelegated = delegation.Delegated.Namespace
}
}
if nsDelegated != "http://jabber.org/protocol/pubsub" {
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
}
}
// Check that we can parse a delegation IQ.
// The most important thing is to be able to
func TestParsingDelegationIQ(t *testing.T) {
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
<delegation xmlns='urn:xmpp:delegation:1'>
<forwarded xmlns='urn:xmpp:forward:0'>
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='http://jabber.org/protocol/mood'>
<item id='current'>
<mood xmlns='http://jabber.org/protocol/mood'>
<excited/>
</mood>
</item>
</publish>
</pubsub>
</iq>
</forwarded>
</delegation>
</iq>`
var iq IQ
data := []byte(packetStr)
if err := xml.Unmarshal(data, &iq); err != nil {
t.Errorf("Unmarshal(%s) returned error", data)
}
// Check that we have extracted the delegation info as IQPayload
var node string
if iq.Payload != nil {
if delegation, ok := iq.Payload.(*Delegation); ok {
packet := delegation.Forwarded.Stanza
forwardedIQ, ok := packet.(IQ)
if !ok {
t.Errorf("Could not extract packet IQ")
return
}
if forwardedIQ.Payload != nil {
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
node = pubsub.Publish.Node
}
}
}
}
if node != "http://jabber.org/protocol/mood" {
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
}
}

114
stanza/error.go Normal file
View File

@@ -0,0 +1,114 @@
package stanza
import (
"encoding/xml"
"strconv"
)
// ============================================================================
// 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"`
}
func (x *Err) Namespace() string {
return x.XMLName.Space
}
// UnmarshalXML implements custom parsing for IQs
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"}
if elt.XMLName == textName {
x.Text = string(elt.Content)
} else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" {
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}
e.EncodeToken(xml.StartElement{Name: reason})
e.EncodeToken(xml.EndElement{Name: reason})
}
// Text
if x.Text != "" {
text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"}
e.EncodeToken(xml.StartElement{Name: text})
e.EncodeToken(xml.CharData(x.Text))
e.EncodeToken(xml.EndElement{Name: text})
}
return e.EncodeToken(xml.EndElement{Name: start.Name})
}

13
stanza/error_enum.go Normal file
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"
)

33
stanza/iot.go Normal file
View File

@@ -0,0 +1,33 @@
package stanza
import (
"encoding/xml"
)
type ControlSet struct {
XMLName xml.Name `xml:"urn:xmpp:iot:control set"`
Fields []ControlField `xml:",any"`
}
func (c *ControlSet) Namespace() string {
return c.XMLName.Space
}
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 {
IQPayload
XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
}
func (c *ControlSetResponse) Namespace() string {
return c.XMLName.Space
}

26
stanza/iot_test.go Normal file
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("Paylod is not an iot control set: %v", cs)
}
}

206
stanza/iq.go Normal file
View File

@@ -0,0 +1,206 @@
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
}
}
}
}
// ============================================================================
// 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{})
}

170
stanza/iq_test.go Normal file
View File

@@ -0,0 +1,170 @@
package stanza_test
import (
"encoding/xml"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestUnmarshalIqs(t *testing.T) {
//var cs1 = new(iot.ControlSet)
var tests = []struct {
iqString string
parsedIQ IQ
}{
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
IQ{XMLName: xml.Name{Local: "iq"}, Attrs: Attrs{Type: 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 := 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 TestGenerateIq(t *testing.T) {
iq := NewIQ(Attrs{Type: IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"})
payload := DiscoInfo{
Identity: Identity{
Name: "Test Gateway",
Category: "gateway",
Type: "mqtt",
},
Features: []Feature{
{Var: NSDiscoInfo},
{Var: 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 := 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 TestErrorTag(t *testing.T) {
xError := 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 := 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 := NewIQ(Attrs{Type: IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"})
payload := DiscoItems{
Node: "music",
}
iq.Payload = &payload
data, err := xml.Marshal(iq)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
parsedIQ := 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 := 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 := 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 := 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)
}
}

148
stanza/message.go Normal file
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
}
}
}
}

75
stanza/message_test.go Normal file
View File

@@ -0,0 +1,75 @@
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestGenerateMessage(t *testing.T) {
message := NewMessage(Attrs{Type: 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 := 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 := 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 := NewMessage(Attrs{To: "test@localhost"})
ext := 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 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 Markable
if ok := msg.Get(&m); ok {
t.Error("we should not have found markable extension")
}
}

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{NSMsgChatMarkers, "markable"}, Markable{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "received"}, MarkReceived{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "displayed"}, MarkDisplayed{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "acknowledged"}, MarkAcknowledged{})
}

45
stanza/msg_chat_state.go Normal file
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{NSMsgChatStateNotifications, "active"}, StateActive{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "composing"}, StateComposing{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "gone"}, StateGone{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "inactive"}, StateInactive{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "paused"}, StatePaused{})
}

22
stanza/msg_html.go Normal file
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{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{})
}

42
stanza/msg_html_test.go Normal file
View File

@@ -0,0 +1,42 @@
package stanza_test
import (
"encoding/xml"
"testing"
)
func TestHTMLGen(t *testing.T) {
htmlBody := "<p>Hello <b>World</b></p>"
msg := NewMessage(Attrs{To: "test@localhost"})
msg.Body = "Hello World"
body := HTMLBody{
InnerXML: htmlBody,
}
html := 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 := 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 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
stanza/msg_oob.go Normal file
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{"jabber:x:oob", "x"}, OOB{})
}

29
stanza/msg_receipts.go Normal file
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{NSMsgReceipts, "request"}, ReceiptRequest{})
TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{})
}

View File

@@ -0,0 +1,40 @@
package stanza_test
import (
"encoding/xml"
"testing"
)
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 := 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 *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")
}
}

51
stanza/node.go Normal file
View File

@@ -0,0 +1,51 @@
package stanza
import "encoding/xml"
// ============================================================================
// Generic / unknown content
// Node is a generic structure to represent XML data. It is used to parse
// unreferenced or custom stanza payload.
type Node struct {
XMLName xml.Name
Attrs []xml.Attr `xml:"-"`
Content string `xml:",innerxml"`
Nodes []Node `xml:",any"`
}
func (n *Node) Namespace() string {
return n.XMLName.Space
}
// Attr represents generic XML attributes, as used on the generic XML Node
// representation.
type Attr struct {
K string
V string
}
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
// transform generic XML content into hierarchical Node structure.
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
for _, attr := range start.Attr {
// Do not repeat xmlns, it is already in XMLName
if attr.Name.Local != "xmlns" {
n.Attrs = append(n.Attrs, attr)
}
}
type node Node
return d.DecodeElement((*node)(n), &start)
}
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
// Node structure to XML.
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
start.Attr = n.Attrs
start.Name = n.XMLName
err = e.EncodeToken(start)
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
return e.EncodeToken(xml.EndElement{Name: start.Name})
}

11
stanza/ns.go Normal file
View File

@@ -0,0 +1,11 @@
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"
NSClient = "jabber:client"
NSComponent = "jabber:component:accept"
)

18
stanza/packet.go Normal file
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
}

25
stanza/packet_enum.go Normal file
View File

@@ -0,0 +1,25 @@
package stanza
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"
)

151
stanza/parser.go Normal file
View File

@@ -0,0 +1,151 @@
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:
if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected <stream> 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
se, err := NextStart(p)
if err != nil {
return nil, err
}
// 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)
default:
return nil, errors.New("unknown namespace " +
se.Name.Space + " <" + se.Name.Local + "/>")
}
}
// Scan XML token stream to find next StartElement.
func NextStart(p *xml.Decoder) (xml.StartElement, error) {
for {
t, err := p.Token()
if err == io.EOF {
return xml.StartElement{}, errors.New("connection closed")
}
if err != nil {
return xml.StartElement{}, fmt.Errorf("NextStart %s", err)
}
switch t := t.(type) {
case xml.StartElement:
return t, nil
}
}
}
/*
TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that
type.
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, se xml.StartElement) (Packet, error) {
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 + "/>")
}
}
// 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 + "/>")
}
}
// decodeClient 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
stanza/pep.go Normal file
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 deta 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"`
}

29
stanza/pres_muc.go Normal file
View File

@@ -0,0 +1,29 @@
package stanza
import (
"encoding/xml"
"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"`
}
// History implements XEP-0045: Multi-User Chat - 19.1
type History struct {
MaxChars int `xml:"maxchars,attr,omitempty"`
MaxStanzas int `xml:"maxstanzas,attr,omitempty"`
Seconds int `xml:"seconds,attr,omitempty"`
Since time.Time `xml:"since,attr,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTPresence, xml.Name{"http://jabber.org/protocol/muc", "x"}, MucPresence{})
}

58
stanza/pres_muc_test.go Normal file
View File

@@ -0,0 +1,58 @@
package stanza_test
import (
"encoding/xml"
"testing"
)
// 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 Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", str)
}
var muc 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 Presence
if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil {
t.Errorf("Unmarshal(%s) returned error", str)
}
var muc MucPresence
if ok := parsedPresence.Get(&muc); !ok {
t.Error("muc presence extension was not found")
}
if muc.History.MaxStanzas != 20 {
t.Errorf("incorrect max stanza: '%d'", muc.History.MaxStanzas)
}
}

139
stanza/presence.go Normal file
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
stanza/presence_enum.go Normal file
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"
)

62
stanza/presence_test.go Normal file
View File

@@ -0,0 +1,62 @@
package stanza_test
import (
"encoding/xml"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestGeneratePresence(t *testing.T) {
presence := NewPresence(Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = PresenceShowChat
data, err := xml.Marshal(presence)
if err != nil {
t.Errorf("cannot marshal xml structure")
}
var parsedPresence 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 PresenceShow `xml:"show"`
Status string `xml:"status"`
Priority int8 `xml:"priority"`
}
presence := NewPresence(Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = 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)
}
}

40
stanza/pubsub.go Normal file
View File

@@ -0,0 +1,40 @@
package stanza
import (
"encoding/xml"
)
type PubSub struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
Publish *Publish
Retract *Retract
// TODO <configure/>
}
func (p *PubSub) Namespace() string {
return p.XMLName.Space
}
type Publish struct {
XMLName xml.Name `xml:"publish"`
Node string `xml:"node,attr"`
Item Item
}
type Item struct {
XMLName xml.Name `xml:"item"`
Id string `xml:"id,attr,omitempty"`
Tune *Tune
Mood *Mood
}
type Retract struct {
XMLName xml.Name `xml:"retract"`
Node string `xml:"node,attr"`
Notify string `xml:"notify,attr"`
Item Item
}
func init() {
TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
}

119
stanza/registry.go Normal file
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
stanza/registry_test.go Normal file
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
}
}
}

17
stanza/starttls.go Normal file
View File

@@ -0,0 +1,17 @@
package stanza
import (
"crypto/tls"
"encoding/xml"
)
var DefaultTlsConfig tls.Config
// 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"`
}

166
stanza/stream.go Normal file
View File

@@ -0,0 +1,166 @@
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 BindBind
Session sessionSession
StreamManagement streamManagement
// 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 (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
}

29
stanza/xmpp_test.go Normal file
View File

@@ -0,0 +1,29 @@
package stanza_test
import (
"encoding/xml"
"github.com/google/go-cmp/cmp"
)
// 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 {
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
}
}
return false
}, alwaysEqual),
}
return cmp.Equal(x, y, opts)
}