mirror of
https://github.com/FluuxIO/go-xmpp.git
synced 2025-11-02 01:03:45 -07:00
Refactor and move parsing and stanza to a separate package
This commit is contained in:
72
stanza/auth_sasl.go
Normal file
72
stanza/auth_sasl.go
Normal 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
89
stanza/component.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Handshake Stanza
|
||||
|
||||
// Handshake is a stanza used by XMPP components to authenticate on XMPP
|
||||
// component port.
|
||||
type Handshake struct {
|
||||
XMLName xml.Name `xml:"jabber:component:accept handshake"`
|
||||
// TODO Add handshake value with test for proper serialization
|
||||
// Value string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (Handshake) Name() string {
|
||||
return "component:handshake"
|
||||
}
|
||||
|
||||
// Handshake decoding wrapper
|
||||
|
||||
type handshakeDecoder struct{}
|
||||
|
||||
var handshake handshakeDecoder
|
||||
|
||||
func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake, error) {
|
||||
var packet Handshake
|
||||
err := p.DecodeElement(&packet, &se)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component delegation
|
||||
// XEP-0355
|
||||
|
||||
// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
|
||||
// depending on the context.
|
||||
type Delegation struct {
|
||||
MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"`
|
||||
Forwarded *Forwarded // This is used in iq to wrap delegated iqs
|
||||
Delegated *Delegated // This is used in a message to confirm delegated namespace
|
||||
}
|
||||
|
||||
func (d *Delegation) Namespace() string {
|
||||
return d.XMLName.Space
|
||||
}
|
||||
|
||||
type Forwarded struct {
|
||||
XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
|
||||
Stanza Packet
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Check subelements to extract required field as boolean
|
||||
for {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch tt := t.(type) {
|
||||
|
||||
case xml.StartElement:
|
||||
if packet, err := decodeClient(d, tt); err == nil {
|
||||
f.Stanza = packet
|
||||
}
|
||||
|
||||
case xml.EndElement:
|
||||
if tt == start.End() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Delegated struct {
|
||||
XMLName xml.Name `xml:"delegated"`
|
||||
Namespace string `xml:"namespace,attr,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
|
||||
}
|
||||
79
stanza/component_test.go
Normal file
79
stanza/component_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package stanza
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// We should be able to properly parse delegation confirmation messages
|
||||
func TestParsingDelegationMessage(t *testing.T) {
|
||||
packetStr := `<message to='service.localhost' from='localhost'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<delegated namespace='http://jabber.org/protocol/pubsub'/>
|
||||
</delegation>
|
||||
</message>`
|
||||
var msg Message
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &msg); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as MsgExtension
|
||||
var nsDelegated string
|
||||
for _, ext := range msg.Extensions {
|
||||
if delegation, ok := ext.(*Delegation); ok {
|
||||
nsDelegated = delegation.Delegated.Namespace
|
||||
}
|
||||
}
|
||||
if nsDelegated != "http://jabber.org/protocol/pubsub" {
|
||||
t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we can parse a delegation IQ.
|
||||
// The most important thing is to be able to
|
||||
func TestParsingDelegationIQ(t *testing.T) {
|
||||
packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
|
||||
<delegation xmlns='urn:xmpp:delegation:1'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<publish node='http://jabber.org/protocol/mood'>
|
||||
<item id='current'>
|
||||
<mood xmlns='http://jabber.org/protocol/mood'>
|
||||
<excited/>
|
||||
</mood>
|
||||
</item>
|
||||
</publish>
|
||||
</pubsub>
|
||||
</iq>
|
||||
</forwarded>
|
||||
</delegation>
|
||||
</iq>`
|
||||
var iq IQ
|
||||
data := []byte(packetStr)
|
||||
if err := xml.Unmarshal(data, &iq); err != nil {
|
||||
t.Errorf("Unmarshal(%s) returned error", data)
|
||||
}
|
||||
|
||||
// Check that we have extracted the delegation info as IQPayload
|
||||
var node string
|
||||
if iq.Payload != nil {
|
||||
if delegation, ok := iq.Payload.(*Delegation); ok {
|
||||
packet := delegation.Forwarded.Stanza
|
||||
forwardedIQ, ok := packet.(IQ)
|
||||
if !ok {
|
||||
t.Errorf("Could not extract packet IQ")
|
||||
return
|
||||
}
|
||||
if forwardedIQ.Payload != nil {
|
||||
if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok {
|
||||
node = pubsub.Publish.Node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if node != "http://jabber.org/protocol/mood" {
|
||||
t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
|
||||
}
|
||||
}
|
||||
114
stanza/error.go
Normal file
114
stanza/error.go
Normal 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
13
stanza/error_enum.go
Normal 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
33
stanza/iot.go
Normal 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
26
stanza/iot_test.go
Normal 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
206
stanza/iq.go
Normal 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
170
stanza/iq_test.go
Normal 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
148
stanza/message.go
Normal 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
75
stanza/message_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
42
stanza/msg_chat_markers.go
Normal file
42
stanza/msg_chat_markers.go
Normal 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
45
stanza/msg_chat_state.go
Normal 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
22
stanza/msg_html.go
Normal 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
42
stanza/msg_html_test.go
Normal 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
21
stanza/msg_oob.go
Normal 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
29
stanza/msg_receipts.go
Normal 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{})
|
||||
}
|
||||
40
stanza/msg_receipts_test.go
Normal file
40
stanza/msg_receipts_test.go
Normal 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
51
stanza/node.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package stanza
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// ============================================================================
|
||||
// Generic / unknown content
|
||||
|
||||
// Node is a generic structure to represent XML data. It is used to parse
|
||||
// unreferenced or custom stanza payload.
|
||||
type Node struct {
|
||||
XMLName xml.Name
|
||||
Attrs []xml.Attr `xml:"-"`
|
||||
Content string `xml:",innerxml"`
|
||||
Nodes []Node `xml:",any"`
|
||||
}
|
||||
|
||||
func (n *Node) Namespace() string {
|
||||
return n.XMLName.Space
|
||||
}
|
||||
|
||||
// Attr represents generic XML attributes, as used on the generic XML Node
|
||||
// representation.
|
||||
type Attr struct {
|
||||
K string
|
||||
V string
|
||||
}
|
||||
|
||||
// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
|
||||
// transform generic XML content into hierarchical Node structure.
|
||||
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
|
||||
for _, attr := range start.Attr {
|
||||
// Do not repeat xmlns, it is already in XMLName
|
||||
if attr.Name.Local != "xmlns" {
|
||||
n.Attrs = append(n.Attrs, attr)
|
||||
}
|
||||
}
|
||||
type node Node
|
||||
return d.DecodeElement((*node)(n), &start)
|
||||
}
|
||||
|
||||
// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a
|
||||
// Node structure to XML.
|
||||
func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
|
||||
start.Attr = n.Attrs
|
||||
start.Name = n.XMLName
|
||||
|
||||
err = e.EncodeToken(start)
|
||||
e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
11
stanza/ns.go
Normal file
11
stanza/ns.go
Normal 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
18
stanza/packet.go
Normal 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
25
stanza/packet_enum.go
Normal 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
151
stanza/parser.go
Normal 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
27
stanza/pep.go
Normal 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
29
stanza/pres_muc.go
Normal 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
58
stanza/pres_muc_test.go
Normal 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
139
stanza/presence.go
Normal 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
12
stanza/presence_enum.go
Normal 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
62
stanza/presence_test.go
Normal 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
40
stanza/pubsub.go
Normal 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
119
stanza/registry.go
Normal 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 ®istry{
|
||||
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
47
stanza/registry_test.go
Normal 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
17
stanza/starttls.go
Normal 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
166
stanza/stream.go
Normal 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
29
stanza/xmpp_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user