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.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/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/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index e2126d1..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,34 +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") - result := fmt.Sprintf(` - - - - - -`, c.xmpp.Host, from, id, c.Category, c.Type, c.Name) - c.xmpp.Send(result) - default: - iqErr := fmt.Sprintf(` - - - -`, c.xmpp.Host, from, id) - c.xmpp.Send(iqErr) +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 aa5d8d0..702c7d7 100644 --- a/component.go +++ b/component.go @@ -94,8 +94,13 @@ func (c *Component) ReadPacket() (Packet, error) { return next(c.decoder) } -func (c *Component) Send(packet string) error { - if _, err := fmt.Fprintf(c.conn, packet); err != nil { +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 diff --git a/iq.go b/iq.go index 1b174f6..2ac7d0b 100644 --- a/iq.go +++ b/iq.go @@ -4,32 +4,156 @@ import ( "encoding/xml" "fmt" + "reflect" + + "strconv" + "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 */ +// ============================================================================ +// 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 @@ -38,7 +162,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 { @@ -58,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" } @@ -75,6 +211,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 + // Extract IQ attributes for _, attr := range start.Attr { if attr.Name.Local == "id" { @@ -95,34 +232,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) } - iq.Payload = []IQPayload{p} - p = nil } case xml.EndElement: + level-- if tt == start.End() { return nil } @@ -161,14 +302,19 @@ 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 { + 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) } @@ -177,7 +323,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 @@ -187,3 +333,40 @@ func (n *Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { } 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{}) +} diff --git a/iq_test.go b/iq_test.go index ab85370..aaf3343 100644 --- a/iq_test.go +++ b/iq_test.go @@ -2,8 +2,9 @@ package xmpp // import "fluux.io/xmpp" import ( "encoding/xml" - "reflect" "testing" + + "github.com/google/go-cmp/cmp" ) func TestUnmarshalIqs(t *testing.T) { @@ -17,49 +18,91 @@ 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)) } + } } 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", + iq := NewIQ("result", "admin@localhost", "test@localhost", "1", "en") + payload := DiscoInfo{ + Identity: Identity{ + Name: "Test Gateway", + 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) + 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 { + parsedIQ := 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]) + 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)) + } +} + +// 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) +} 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"` }