From d33490cdc03df510aa2757686faa355e1d25fd81 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Tue, 16 Jan 2018 22:33:21 +0100 Subject: [PATCH 1/5] Work-in-progress on dynamic IQ parsing --- auth.go | 4 +- client_test.go | 2 +- iq.go | 133 +++++++++++++++++++++++++++++++++++++------------ iq_test.go | 40 ++++++++++++++- session.go | 2 +- stream.go | 2 +- 6 files changed, 144 insertions(+), 39 deletions(-) diff --git a/auth.go b/auth.go index 3b4b754..9474817 100644 --- a/auth.go +++ b/auth.go @@ -104,13 +104,13 @@ type auth struct { Value string `xml:",innerxml"` } -type bindBind struct { +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 (*bindBind) IsIQPayload() { +func (*BindBind) IsIQPayload() { } // Session is obsolete in RFC 6121. diff --git a/client_test.go b/client_test.go index 381d5af..5ed938b 100644 --- a/client_test.go +++ b/client_test.go @@ -172,7 +172,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { // TODO Check all elements switch iq.Payload[0].(type) { - case *bindBind: + case *BindBind: result := ` %s diff --git a/iq.go b/iq.go index 1b174f6..695df48 100644 --- a/iq.go +++ b/iq.go @@ -4,29 +4,59 @@ import ( "encoding/xml" "fmt" + "reflect" + "fluux.io/xmpp/iot" ) /* 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{ - XMLName: xml.Name{ - Space: "", - Local: "", - }, - PacketAttrs: xmpp.PacketAttrs{ - Id: "", - From: "", - To: "", - Type: "", - Lang: "", - }, - Payload: nil, - RawXML: "", - } + payload := Node{ + Ns: "http://jabber.org/protocol/disco#info", + Tag: "identity", + Attrs: map[string]string{ + "category":"gateway", + "type": "skype", + "name": "Test Gateway", + }, + Nodes: []Node{}, + } + + AddPayload(Ns, Tag, Attrs) + +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 */ @@ -75,6 +105,7 @@ func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) { // UnmarshalXML implements custom parsing for IQs func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { iq.XMLName = start.Name + fmt.Println("IQ Name", iq.XMLName) // Extract IQ attributes for _, attr := range start.Attr { if attr.Name.Local == "id" { @@ -95,34 +126,38 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } // decode inner elements + level := 0 for { t, err := d.Token() if err != nil { return err } - var p IQPayload switch tt := t.(type) { case xml.StartElement: - switch tt.Name.Space + " " + tt.Name.Local { - case "urn:ietf:params:xml:ns:xmpp-bind bind": - p = new(bindBind) - case "urn:xmpp:iot:control set": - p = new(iot.ControlSet) - default: - p = new(Node) - } - if p != nil { - err = d.DecodeElement(p, &tt) - if err != nil { - return err + level++ + if level <= 1 { + var elt interface{} + payloadType := tt.Name.Space + " " + tt.Name.Local + if payloadType := typeRegistry[payloadType]; payloadType != nil { + val := reflect.New(payloadType) + elt = val.Interface() + } else { + elt = new(Node) + } + + if iqPl, ok := elt.(IQPayload); ok { + err = d.DecodeElement(elt, &tt) + if err != nil { + return err + } + iq.Payload = append(iq.Payload, iqPl) // []IQPayload{iqPl} } - iq.Payload = []IQPayload{p} - p = nil } case xml.EndElement: + level-- if tt == start.End() { return nil } @@ -165,10 +200,15 @@ type Node struct { Nodes []Node `xml:",any"` } +type Attr struct { + K string + V string +} + 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 { - // Do not repeat xmlns + // Do not repeat xmlns, it is already in XMLName if attr.Name.Local != "xmlns" { n.Attrs = append(n.Attrs, attr) } @@ -187,3 +227,30 @@ func (n *Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { } func (*Node) IsIQPayload() {} + +// ============================================================================ +// Disco + +type DiscoInfo struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` + Identity Identity +} + +func (*DiscoInfo) IsIQPayload() {} + +type Identity struct { + XMLName xml.Name `xml:"identity"` + Name string `xml:"name,attr"` + Category string `xml:"category,attr"` + Type string `xml:"type,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{}) +} diff --git a/iq_test.go b/iq_test.go index ab85370..1a88d72 100644 --- a/iq_test.go +++ b/iq_test.go @@ -2,6 +2,7 @@ package xmpp // import "fluux.io/xmpp" import ( "encoding/xml" + "fmt" "reflect" "testing" ) @@ -42,7 +43,7 @@ func TestGenerateIq(t *testing.T) { }, Attrs: []xml.Attr{ {Name: xml.Name{Local: "category"}, Value: "gateway"}, - {Name: xml.Name{Local: "type"}, Value: "skype"}, + {Name: xml.Name{Local: "type"}, Value: "mqtt"}, {Name: xml.Name{Local: "name"}, Value: "Test Gateway"}, }, Nodes: nil, @@ -54,6 +55,43 @@ func TestGenerateIq(t *testing.T) { t.Errorf("cannot marshal xml structure") } + fmt.Printf("XML Struct: %s\n", data) + + var parsedIQ = new(IQ) + if err = xml.Unmarshal(data, parsedIQ); err != nil { + t.Errorf("Unmarshal(%s) returned error", data) + } + + if !reflect.DeepEqual(parsedIQ.Payload[0], iq.Payload[0]) { + t.Errorf("expecting result %+v = %+v", parsedIQ.Payload[0], iq.Payload[0]) + } + + fmt.Println("ParsedIQ", parsedIQ) +} + +func TestGenerateIqNew(t *testing.T) { + iq := NewIQ("get", "admin@localhost", "test@localhost", "1", "en") + payload := DiscoInfo{ + XMLName: xml.Name{ + Space: "http://jabber.org/protocol/disco#info", + Local: "query", + }, + Identity: Identity{ + XMLName: xml.Name{ + Space: "http://jabber.org/protocol/disco#info", + Local: "identity", + }, + Name: "Test Gateway", + Category: "gateway", + Type: "mqtt", + }, + } + iq.AddPayload(&payload) + data, err := xml.Marshal(iq) + if err != nil { + t.Errorf("cannot marshal xml structure") + } + var parsedIQ = new(IQ) if err = xml.Unmarshal(data, parsedIQ); err != nil { t.Errorf("Unmarshal(%s) returned error", data) diff --git a/session.go b/session.go index c4afa58..1b33e14 100644 --- a/session.go +++ b/session.go @@ -165,7 +165,7 @@ func (s *Session) bind(o Options) { // TODO Check all elements switch payload := iq.Payload[0].(type) { - case *bindBind: + case *BindBind: s.BindJid = payload.Jid // our local id (with possibly randomly generated resource default: s.err = errors.New("iq bind result missing") diff --git a/stream.go b/stream.go index 3c6afd6..0e29bda 100644 --- a/stream.go +++ b/stream.go @@ -12,7 +12,7 @@ type streamFeatures struct { StartTLS tlsStartTLS Caps Caps Mechanisms saslMechanisms - Bind bindBind + Bind BindBind Session sessionSession Any []xml.Name `xml:",any"` } From 993ca630f7a4edd7fdeb70b211690b2416e6893d Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Wed, 17 Jan 2018 18:47:34 +0100 Subject: [PATCH 2/5] Test and code refactor --- client.go | 1 + cmd/xmpp_component/xmpp_component.go | 31 +++++----- component.go | 14 ++++- iq.go | 20 ++++--- iq_test.go | 84 +++++++++++----------------- 5 files changed, 75 insertions(+), 75 deletions(-) diff --git a/client.go b/client.go index defa642..08ee804 100644 --- a/client.go +++ b/client.go @@ -127,6 +127,7 @@ func (c *Client) Recv() <-chan interface{} { } // Send sends message text. +// TODO Move to Go XML Marshaller func (c *Client) Send(packet string) error { fmt.Fprintf(c.Session.socketProxy, packet) // TODO handle errors return nil diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index e2126d1..01a222a 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -58,20 +58,21 @@ func (c MyComponent) processIQ(iqType, id, from string, inner *xmpp.Node) { switch inner.XMLName.Space + " " + iqType { case NSDiscoInfo + " get": fmt.Println("Send Disco Info") - result := fmt.Sprintf(` - - - - - -`, c.xmpp.Host, from, id, c.Category, c.Type, c.Name) - c.xmpp.Send(result) + + iq := xmpp.NewIQ("result", "admin@localhost", "test@localhost", "1", "en") + payload := xmpp.DiscoInfo{ + Identity: xmpp.Identity{ + Name: "Test Gateway", + Category: "gateway", + Type: "mqtt", + }, + Features: []xmpp.Feature{ + {Var: "http://jabber.org/protocol/disco#info"}, + {Var: "http://jabber.org/protocol/disco#item"}, + }, + } + iq.AddPayload(&payload) + c.xmpp.Send(iq) default: iqErr := fmt.Sprintf(` `, c.xmpp.Host, from, id) - c.xmpp.Send(iqErr) + c.xmpp.SendOld(iqErr) // FIXME Remove that method } } diff --git a/component.go b/component.go index aa5d8d0..750bfc9 100644 --- a/component.go +++ b/component.go @@ -94,7 +94,19 @@ func (c *Component) ReadPacket() (Packet, error) { return next(c.decoder) } -func (c *Component) Send(packet string) error { +func (c *Component) Send(packet Packet) error { + 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 nil +} + +func (c *Component) SendOld(packet string) error { if _, err := fmt.Fprintf(c.conn, packet); err != nil { return errors.New("cannot send packet " + err.Error()) } diff --git a/iq.go b/iq.go index 695df48..69e04c4 100644 --- a/iq.go +++ b/iq.go @@ -152,7 +152,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if err != nil { return err } - iq.Payload = append(iq.Payload, iqPl) // []IQPayload{iqPl} + iq.Payload = append(iq.Payload, iqPl) } } @@ -232,17 +232,23 @@ func (*Node) IsIQPayload() {} // Disco type DiscoInfo struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` - Identity Identity + 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"` - Name string `xml:"name,attr"` - Category string `xml:"category,attr"` - Type string `xml:"type,attr"` + 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"` } // ============================================================================ diff --git a/iq_test.go b/iq_test.go index 1a88d72..2ee86b4 100644 --- a/iq_test.go +++ b/iq_test.go @@ -2,9 +2,10 @@ package xmpp // import "fluux.io/xmpp" import ( "encoding/xml" - "fmt" "reflect" "testing" + + "github.com/google/go-cmp/cmp" ) func TestUnmarshalIqs(t *testing.T) { @@ -30,63 +31,20 @@ func TestUnmarshalIqs(t *testing.T) { } func TestGenerateIq(t *testing.T) { - iq := NewIQ("get", "admin@localhost", "test@localhost", "1", "en") - payload := Node{ - XMLName: xml.Name{ - Space: "http://jabber.org/protocol/disco#info", - Local: "query", - }, - 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: "mqtt"}, - {Name: xml.Name{Local: "name"}, Value: "Test Gateway"}, - }, - Nodes: nil, - }}, - } - iq.AddPayload(&payload) - data, err := xml.Marshal(iq) - if err != nil { - t.Errorf("cannot marshal xml structure") - } - - fmt.Printf("XML Struct: %s\n", data) - - var parsedIQ = new(IQ) - if err = xml.Unmarshal(data, parsedIQ); err != nil { - t.Errorf("Unmarshal(%s) returned error", data) - } - - if !reflect.DeepEqual(parsedIQ.Payload[0], iq.Payload[0]) { - t.Errorf("expecting result %+v = %+v", parsedIQ.Payload[0], iq.Payload[0]) - } - - fmt.Println("ParsedIQ", parsedIQ) -} - -func TestGenerateIqNew(t *testing.T) { - iq := NewIQ("get", "admin@localhost", "test@localhost", "1", "en") + iq := NewIQ("result", "admin@localhost", "test@localhost", "1", "en") payload := DiscoInfo{ - XMLName: xml.Name{ - Space: "http://jabber.org/protocol/disco#info", - Local: "query", - }, Identity: Identity{ - XMLName: xml.Name{ - Space: "http://jabber.org/protocol/disco#info", - Local: "identity", - }, Name: "Test Gateway", Category: "gateway", Type: "mqtt", }, + Features: []Feature{ + {Var: "http://jabber.org/protocol/disco#info"}, + {Var: "http://jabber.org/protocol/disco#item"}, + }, } iq.AddPayload(&payload) + data, err := xml.Marshal(iq) if err != nil { t.Errorf("cannot marshal xml structure") @@ -97,7 +55,29 @@ func TestGenerateIqNew(t *testing.T) { t.Errorf("Unmarshal(%s) returned error", data) } - if !reflect.DeepEqual(parsedIQ.Payload[0], iq.Payload[0]) { - t.Errorf("expecting result %+v = %+v", parsedIQ.Payload[0], iq.Payload[0]) + if !xmlEqual(parsedIQ.Payload, iq.Payload) { + t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload)) } } + +// 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) +} From bbfafbb32c4c13ee8a273be1fc9ab0bd0180eed6 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 18 Jan 2018 17:03:54 +0100 Subject: [PATCH 3/5] Clean-up / Consistency --- iq.go | 2 +- iq_test.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/iq.go b/iq.go index 69e04c4..3ef76b3 100644 --- a/iq.go +++ b/iq.go @@ -105,7 +105,7 @@ func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) { // UnmarshalXML implements custom parsing for IQs func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { iq.XMLName = start.Name - fmt.Println("IQ Name", iq.XMLName) + // Extract IQ attributes for _, attr := range start.Attr { if attr.Name.Local == "id" { diff --git a/iq_test.go b/iq_test.go index 2ee86b4..d833e5b 100644 --- a/iq_test.go +++ b/iq_test.go @@ -2,7 +2,6 @@ package xmpp // import "fluux.io/xmpp" import ( "encoding/xml" - "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -19,14 +18,16 @@ func TestUnmarshalIqs(t *testing.T) { } for _, test := range tests { - var parsedIQ = new(IQ) - err := xml.Unmarshal([]byte(test.iqString), parsedIQ) + parsedIQ := IQ{} + err := xml.Unmarshal([]byte(test.iqString), &parsedIQ) if err != nil { 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)) } + } } @@ -50,8 +51,8 @@ func TestGenerateIq(t *testing.T) { t.Errorf("cannot marshal xml structure") } - var parsedIQ = new(IQ) - if err = xml.Unmarshal(data, parsedIQ); err != nil { + parsedIQ := IQ{} + if err = xml.Unmarshal(data, &parsedIQ); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } From 8470c01c09556223a962f90e86f17a15e6fd5210 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 20 Jan 2018 18:09:13 +0100 Subject: [PATCH 4/5] Implement error parsing --- iq.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++--- iq_test.go | 24 +++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/iq.go b/iq.go index 3ef76b3..68dd2b0 100644 --- a/iq.go +++ b/iq.go @@ -6,6 +6,8 @@ import ( "reflect" + "strconv" + "fluux.io/xmpp/iot" ) @@ -60,6 +62,92 @@ TODO support ability to put Raw payload */ +// ============================================================================ +// XMPP Errors + +type Err struct { + XMLName xml.Name `xml:"error"` + Reason string + Code int `xml:"code,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text"` +} + +// 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 + 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 + 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 @@ -68,7 +156,7 @@ type IQ struct { // Info/Query PacketAttrs Payload []IQPayload `xml:",omitempty"` RawXML string `xml:",innerxml"` - // Error clientError + Error Err `xml:"error,omitempty"` } func NewIQ(iqtype, from, to, id, lang string) IQ { @@ -196,8 +284,8 @@ type IQPayload interface { type Node struct { XMLName xml.Name Attrs []xml.Attr `xml:"-"` - // Content []byte `xml:",innerxml"` - Nodes []Node `xml:",any"` + Content string `xml:",innerxml"` + Nodes []Node `xml:",any"` } type Attr struct { @@ -217,7 +305,7 @@ func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 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.Name = n.XMLName @@ -231,6 +319,10 @@ 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"` diff --git a/iq_test.go b/iq_test.go index d833e5b..aaf3343 100644 --- a/iq_test.go +++ b/iq_test.go @@ -61,6 +61,30 @@ func TestGenerateIq(t *testing.T) { } } +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. From fb8d050a001f75288cfe6b91e54e0ef186639240 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 20 Jan 2018 18:56:07 +0100 Subject: [PATCH 5/5] IQ error management --- cmd/xmpp_component/xmpp_component.go | 73 +++++++++++----------------- component.go | 7 --- iq.go | 38 +++++++++++---- 3 files changed, 56 insertions(+), 62 deletions(-) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index 01a222a..ec400e8 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -1,7 +1,6 @@ package main import ( - "encoding/xml" "fmt" "fluux.io/xmpp" @@ -22,28 +21,28 @@ func main() { switch p := packet.(type) { case xmpp.IQ: switch inner := p.Payload[0].(type) { - case *xmpp.Node: - fmt.Printf("%q\n", inner) - - data, err := xml.Marshal(inner) - if err != nil { - fmt.Println("cannot marshall payload") + case *xmpp.DiscoInfo: + fmt.Println("Disco Info") + if p.Type == "get" { + DiscoResult(component, p.From, p.To, p.Id) } - fmt.Println("data=", string(data)) - component.processIQ(p.Type, p.Id, p.From, inner) + 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: - fmt.Println("Packet unhandled packet:", packet) + fmt.Println("ignoring packet:", packet) } } } -const ( - NSDiscoInfo = "http://jabber.org/protocol/disco#info" -) - type MyComponent struct { Name string // Typical categories and types: https://xmpp.org/registrar/disco-categories.html @@ -53,35 +52,19 @@ type MyComponent struct { xmpp *xmpp.Component } -func (c MyComponent) processIQ(iqType, id, from string, inner *xmpp.Node) { - fmt.Println("Node:", inner.XMLName.Space, inner.XMLName.Local) - switch inner.XMLName.Space + " " + iqType { - case NSDiscoInfo + " get": - fmt.Println("Send Disco Info") - - iq := xmpp.NewIQ("result", "admin@localhost", "test@localhost", "1", "en") - payload := xmpp.DiscoInfo{ - Identity: xmpp.Identity{ - Name: "Test Gateway", - Category: "gateway", - Type: "mqtt", - }, - Features: []xmpp.Feature{ - {Var: "http://jabber.org/protocol/disco#info"}, - {Var: "http://jabber.org/protocol/disco#item"}, - }, - } - iq.AddPayload(&payload) - c.xmpp.Send(iq) - default: - iqErr := fmt.Sprintf(` - - - -`, c.xmpp.Host, from, id) - c.xmpp.SendOld(iqErr) // FIXME Remove that method +func DiscoResult(c MyComponent, from, to, id string) { + iq := xmpp.NewIQ("result", to, from, id, "en") + payload := xmpp.DiscoInfo{ + Identity: xmpp.Identity{ + Name: c.Name, + Category: c.Category, + Type: c.Type, + }, + Features: []xmpp.Feature{ + {Var: "http://jabber.org/protocol/disco#info"}, + {Var: "http://jabber.org/protocol/disco#item"}, + }, } + iq.AddPayload(&payload) + c.xmpp.Send(iq) } diff --git a/component.go b/component.go index 750bfc9..702c7d7 100644 --- a/component.go +++ b/component.go @@ -106,13 +106,6 @@ func (c *Component) Send(packet Packet) error { return nil } -func (c *Component) SendOld(packet string) error { - if _, err := fmt.Fprintf(c.conn, packet); err != nil { - return errors.New("cannot send packet " + err.Error()) - } - return nil -} - // ============================================================================ // Handshake Packet diff --git a/iq.go b/iq.go index 68dd2b0..2ac7d0b 100644 --- a/iq.go +++ b/iq.go @@ -67,12 +67,14 @@ TODO support ability to put Raw payload type Err struct { XMLName xml.Name `xml:"error"` + Code int `xml:"code,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` Reason string - Code int `xml:"code,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text"` + 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 @@ -135,15 +137,19 @@ func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { // Subtags // 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}) + 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 - 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}) + 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}) } @@ -176,6 +182,18 @@ func (iq *IQ) AddPayload(payload IQPayload) { 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 { return "iq" }