Merge pull request #6 "Error parsing / generation"

This commit is contained in:
Mickaël Rémond 2018-01-20 18:58:08 +01:00 committed by GitHub
commit adb14260f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 114 deletions

View file

@ -104,13 +104,13 @@ type auth struct {
Value string `xml:",innerxml"` Value string `xml:",innerxml"`
} }
type bindBind struct { type BindBind struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
Resource string `xml:"resource,omitempty"` Resource string `xml:"resource,omitempty"`
Jid string `xml:"jid,omitempty"` Jid string `xml:"jid,omitempty"`
} }
func (*bindBind) IsIQPayload() { func (*BindBind) IsIQPayload() {
} }
// Session is obsolete in RFC 6121. // Session is obsolete in RFC 6121.

View file

@ -127,6 +127,7 @@ func (c *Client) Recv() <-chan interface{} {
} }
// Send sends message text. // Send sends message text.
// TODO Move to Go XML Marshaller
func (c *Client) Send(packet string) error { func (c *Client) Send(packet string) error {
fmt.Fprintf(c.Session.socketProxy, packet) // TODO handle errors fmt.Fprintf(c.Session.socketProxy, packet) // TODO handle errors
return nil return nil

View file

@ -172,7 +172,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) {
// TODO Check all elements // TODO Check all elements
switch iq.Payload[0].(type) { switch iq.Payload[0].(type) {
case *bindBind: case *BindBind:
result := `<iq id='%s' type='result'> result := `<iq id='%s' type='result'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>%s</jid> <jid>%s</jid>

View file

@ -1,7 +1,6 @@
package main package main
import ( import (
"encoding/xml"
"fmt" "fmt"
"fluux.io/xmpp" "fluux.io/xmpp"
@ -22,28 +21,28 @@ func main() {
switch p := packet.(type) { switch p := packet.(type) {
case xmpp.IQ: case xmpp.IQ:
switch inner := p.Payload[0].(type) { switch inner := p.Payload[0].(type) {
case *xmpp.Node: case *xmpp.DiscoInfo:
fmt.Printf("%q\n", inner) fmt.Println("Disco Info")
if p.Type == "get" {
data, err := xml.Marshal(inner) DiscoResult(component, p.From, p.To, p.Id)
if err != nil {
fmt.Println("cannot marshall payload")
} }
fmt.Println("data=", string(data))
component.processIQ(p.Type, p.Id, p.From, inner)
default: default:
fmt.Println("default") fmt.Println("ignoring iq packet", inner)
xerror := xmpp.Err{
Code: 501,
Reason: "feature-not-implemented",
Type: "cancel",
}
reply := p.MakeError(xerror)
component.xmpp.Send(&reply)
} }
default: default:
fmt.Println("Packet unhandled packet:", packet) fmt.Println("ignoring packet:", packet)
} }
} }
} }
const (
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
)
type MyComponent struct { type MyComponent struct {
Name string Name string
// Typical categories and types: https://xmpp.org/registrar/disco-categories.html // Typical categories and types: https://xmpp.org/registrar/disco-categories.html
@ -53,34 +52,19 @@ type MyComponent struct {
xmpp *xmpp.Component xmpp *xmpp.Component
} }
func (c MyComponent) processIQ(iqType, id, from string, inner *xmpp.Node) { func DiscoResult(c MyComponent, from, to, id string) {
fmt.Println("Node:", inner.XMLName.Space, inner.XMLName.Local) iq := xmpp.NewIQ("result", to, from, id, "en")
switch inner.XMLName.Space + " " + iqType { payload := xmpp.DiscoInfo{
case NSDiscoInfo + " get": Identity: xmpp.Identity{
fmt.Println("Send Disco Info") Name: c.Name,
result := fmt.Sprintf(`<iq type='result' Category: c.Category,
from='%s' Type: c.Type,
to='%s' },
id='%s'> Features: []xmpp.Feature{
<query xmlns='http://jabber.org/protocol/disco#info'> {Var: "http://jabber.org/protocol/disco#info"},
<identity {Var: "http://jabber.org/protocol/disco#item"},
category='%s' },
type='%s'
name='%s'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<feature var='http://jabber.org/protocol/disco#items'/>
</query>
</iq>`, c.xmpp.Host, from, id, c.Category, c.Type, c.Name)
c.xmpp.Send(result)
default:
iqErr := fmt.Sprintf(`<iq type='error'
from='%s'
to='%s'
id='%s'>
<error type="cancel" code="501">
<feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
</error>
</iq>`, c.xmpp.Host, from, id)
c.xmpp.Send(iqErr)
} }
iq.AddPayload(&payload)
c.xmpp.Send(iq)
} }

View file

@ -94,8 +94,13 @@ func (c *Component) ReadPacket() (Packet, error) {
return next(c.decoder) return next(c.decoder)
} }
func (c *Component) Send(packet string) error { func (c *Component) Send(packet Packet) error {
if _, err := fmt.Fprintf(c.conn, packet); err != nil { data, err := xml.Marshal(packet)
if err != nil {
return errors.New("cannot marshal packet " + err.Error())
}
if _, err := fmt.Fprintf(c.conn, string(data)); err != nil {
return errors.New("cannot send packet " + err.Error()) return errors.New("cannot send packet " + err.Error())
} }
return nil return nil

257
iq.go
View file

@ -4,32 +4,156 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"reflect"
"strconv"
"fluux.io/xmpp/iot" "fluux.io/xmpp/iot"
) )
/* /*
TODO I would like to be able to write TODO I would like to be able to write
newIQ(Id, From, To, Type, Lang).AddPayload(IQPayload) NewIQ(Id, From, To, Type, Lang).AddPayload(IQPayload)
Payload would be:
xmpp.IQ{ payload := Node{
XMLName: xml.Name{ Ns: "http://jabber.org/protocol/disco#info",
Space: "", Tag: "identity",
Local: "", Attrs: map[string]string{
}, "category":"gateway",
PacketAttrs: xmpp.PacketAttrs{ "type": "skype",
Id: "", "name": "Test Gateway",
From: "", },
To: "", Nodes: []Node{},
Type: "", }
Lang: "",
}, AddPayload(Ns, Tag, Attrs)
Payload: nil,
RawXML: "", ex:
}
NewIQ("get", "test@localhost", "admin@localhost", "en")
.AddPayload("http://jabber.org/protocol/disco#info",
"identity",
map[string]string{
"category":"gateway",
"type": "skype",
"name": "Test Gateway",
})
NewNode(Ns, Tag, Attrs)
NewNodeWithChildren(Ns, Tag, Attrs, Nodes)
Attr {
K string
V string
}
xmpp.Elt.DiscoInfo("identity", "gateway", "skype", "Test Gateway")
xmppElt.DiscoInfo.identity("
import xmpp/node/discoinfo
discoinfo.Identity("gateway", "skype", "Test Gateway")
[]Attr{{"category", "gateway"}
TODO support ability to put Raw payload
*/ */
// ============================================================================
// XMPP Errors
type Err struct {
XMLName xml.Name `xml:"error"`
Code int `xml:"code,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Reason string
Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
}
func (*Err) IsIQPayload() {}
// 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 = attr.Value
}
if attr.Name.Local == "code" {
if code, err := strconv.Atoi(attr.Value); err == nil {
x.Code = code
}
}
}
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) {
code := xml.Attr{
Name: xml.Name{Local: "code"},
Value: strconv.Itoa(x.Code),
}
typ := xml.Attr{
Name: xml.Name{Local: "type"},
Value: x.Type,
}
start.Name = xml.Name{Local: "error"}
start.Attr = append(start.Attr, code, 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})
}
// ============================================================================ // ============================================================================
// IQ Packet // IQ Packet
@ -38,7 +162,7 @@ type IQ struct { // Info/Query
PacketAttrs PacketAttrs
Payload []IQPayload `xml:",omitempty"` Payload []IQPayload `xml:",omitempty"`
RawXML string `xml:",innerxml"` RawXML string `xml:",innerxml"`
// Error clientError Error Err `xml:"error,omitempty"`
} }
func NewIQ(iqtype, from, to, id, lang string) IQ { func NewIQ(iqtype, from, to, id, lang string) IQ {
@ -58,6 +182,18 @@ func (iq *IQ) AddPayload(payload IQPayload) {
iq.Payload = append(iq.Payload, payload) iq.Payload = append(iq.Payload, payload)
} }
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 { func (IQ) Name() string {
return "iq" return "iq"
} }
@ -75,6 +211,7 @@ func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) {
// UnmarshalXML implements custom parsing for IQs // UnmarshalXML implements custom parsing for IQs
func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.XMLName = start.Name iq.XMLName = start.Name
// Extract IQ attributes // Extract IQ attributes
for _, attr := range start.Attr { for _, attr := range start.Attr {
if attr.Name.Local == "id" { if attr.Name.Local == "id" {
@ -95,34 +232,38 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
} }
// decode inner elements // decode inner elements
level := 0
for { for {
t, err := d.Token() t, err := d.Token()
if err != nil { if err != nil {
return err return err
} }
var p IQPayload
switch tt := t.(type) { switch tt := t.(type) {
case xml.StartElement: case xml.StartElement:
switch tt.Name.Space + " " + tt.Name.Local { level++
case "urn:ietf:params:xml:ns:xmpp-bind bind": if level <= 1 {
p = new(bindBind) var elt interface{}
case "urn:xmpp:iot:control set": payloadType := tt.Name.Space + " " + tt.Name.Local
p = new(iot.ControlSet) if payloadType := typeRegistry[payloadType]; payloadType != nil {
default: val := reflect.New(payloadType)
p = new(Node) elt = val.Interface()
} } else {
if p != nil { elt = new(Node)
err = d.DecodeElement(p, &tt) }
if err != nil {
return err if iqPl, ok := elt.(IQPayload); ok {
err = d.DecodeElement(elt, &tt)
if err != nil {
return err
}
iq.Payload = append(iq.Payload, iqPl)
} }
iq.Payload = []IQPayload{p}
p = nil
} }
case xml.EndElement: case xml.EndElement:
level--
if tt == start.End() { if tt == start.End() {
return nil return nil
} }
@ -161,14 +302,19 @@ type IQPayload interface {
type Node struct { type Node struct {
XMLName xml.Name XMLName xml.Name
Attrs []xml.Attr `xml:"-"` Attrs []xml.Attr `xml:"-"`
// Content []byte `xml:",innerxml"` Content string `xml:",innerxml"`
Nodes []Node `xml:",any"` Nodes []Node `xml:",any"`
}
type Attr struct {
K string
V string
} }
func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Assign "n.Attrs = start.Attr", without repeating xmlns in attributes // Assign "n.Attrs = start.Attr", without repeating xmlns in attributes:
for _, attr := range start.Attr { for _, attr := range start.Attr {
// Do not repeat xmlns // Do not repeat xmlns, it is already in XMLName
if attr.Name.Local != "xmlns" { if attr.Name.Local != "xmlns" {
n.Attrs = append(n.Attrs, attr) n.Attrs = append(n.Attrs, attr)
} }
@ -177,7 +323,7 @@ func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return d.DecodeElement((*node)(n), &start) return d.DecodeElement((*node)(n), &start)
} }
func (n *Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
start.Attr = n.Attrs start.Attr = n.Attrs
start.Name = n.XMLName start.Name = n.XMLName
@ -187,3 +333,40 @@ func (n *Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
} }
func (*Node) IsIQPayload() {} func (*Node) IsIQPayload() {}
// ============================================================================
// Disco
const (
NSDiscoInfo = "http://jabber.org/protocol/disco#info"
)
type DiscoInfo struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"`
Identity Identity `xml:"identity"`
Features []Feature `xml:"feature"`
}
func (*DiscoInfo) IsIQPayload() {}
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"`
}
// ============================================================================
var typeRegistry = make(map[string]reflect.Type)
func init() {
typeRegistry["http://jabber.org/protocol/disco#info query"] = reflect.TypeOf(DiscoInfo{})
typeRegistry["urn:ietf:params:xml:ns:xmpp-bind bind"] = reflect.TypeOf(BindBind{})
typeRegistry["urn:xmpp:iot:control set"] = reflect.TypeOf(iot.ControlSet{})
}

View file

@ -2,8 +2,9 @@ package xmpp // import "fluux.io/xmpp"
import ( import (
"encoding/xml" "encoding/xml"
"reflect"
"testing" "testing"
"github.com/google/go-cmp/cmp"
) )
func TestUnmarshalIqs(t *testing.T) { func TestUnmarshalIqs(t *testing.T) {
@ -17,49 +18,91 @@ func TestUnmarshalIqs(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
var parsedIQ = new(IQ) parsedIQ := IQ{}
err := xml.Unmarshal([]byte(test.iqString), parsedIQ) err := xml.Unmarshal([]byte(test.iqString), &parsedIQ)
if err != nil { if err != nil {
t.Errorf("Unmarshal(%s) returned error", test.iqString) t.Errorf("Unmarshal(%s) returned error", test.iqString)
} }
if !reflect.DeepEqual(parsedIQ, &test.parsedIQ) {
t.Errorf("Unmarshal(%s) expecting result %+v = %+v", test.iqString, parsedIQ, &test.parsedIQ) if !xmlEqual(parsedIQ, test.parsedIQ) {
t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ, test.parsedIQ))
} }
} }
} }
func TestGenerateIq(t *testing.T) { func TestGenerateIq(t *testing.T) {
iq := NewIQ("get", "admin@localhost", "test@localhost", "1", "en") iq := NewIQ("result", "admin@localhost", "test@localhost", "1", "en")
payload := Node{ payload := DiscoInfo{
XMLName: xml.Name{ Identity: Identity{
Space: "http://jabber.org/protocol/disco#info", Name: "Test Gateway",
Local: "query", Category: "gateway",
Type: "mqtt",
},
Features: []Feature{
{Var: "http://jabber.org/protocol/disco#info"},
{Var: "http://jabber.org/protocol/disco#item"},
}, },
Nodes: []Node{
{XMLName: xml.Name{
Space: "http://jabber.org/protocol/disco#info",
Local: "identity",
},
Attrs: []xml.Attr{
{Name: xml.Name{Local: "category"}, Value: "gateway"},
{Name: xml.Name{Local: "type"}, Value: "skype"},
{Name: xml.Name{Local: "name"}, Value: "Test Gateway"},
},
Nodes: nil,
}},
} }
iq.AddPayload(&payload) iq.AddPayload(&payload)
data, err := xml.Marshal(iq) data, err := xml.Marshal(iq)
if err != nil { if err != nil {
t.Errorf("cannot marshal xml structure") t.Errorf("cannot marshal xml structure")
} }
var parsedIQ = new(IQ) parsedIQ := IQ{}
if err = xml.Unmarshal(data, parsedIQ); err != nil { if err = xml.Unmarshal(data, &parsedIQ); err != nil {
t.Errorf("Unmarshal(%s) returned error", data) t.Errorf("Unmarshal(%s) returned error", data)
} }
if !reflect.DeepEqual(parsedIQ.Payload[0], iq.Payload[0]) { if !xmlEqual(parsedIQ.Payload, iq.Payload) {
t.Errorf("expecting result %+v = %+v", parsedIQ.Payload[0], iq.Payload[0]) 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))
}
}
// 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)
}

View file

@ -165,7 +165,7 @@ func (s *Session) bind(o Options) {
// TODO Check all elements // TODO Check all elements
switch payload := iq.Payload[0].(type) { switch payload := iq.Payload[0].(type) {
case *bindBind: case *BindBind:
s.BindJid = payload.Jid // our local id (with possibly randomly generated resource s.BindJid = payload.Jid // our local id (with possibly randomly generated resource
default: default:
s.err = errors.New("iq bind result missing") s.err = errors.New("iq bind result missing")

View file

@ -12,7 +12,7 @@ type streamFeatures struct {
StartTLS tlsStartTLS StartTLS tlsStartTLS
Caps Caps Caps Caps
Mechanisms saslMechanisms Mechanisms saslMechanisms
Bind bindBind Bind BindBind
Session sessionSession Session sessionSession
Any []xml.Name `xml:",any"` Any []xml.Name `xml:",any"`
} }