From 10219ec1e6d8a66f8196c4c572b49bbd899e00fc Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Sat, 13 Jan 2018 18:50:17 +0100 Subject: [PATCH] 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"`