From ec68a04554acd448e546f0119c059c18ca404aa2 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 11 Jan 2018 22:15:54 +0100 Subject: [PATCH 01/11] Component skeleton --- cmd/xmpp_component/xmpp_component.go | 7 ++++++ component.go | 37 ++++++++++++++++++++++++++++ ns.go | 13 +++++----- 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 cmd/xmpp_component/xmpp_component.go create mode 100644 component.go diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go new file mode 100644 index 0000000..65c0e4b --- /dev/null +++ b/cmd/xmpp_component/xmpp_component.go @@ -0,0 +1,7 @@ +package main + +import "fluux.io/xmpp" + +func main() { + xmpp.Open("test") +} diff --git a/component.go b/component.go new file mode 100644 index 0000000..d457d88 --- /dev/null +++ b/component.go @@ -0,0 +1,37 @@ +package xmpp + +import ( + "fmt" + "net" + "time" +) + +const componentStreamOpen = "" + +// Component implements an XMPP extension allowing to extend XMPP server +// using external components. Component specifications are defined +// in XEP-0114, XEP-0355 and XEP-0356. +type Component struct { + // TCP level connection + conn net.Conn +} + +// TODO Helper to prepare connection string +func Open(connStr string) error { + var conn net.Conn + var err error + + if conn, err = net.DialTimeout("tcp", "localhost:8888", time.Duration(5)*time.Second); err != nil { + return err + } + + // TODO send stream open and check for reply + // Send stream open tag + componentHost := "mqtt.localhost" + if _, err := fmt.Fprintf(conn, componentStreamOpen, componentHost, NSComponent, NSStream); err != nil { + fmt.Println("Cannot send stream open.") + return err + } + + return nil +} diff --git a/ns.go b/ns.go index 9b3bdac..9cd162c 100644 --- a/ns.go +++ b/ns.go @@ -1,10 +1,11 @@ package xmpp // import "fluux.io/xmpp" 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" + 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" ) From b31c29a03da97a11436d6e076a99ca9598c28f9d Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 11 Jan 2018 23:00:59 +0100 Subject: [PATCH 02/11] Implements dummy auth + stream error --- cmd/xmpp_component/xmpp_component.go | 2 +- component.go | 45 ++++++++++++++++++++++++++-- parser.go | 7 +++-- stream.go | 5 ++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index 65c0e4b..ea97c1f 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -3,5 +3,5 @@ package main import "fluux.io/xmpp" func main() { - xmpp.Open("test") + xmpp.Open("mqtt.localhost") } diff --git a/component.go b/component.go index d457d88..d5555d2 100644 --- a/component.go +++ b/component.go @@ -1,7 +1,10 @@ package xmpp import ( + "encoding/xml" + "errors" "fmt" + "io" "net" "time" ) @@ -14,24 +17,60 @@ const componentStreamOpen = "") From b21fee420f9451ea37a9e130566a6d890073645c Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Fri, 12 Jan 2018 18:14:41 +0100 Subject: [PATCH 04/11] Code clean-up --- cmd/xmpp_component/xmpp_component.go | 3 +- component.go | 54 ++++++++++++---------------- component_test.go | 2 +- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index ea97c1f..f08dbab 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -3,5 +3,6 @@ package main import "fluux.io/xmpp" func main() { - xmpp.Open("mqtt.localhost") + component := xmpp.Component{Host: "mqtt.localhost", Secret: "mypass"} + component.Connect("localhost:8888") } diff --git a/component.go b/component.go index 64a3e57..55b4c48 100644 --- a/component.go +++ b/component.go @@ -28,12 +28,8 @@ type Component struct { decoder *xml.Decoder } -type Handshake struct { - XMLName xml.Name `xml:"jabber:component:accept handshake"` -} - -// Handshake generates an authentication token based on StreamID and shared secret. -func (c *Component) Handshake(streamId string) string { +// handshake generates an authentication token based on StreamID and shared secret. +func (c *Component) handshake(streamId string) string { // 1. Concatenate the Stream ID received from the server with the shared secret. concatStr := streamId + c.Secret @@ -50,55 +46,51 @@ func (c *Component) Handshake(streamId string) string { } // TODO Helper to prepare connection string -func Open(connStr string) error { - c := Component{Host: connStr, Secret: "mypass"} - +func (c *Component) Connect(connStr string) error { var conn net.Conn var err error - if conn, err = net.DialTimeout("tcp", "localhost:8888", time.Duration(5)*time.Second); err != nil { + if conn, err = net.DialTimeout("tcp", connStr, time.Duration(5)*time.Second); err != nil { return err } c.conn = conn - // TODO send stream open and check for reply - // Send stream open tag - componentHost := connStr // TODO Fix me: Extract componentID + secret - if _, err := fmt.Fprintf(conn, componentStreamOpen, componentHost, NSComponent, NSStream); err != nil { - fmt.Println("cannot send stream open.") - return err + // 1. Send stream open tag + if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Host, NSComponent, NSStream); err != nil { + return errors.New("cannot send stream open " + err.Error()) } c.decoder = xml.NewDecoder(conn) - // Initialize xml decoder and extract streamID from reply + // 2. Initialize xml decoder and extract streamID from reply streamId, err := initDecoder(c.decoder) if err != nil { - fmt.Println("cannot init decoder") - return err + return errors.New("cannot init decoder " + err.Error()) } - fmt.Println("StreamID = ", streamId) - - // Authentication - if _, err := fmt.Fprintf(conn, "%s", c.Handshake(streamId)); err != nil { - fmt.Println("cannot send stream open.") - return err + // 3. Authentication + if _, err := fmt.Fprintf(conn, "%s", c.handshake(streamId)); err != nil { + return errors.New("cannot send handshake " + err.Error()) } - // Next message should be either success or failure. + // 4. Check server response for authentication name, val, err := next(c.decoder) if err != nil { - fmt.Println(err) return err } switch v := val.(type) { case *StreamError: - fmt.Printf("error: %s", v.Error.Local) + return errors.New("handshake failed " + v.Error.Local) case *Handshake: - fmt.Println("Component connected") + return nil default: return errors.New("unexpected packet, got " + name.Local + " in " + name.Space) } - - return nil + panic("unreachable") +} + +// ============================================================================ +// XMPP packets struct + +type Handshake struct { + XMLName xml.Name `xml:"jabber:component:accept handshake"` } diff --git a/component_test.go b/component_test.go index 925222c..76e3849 100644 --- a/component_test.go +++ b/component_test.go @@ -11,7 +11,7 @@ func TestHandshake(t *testing.T) { streamID := "1263952298440005243" expected := "c77e2ef0109fbbc5161e83b51629cd1353495332" - result := c.Handshake(streamID) + result := c.handshake(streamID) if result != expected { t.Errorf("incorrect handshake calculation '%s' != '%s'", result, expected) } From 24ac2c05261011897bcbe6bb3a48fcff44746736 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Fri, 12 Jan 2018 19:08:47 +0100 Subject: [PATCH 05/11] Keeps component connection open --- client.go | 2 +- cmd/xmpp_component/xmpp_component.go | 14 +++++++++++++- component.go | 6 ++++++ parser.go | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 689720e..ec62e37 100644 --- a/client.go +++ b/client.go @@ -128,7 +128,7 @@ func (c *Client) Recv() <-chan interface{} { // Send sends message text. func (c *Client) Send(packet string) error { - fmt.Fprintf(c.Session.socketProxy, packet) + 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 f08dbab..3830352 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -1,8 +1,20 @@ package main -import "fluux.io/xmpp" +import ( + "fmt" + + "fluux.io/xmpp" +) func main() { component := xmpp.Component{Host: "mqtt.localhost", Secret: "mypass"} component.Connect("localhost:8888") + + for { + _, packet, err := component.ReadPacket() + if err != nil { + return + } + fmt.Println("Packet received: ", packet) + } } diff --git a/component.go b/component.go index 55b4c48..4105700 100644 --- a/component.go +++ b/component.go @@ -88,6 +88,12 @@ func (c *Component) Connect(connStr string) error { panic("unreachable") } +// ReadPacket reads next incoming XMPP packet +// TODO use defined interface Packet +func (c *Component) ReadPacket() (xml.Name, interface{}, error) { + return next(c.decoder) +} + // ============================================================================ // XMPP packets struct diff --git a/parser.go b/parser.go index 41b778e..fac1a49 100644 --- a/parser.go +++ b/parser.go @@ -63,6 +63,7 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) { // Scan XML token stream for next element and save into val. // If val == nil, allocate new element based on proto map. // Either way, return val. +// TODO Use an interface to return packets interface xmppDecoder func next(p *xml.Decoder) (xml.Name, interface{}, error) { // Read start element to find out what type we want. se, err := nextStart(p) From d2765aec157ddc782056f5278472d80b3063ac01 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 17:46:10 +0100 Subject: [PATCH 06/11] Refactor namespace handling --- cmd/xmpp_component/xmpp_component.go | 1 + parser.go | 91 +++++++++++++++++++++------- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index 3830352..223e83d 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -13,6 +13,7 @@ func main() { for { _, packet, err := component.ReadPacket() if err != nil { + fmt.Println("read error", err) return } fmt.Println("Packet received: ", packet) diff --git a/parser.go b/parser.go index fac1a49..42b940d 100644 --- a/parser.go +++ b/parser.go @@ -60,9 +60,8 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) { panic("unreachable") } -// Scan XML token stream for next element and save into val. -// If val == nil, allocate new element based on proto map. -// Either way, return val. +// next scans XML token stream for next element and then assign a structure to decode +// that elements. // TODO Use an interface to return packets interface xmppDecoder func next(p *xml.Decoder) (xml.Name, interface{}, error) { // Read start element to find out what type we want. @@ -71,26 +70,28 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { return xml.Name{}, nil, err } - // Put it in an interface and allocate one. + // Put it in an interface and allocate the right structure var nv interface{} - switch se.Name.Space + " " + se.Name.Local { - case NSStream + " error": - nv = &StreamError{} - // TODO: general case = Parse IQ / presence / message => split SASL case - case nsSASL + " success": - nv = &saslSuccess{} - case nsSASL + " failure": - nv = &saslFailure{} - case NSClient + " message": - nv = &ClientMessage{} - case NSClient + " presence": - nv = &ClientPresence{} - case NSClient + " iq": - nv = &ClientIQ{} - case NSComponent + " handshake": - nv = &Handshake{} + // TODO: general case = Parse IQ / presence / message => split SASL Stream and component cases + switch se.Name.Space { + case NSStream: + if nv, err = decodeStream(se); err != nil { + return xml.Name{}, nil, err + } + case nsSASL: + if nv, err = decodeSASL(se); err != nil { + return xml.Name{}, nil, err + } + case NSClient: + if nv, err = decodeClient(se); err != nil { + return xml.Name{}, nil, err + } + case NSComponent: + if nv, err = decodeComponent(se); err != nil { + return xml.Name{}, nil, err + } default: - return xml.Name{}, nil, errors.New("unexpected XMPP message " + + return xml.Name{}, nil, errors.New("unknown namespace " + se.Name.Space + " <" + se.Name.Local + "/>") } @@ -100,3 +101,51 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { } return se.Name, nv, err } + +func decodeStream(se xml.StartElement) (interface{}, error) { + switch se.Name.Local { + case "error": + return &StreamError{}, nil + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeSASL(se xml.StartElement) (interface{}, error) { + switch se.Name.Local { + case "success": + return &saslSuccess{}, nil + case "failure": + return &saslFailure{}, nil + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeClient(se xml.StartElement) (interface{}, error) { + switch se.Name.Local { + case "message": + return &ClientMessage{}, nil + case "presence": + return &ClientPresence{}, nil + case "iq": + return &ClientIQ{}, nil + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} + +func decodeComponent(se xml.StartElement) (interface{}, error) { + switch se.Name.Local { + case "handshake": + return &Handshake{}, nil + case "iq": + return &ClientIQ{}, nil + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } +} From 01063ec284aebbf1a01e75466d059b7435363ffc Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 17:54:07 +0100 Subject: [PATCH 07/11] Refactor attributes name --- cmd/xmpp_echo/xmpp_echo.go | 2 +- cmd/xmpp_jukebox/xmpp_jukebox.go | 2 +- iq.go | 2 +- iq_test.go | 4 ++-- message.go | 2 +- packet.go | 4 ++-- pep/user_tune.go | 8 ++++---- presence.go | 2 +- stream.go | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/xmpp_echo/xmpp_echo.go b/cmd/xmpp_echo/xmpp_echo.go index 69067be..012c713 100644 --- a/cmd/xmpp_echo/xmpp_echo.go +++ b/cmd/xmpp_echo/xmpp_echo.go @@ -33,7 +33,7 @@ func main() { switch packet := packet.(type) { case *xmpp.ClientMessage: fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From) - reply := xmpp.ClientMessage{Packet: xmpp.Packet{To: packet.From}, Body: packet.Body} + reply := xmpp.ClientMessage{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body} client.Send(reply.XMPPFormat()) default: fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet) diff --git a/cmd/xmpp_jukebox/xmpp_jukebox.go b/cmd/xmpp_jukebox/xmpp_jukebox.go index 0591b4b..a1737e6 100644 --- a/cmd/xmpp_jukebox/xmpp_jukebox.go +++ b/cmd/xmpp_jukebox/xmpp_jukebox.go @@ -76,7 +76,7 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientIQ) { playSCURL(p, url) setResponse := new(iot.ControlSetResponse) - reply := xmpp.ClientIQ{Packet: xmpp.Packet{To: packet.From, Type: "result", Id: packet.Id}, Payload: setResponse} + reply := xmpp.ClientIQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: setResponse} client.Send(reply.XMPPFormat()) // TODO add Soundclound artist / title retrieval sendUserTune(client, "Radiohead", "Spectre") diff --git a/iq.go b/iq.go index f737e82..9c02dc1 100644 --- a/iq.go +++ b/iq.go @@ -10,7 +10,7 @@ import ( // info/query type ClientIQ struct { XMLName xml.Name `xml:"jabber:client iq"` - Packet + PacketAttrs Payload IQPayload `xml:",omitempty"` RawXML string `xml:",innerxml"` // TODO We need to support detecting the IQ namespace / Query packet diff --git a/iq_test.go b/iq_test.go index b8859c0..71e9edd 100644 --- a/iq_test.go +++ b/iq_test.go @@ -12,8 +12,8 @@ func TestUnmarshalIqs(t *testing.T) { iqString string parsedIQ ClientIQ }{ - {"", ClientIQ{XMLName: xml.Name{Space: "", Local: "iq"}, Packet: Packet{To: "test@localhost", Type: "set", Id: "1"}}}, - //{"", ClientIQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, Packet: Packet{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}}, + {"", ClientIQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}}, + //{"", ClientIQ{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 { diff --git a/message.go b/message.go index dbcd0ad..5ae14d2 100644 --- a/message.go +++ b/message.go @@ -8,7 +8,7 @@ import ( // XMPP Packet Parsing type ClientMessage struct { XMLName xml.Name `xml:"jabber:client message"` - Packet + PacketAttrs Subject string `xml:"subject,omitempty"` Body string `xml:"body,omitempty"` Thread string `xml:"thread,omitempty"` diff --git a/packet.go b/packet.go index a896440..9e58773 100644 --- a/packet.go +++ b/packet.go @@ -1,7 +1,7 @@ package xmpp // import "fluux.io/xmpp" -// Packet represents the root default structure for an XMPP packet. -type Packet struct { +// PacketAttrs represents the common structure for base XMPP packets. +type PacketAttrs struct { Id string `xml:"id,attr,omitempty"` From string `xml:"from,attr,omitempty"` To string `xml:"to,attr,omitempty"` diff --git a/pep/user_tune.go b/pep/user_tune.go index 846dda0..70b4b47 100644 --- a/pep/user_tune.go +++ b/pep/user_tune.go @@ -7,9 +7,9 @@ import ( ) type iq struct { - XMLName xml.Name `xml:"jabber:client iq"` - C pubSub // c for "contains" - xmpp.Packet // Rename h for "header" ? + XMLName xml.Name `xml:"jabber:client iq"` + C pubSub // c for "contains" + xmpp.PacketAttrs // Rename h for "header" ? } type pubSub struct { @@ -68,7 +68,7 @@ type Tune struct { */ func (t *Tune) XMPPFormat() (s string) { - packet, _ := xml.Marshal(iq{Packet: xmpp.Packet{Id: "tunes", Type: "set"}, C: pubSub{Publish: publish{Node: "http://jabber.org/protocol/tune", Item: item{Tune: *t}}}}) + packet, _ := xml.Marshal(iq{PacketAttrs: xmpp.PacketAttrs{Id: "tunes", Type: "set"}, C: pubSub{Publish: publish{Node: "http://jabber.org/protocol/tune", Item: item{Tune: *t}}}}) return string(packet) } diff --git a/presence.go b/presence.go index 21cfd77..6c9eb29 100644 --- a/presence.go +++ b/presence.go @@ -5,7 +5,7 @@ import "encoding/xml" // XMPP Packet Parsing type ClientPresence struct { XMLName xml.Name `xml:"jabber:client presence"` - Packet + PacketAttrs Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa Status string `xml:"status,attr,omitempty"` Priority string `xml:"priority,attr,omitempty"` diff --git a/stream.go b/stream.go index 510169c..e5173c6 100644 --- a/stream.go +++ b/stream.go @@ -2,7 +2,7 @@ package xmpp // import "fluux.io/xmpp" import "encoding/xml" -// XMPP Packet Parsing +// XMPP PacketAttrs Parsing type streamFeatures struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` StartTLS tlsStartTLS From 10219ec1e6d8a66f8196c4c572b49bbd899e00fc Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 18:50:17 +0100 Subject: [PATCH 08/11] Refactor parsing / improve typing --- auth.go | 49 ++++++++++++++++++++---- client.go | 2 +- client_test.go | 2 +- cmd/xmpp_component/xmpp_component.go | 11 +++++- cmd/xmpp_echo/xmpp_echo.go | 4 +- cmd/xmpp_jukebox/xmpp_jukebox.go | 12 +++--- component.go | 22 +++++++++-- iq.go | 26 ++++++++++--- iq_test.go | 8 ++-- message.go | 30 +++++++++++---- packet.go | 4 ++ parser.go | 56 ++++++++++------------------ presence.go | 22 +++++++++-- session.go | 4 +- stream.go | 28 +++++++++++++- 15 files changed, 198 insertions(+), 82 deletions(-) diff --git a/auth.go b/auth.go index 846dd33..76feac7 100644 --- a/auth.go +++ b/auth.go @@ -32,37 +32,72 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password fmt.Fprintf(socket, "%s", nsSASL, enc) // Next message should be either success or failure. - name, val, err := next(decoder) + val, err := next(decoder) if err != nil { return err } switch v := val.(type) { - case *saslSuccess: - case *saslFailure: + case *SASLSuccess: + case *SASLFailure: // v.Any is type of sub-element in failure, which gives a description of what failed. return errors.New("auth failure: " + v.Any.Local) default: - return errors.New("expected success or failure, got " + name.Local + " in " + name.Space) + return errors.New("expected SASL success or failure, got " + v.Name()) } return err } -// XMPP Packet Parsing type saslMechanisms struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl mechanisms"` Mechanism []string `xml:"mechanism"` } -type saslSuccess struct { +// ============================================================================ +// SASLSuccess + +type SASLSuccess struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"` } -type saslFailure struct { +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"` diff --git a/client.go b/client.go index ec62e37..defa642 100644 --- a/client.go +++ b/client.go @@ -108,7 +108,7 @@ func (c *Client) Connect() (*Session, error) { func (c *Client) recv(receiver chan<- interface{}) (err error) { for { - _, val, err := next(c.Session.decoder) + val, err := next(c.Session.decoder) if err != nil { return err } diff --git a/client_test.go b/client_test.go index a6746d2..2746041 100644 --- a/client_test.go +++ b/client_test.go @@ -163,7 +163,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { return } - iq := &ClientIQ{} + iq := &IQ{} // Decode element into pointer storage if err = decoder.DecodeElement(&iq, &se); err != nil { t.Errorf("cannot decode bind iq: %s", err) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index 223e83d..58f9227 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -11,11 +11,18 @@ func main() { component.Connect("localhost:8888") for { - _, packet, err := component.ReadPacket() + packet, err := component.ReadPacket() if err != nil { fmt.Println("read error", err) return } - fmt.Println("Packet received: ", packet) + + switch p := packet.(type) { + case xmpp.IQ: + fmt.Println("IQ received: ", p) + fmt.Println("IQ type:", p.Type) + default: + fmt.Println("Packet unhandled packet:", packet) + } } } diff --git a/cmd/xmpp_echo/xmpp_echo.go b/cmd/xmpp_echo/xmpp_echo.go index 012c713..d092508 100644 --- a/cmd/xmpp_echo/xmpp_echo.go +++ b/cmd/xmpp_echo/xmpp_echo.go @@ -31,9 +31,9 @@ func main() { // Iterator to receive packets coming from our XMPP connection for packet := range client.Recv() { switch packet := packet.(type) { - case *xmpp.ClientMessage: + case *xmpp.Message: fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From) - reply := xmpp.ClientMessage{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body} + reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body} client.Send(reply.XMPPFormat()) default: fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet) diff --git a/cmd/xmpp_jukebox/xmpp_jukebox.go b/cmd/xmpp_jukebox/xmpp_jukebox.go index a1737e6..a67d1fe 100644 --- a/cmd/xmpp_jukebox/xmpp_jukebox.go +++ b/cmd/xmpp_jukebox/xmpp_jukebox.go @@ -40,11 +40,11 @@ func main() { for packet := range client.Recv() { switch packet := packet.(type) { - case *xmpp.ClientMessage: + case *xmpp.Message: processMessage(client, p, packet) - case *xmpp.ClientIQ: + case *xmpp.IQ: processIq(client, p, packet) - case *xmpp.ClientPresence: + case *xmpp.Presence: // Do nothing with received presence default: fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet) @@ -52,7 +52,7 @@ func main() { } } -func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientMessage) { +func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.Message) { command := strings.Trim(packet.Body, " ") if command == "stop" { p.Stop() @@ -62,7 +62,7 @@ func processMessage(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientMe } } -func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientIQ) { +func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) { switch payload := packet.Payload.(type) { // We support IOT Control IQ case *iot.ControlSet: @@ -76,7 +76,7 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.ClientIQ) { playSCURL(p, url) setResponse := new(iot.ControlSetResponse) - reply := xmpp.ClientIQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: setResponse} + reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: setResponse} client.Send(reply.XMPPFormat()) // TODO add Soundclound artist / title retrieval sendUserTune(client, "Radiohead", "Spectre") diff --git a/component.go b/component.go index 4105700..6474660 100644 --- a/component.go +++ b/component.go @@ -72,7 +72,7 @@ func (c *Component) Connect(connStr string) error { } // 4. Check server response for authentication - name, val, err := next(c.decoder) + val, err := next(c.decoder) if err != nil { return err } @@ -83,20 +83,34 @@ func (c *Component) Connect(connStr string) error { case *Handshake: return nil default: - return errors.New("unexpected packet, got " + name.Local + " in " + name.Space) + return errors.New("unexpected packet, got " + v.Name()) } panic("unreachable") } // ReadPacket reads next incoming XMPP packet // TODO use defined interface Packet -func (c *Component) ReadPacket() (xml.Name, interface{}, error) { +func (c *Component) ReadPacket() (Packet, error) { return next(c.decoder) } // ============================================================================ -// XMPP packets struct +// Handshake Packet type Handshake struct { XMLName xml.Name `xml:"jabber:component:accept handshake"` } + +func (Handshake) Name() string { + return "component:handshake" +} + +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 +} diff --git a/iq.go b/iq.go index 9c02dc1..97438ba 100644 --- a/iq.go +++ b/iq.go @@ -7,9 +7,11 @@ import ( "fluux.io/xmpp/iot" ) -// info/query -type ClientIQ struct { - XMLName xml.Name `xml:"jabber:client iq"` +// ============================================================================ +// IQ Packet + +type IQ struct { // Info/Query + XMLName xml.Name `xml:"iq"` PacketAttrs Payload IQPayload `xml:",omitempty"` RawXML string `xml:",innerxml"` @@ -17,12 +19,26 @@ type ClientIQ struct { // Error clientError } +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 +} + type IQPayload interface { IsIQPayload() } // UnmarshalXML implements custom parsing for IQs -func (iq *ClientIQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { iq.XMLName = start.Name // Extract IQ attributes for _, attr := range start.Attr { @@ -80,7 +96,7 @@ func (iq *ClientIQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // XMPPFormat returns the string representation of the XMPP packet. // TODO: Should I simply rely on xml.Marshal ? -func (iq *ClientIQ) XMPPFormat() string { +func (iq *IQ) XMPPFormat() string { if iq.Payload != nil { var payload []byte var err error diff --git a/iq_test.go b/iq_test.go index 71e9edd..9042ebc 100644 --- a/iq_test.go +++ b/iq_test.go @@ -10,14 +10,14 @@ func TestUnmarshalIqs(t *testing.T) { //var cs1 = new(iot.ControlSet) var tests = []struct { iqString string - parsedIQ ClientIQ + parsedIQ IQ }{ - {"", ClientIQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}}, - //{"", ClientIQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}}, + {"", IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}}, + //{"", 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 { - var parsedIQ = new(ClientIQ) + var parsedIQ = new(IQ) err := xml.Unmarshal([]byte(test.iqString), parsedIQ) if err != nil { t.Errorf("Unmarshal(%s) returned error", test.iqString) diff --git a/message.go b/message.go index 5ae14d2..be3d758 100644 --- a/message.go +++ b/message.go @@ -5,20 +5,36 @@ import ( "fmt" ) -// XMPP Packet Parsing -type ClientMessage struct { - XMLName xml.Name `xml:"jabber:client message"` +// ============================================================================ +// Message Packet + +type Message struct { + XMLName xml.Name `xml:"message"` PacketAttrs Subject string `xml:"subject,omitempty"` Body string `xml:"body,omitempty"` Thread string `xml:"thread,omitempty"` } -// TODO: Func new message to create an empty message structure without the XML tag matching elements +func (Message) Name() string { + return "message" +} -func (message *ClientMessage) XMPPFormat() string { +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 +} + +func (msg *Message) XMPPFormat() string { return fmt.Sprintf(""+ "%s", - message.To, - xmlEscape(message.Body)) + msg.To, + xmlEscape(msg.Body)) } + +// TODO: Func new message to create an empty message structure without the XML tag matching elements diff --git a/packet.go b/packet.go index 9e58773..ca42d6c 100644 --- a/packet.go +++ b/packet.go @@ -1,5 +1,9 @@ package xmpp // import "fluux.io/xmpp" +type Packet interface { + Name() string +} + // PacketAttrs represents the common structure for base XMPP packets. type PacketAttrs struct { Id string `xml:"id,attr,omitempty"` diff --git a/parser.go b/parser.go index 42b940d..aec306c 100644 --- a/parser.go +++ b/parser.go @@ -63,87 +63,71 @@ func nextStart(p *xml.Decoder) (xml.StartElement, error) { // next scans XML token stream for next element and then assign a structure to decode // that elements. // TODO Use an interface to return packets interface xmppDecoder -func next(p *xml.Decoder) (xml.Name, interface{}, error) { - // Read start element to find out what type we want. +func next(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 xml.Name{}, nil, err + return nil, err } - // Put it in an interface and allocate the right structure - var nv interface{} // TODO: general case = Parse IQ / presence / message => split SASL Stream and component cases switch se.Name.Space { case NSStream: - if nv, err = decodeStream(se); err != nil { - return xml.Name{}, nil, err - } + return decodeStream(p, se) case nsSASL: - if nv, err = decodeSASL(se); err != nil { - return xml.Name{}, nil, err - } + return decodeSASL(p, se) case NSClient: - if nv, err = decodeClient(se); err != nil { - return xml.Name{}, nil, err - } + return decodeClient(p, se) case NSComponent: - if nv, err = decodeComponent(se); err != nil { - return xml.Name{}, nil, err - } + return decodeComponent(p, se) default: - return xml.Name{}, nil, errors.New("unknown namespace " + + return nil, errors.New("unknown namespace " + se.Name.Space + " <" + se.Name.Local + "/>") } - - // Decode element into pointer storage - if err = p.DecodeElement(nv, &se); err != nil { - return xml.Name{}, nil, err - } - return se.Name, nv, err } -func decodeStream(se xml.StartElement) (interface{}, error) { +func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "error": - return &StreamError{}, nil + return streamError.decode(p, se) default: return nil, errors.New("unexpected XMPP packet " + se.Name.Space + " <" + se.Name.Local + "/>") } } -func decodeSASL(se xml.StartElement) (interface{}, error) { +func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "success": - return &saslSuccess{}, nil + return saslSuccess.decode(p, se) case "failure": - return &saslFailure{}, nil + return saslFailure.decode(p, se) default: return nil, errors.New("unexpected XMPP packet " + se.Name.Space + " <" + se.Name.Local + "/>") } } -func decodeClient(se xml.StartElement) (interface{}, error) { +func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "message": - return &ClientMessage{}, nil + return message.decode(p, se) case "presence": - return &ClientPresence{}, nil + return presence.decode(p, se) case "iq": - return &ClientIQ{}, nil + return iq.decode(p, se) default: return nil, errors.New("unexpected XMPP packet " + se.Name.Space + " <" + se.Name.Local + "/>") } } -func decodeComponent(se xml.StartElement) (interface{}, error) { +func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "handshake": - return &Handshake{}, nil + return handshake.decode(p, se) case "iq": - return &ClientIQ{}, nil + return iq.decode(p, se) default: return nil, errors.New("unexpected XMPP packet " + se.Name.Space + " <" + se.Name.Local + "/>") diff --git a/presence.go b/presence.go index 6c9eb29..6d9eaed 100644 --- a/presence.go +++ b/presence.go @@ -2,12 +2,28 @@ package xmpp // import "fluux.io/xmpp" import "encoding/xml" -// XMPP Packet Parsing -type ClientPresence struct { - XMLName xml.Name `xml:"jabber:client presence"` +// ============================================================================ +// Presence Packet + +type Presence struct { + XMLName xml.Name `xml:"presence"` PacketAttrs Show string `xml:"show,attr,omitempty"` // away, chat, dnd, xa Status string `xml:"status,attr,omitempty"` Priority string `xml:"priority,attr,omitempty"` //Error *clientError } + +func (Presence) Name() string { + return "presence" +} + +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) + return packet, err +} diff --git a/session.go b/session.go index 6012dad..dc47ffe 100644 --- a/session.go +++ b/session.go @@ -157,7 +157,7 @@ func (s *Session) bind(o Options) { fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsBind) } - var iq ClientIQ + var iq IQ if s.err = s.decoder.Decode(&iq); s.err != nil { s.err = errors.New("error decoding iq bind result: " + s.err.Error()) return @@ -180,7 +180,7 @@ func (s *Session) rfc3921Session(o Options) { return } - var iq ClientIQ + var iq IQ if s.Features.Session.optional.Local != "" { fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsSession) if s.err = s.decoder.Decode(&iq); s.err != nil { diff --git a/stream.go b/stream.go index e5173c6..3c6afd6 100644 --- a/stream.go +++ b/stream.go @@ -1,8 +1,12 @@ package xmpp // import "fluux.io/xmpp" -import "encoding/xml" +import ( + "encoding/xml" +) + +// ============================================================================ +// StreamFeatures Packet -// XMPP PacketAttrs Parsing type streamFeatures struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"` StartTLS tlsStartTLS @@ -13,11 +17,31 @@ type streamFeatures struct { Any []xml.Name `xml:",any"` } +// ============================================================================ +// StreamError Packet + type StreamError struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams error"` Error xml.Name `xml:",any"` } +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 +} + +// ============================================================================ +// Caps subElement + type Caps struct { XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"` Hash string `xml:"hash,attr"` From ec95020ac27e8e49f1e9b4cda865d20424c5d0b7 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 18:56:38 +0100 Subject: [PATCH 09/11] Fix failing test --- auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth.go b/auth.go index 76feac7..3b4b754 100644 --- a/auth.go +++ b/auth.go @@ -38,8 +38,8 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password } switch v := val.(type) { - case *SASLSuccess: - case *SASLFailure: + case SASLSuccess: + case SASLFailure: // v.Any is type of sub-element in failure, which gives a description of what failed. return errors.New("auth failure: " + v.Any.Local) default: From e14f58d9a991a129ae3811ed2c79116d4c2725f3 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 19:14:26 +0100 Subject: [PATCH 10/11] Decode query --- cmd/xmpp_component/xmpp_component.go | 6 ++++-- iq.go | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index 58f9227..3a6fb4b 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -19,8 +19,10 @@ func main() { switch p := packet.(type) { case xmpp.IQ: - fmt.Println("IQ received: ", p) - fmt.Println("IQ type:", p.Type) + switch p.Payload.(type) { + case *xmpp.Query: + fmt.Println("Received query:", p.Type) + } default: fmt.Println("Packet unhandled packet:", packet) } diff --git a/iq.go b/iq.go index 97438ba..582bcae 100644 --- a/iq.go +++ b/iq.go @@ -73,6 +73,8 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { switch tt.Name.Space + " " + tt.Name.Local { case "urn:ietf:params:xml:ns:xmpp-bind bind": p = new(bindBind) + case "http://jabber.org/protocol/disco#items query": + p = new(Query) case "urn:xmpp:iot:control set": p = new(iot.ControlSet) // TODO: Add a default Type that passes RawXML @@ -114,3 +116,12 @@ func (iq *IQ) XMPPFormat() string { iq.To, iq.Type, iq.Id, iq.RawXML) } + +// ============================================================================ +// IQ Query subelement + +type Query struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` +} + +func (*Query) IsIQPayload() {} From 94815de1733d30e0230ee6053a67240abe73624b Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 19:27:46 +0100 Subject: [PATCH 11/11] Makes parsing of inner IQ XML generic --- cmd/xmpp_component/xmpp_component.go | 8 +++++--- iq.go | 15 ++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cmd/xmpp_component/xmpp_component.go b/cmd/xmpp_component/xmpp_component.go index 3a6fb4b..7b794c2 100644 --- a/cmd/xmpp_component/xmpp_component.go +++ b/cmd/xmpp_component/xmpp_component.go @@ -19,9 +19,11 @@ func main() { switch p := packet.(type) { case xmpp.IQ: - switch p.Payload.(type) { - case *xmpp.Query: - fmt.Println("Received query:", p.Type) + switch inner := p.Payload.(type) { + case *xmpp.Node: + fmt.Println("Node:", inner.XMLName.Space, inner.XMLName.Local) + default: + fmt.Println("default") } default: fmt.Println("Packet unhandled packet:", packet) diff --git a/iq.go b/iq.go index 582bcae..63667f1 100644 --- a/iq.go +++ b/iq.go @@ -73,11 +73,10 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { switch tt.Name.Space + " " + tt.Name.Local { case "urn:ietf:params:xml:ns:xmpp-bind bind": p = new(bindBind) - case "http://jabber.org/protocol/disco#items query": - p = new(Query) case "urn:xmpp:iot:control set": p = new(iot.ControlSet) - // TODO: Add a default Type that passes RawXML + default: + p = new(Node) } if p != nil { err = d.DecodeElement(p, &tt) @@ -118,10 +117,12 @@ func (iq *IQ) XMPPFormat() string { } // ============================================================================ -// IQ Query subelement +// Genery IQ Node -type Query struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` +type Node struct { + XMLName xml.Name + Content []byte `xml:",innerxml"` + Nodes []Node `xml:",any"` } -func (*Query) IsIQPayload() {} +func (*Node) IsIQPayload() {}