From d9fdff08395b92c41b38bf57d9777b417d024bbe Mon Sep 17 00:00:00 2001 From: genofire Date: Sat, 22 Jun 2019 11:13:33 +0200 Subject: [PATCH] Add constants (enumlike) for stanza types and simplify packet creation (#62) * Add constants (enumlike) for stanza types * NewIQ, NewMessage and NewPresence are now initialized with the Attrs struct * Update examples * Do not export backoff code. For now, we do not need to expose backoff in the documentation * Make presence priority an int8 --- _examples/delegation/delegation.go | 6 +- _examples/go.mod | 2 + _examples/go.sum | 3 + _examples/xmpp_component/xmpp_component.go | 6 +- _examples/xmpp_echo/xmpp_echo.go | 2 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 5 +- auth.go | 2 +- backoff.go | 28 ++-- backoff_test.go | 12 +- check_cert.go | 4 +- client.go | 2 +- component.go | 6 +- doc.go | 16 ++- error.go | 118 +++++++++++++++++ error_enum.go | 13 ++ iq.go | 141 ++------------------- iq_test.go | 6 +- message.go | 16 +-- message_test.go | 2 +- packet.go | 14 +- packet_enum.go | 25 ++++ parser.go | 30 +++-- presence.go | 20 ++- presence_enum.go | 12 ++ presence_test.go | 14 +- router_test.go | 13 +- session.go | 2 +- stream_manager.go | 4 +- 28 files changed, 299 insertions(+), 225 deletions(-) create mode 100644 error.go create mode 100644 error_enum.go create mode 100644 packet_enum.go create mode 100644 presence_enum.go diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index 1577147..473fefa 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -83,7 +83,7 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) { return } - iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en") + iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) switch info.Node { case "": @@ -192,7 +192,7 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) { if pubsub.Publish.XMLName.Local == "publish" { // Prepare pubsub IQ reply - iqResp := xmpp.NewIQ("result", forwardedIQ.To, forwardedIQ.From, forwardedIQ.Id, "en") + iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) payload := xmpp.PubSub{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/pubsub", @@ -201,7 +201,7 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) { } iqResp.Payload = &payload // Wrap the reply in delegation 'forward' - iqForward := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en") + iqForward := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) delegPayload := xmpp.Delegation{ XMLName: xml.Name{ Space: "urn:xmpp:delegation:1", diff --git a/_examples/go.mod b/_examples/go.mod index c9263aa..21ddbc7 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -8,3 +8,5 @@ require ( github.com/processone/soundcloud v1.0.0 gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52 ) + +replace gosrc.io/xmpp => gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764 diff --git a/_examples/go.sum b/_examples/go.sum index b442e11..889d4f7 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -8,4 +8,7 @@ golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52 h1:H5BezaFYvDL9r72ng90ICneftomo1iXx6+BxxZ9jBtg= gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52/go.mod h1:WvSgrZF7lMvjd1SH8nVGi7ZGr6gNU7oUuBdwpFTs9nY= +gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764 h1:jlYtpqdRoBC3Gke7MacXsVpSZL0g5nIBG/b9JVxpAVY= +gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764/go.mod h1:WvSgrZF7lMvjd1SH8nVGi7ZGr6gNU7oUuBdwpFTs9nY= diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index 9752dca..17cfc1a 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -59,7 +59,7 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) { return } - iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en") + iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) identity := xmpp.Identity{ Name: opts.Name, Category: opts.Category, @@ -95,7 +95,7 @@ func discoItems(c xmpp.Sender, p xmpp.Packet) { return } - iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en") + iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) var payload xmpp.DiscoItems if discoItems.Node == "" { @@ -116,7 +116,7 @@ func handleVersion(c xmpp.Sender, p xmpp.Packet) { return } - iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en") + iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) var payload xmpp.Version payload.Name = "Fluux XMPP Component" payload.Version = "0.0.1" diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go index 8aaad73..3725f08 100644 --- a/_examples/xmpp_echo/xmpp_echo.go +++ b/_examples/xmpp_echo/xmpp_echo.go @@ -43,7 +43,7 @@ func handleMessage(s xmpp.Sender, p xmpp.Packet) { } _, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From) - reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: msg.From}, Body: msg.Body} + reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body} _ = s.Send(reply) } diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index c958d3f..2218279 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -34,6 +34,7 @@ func main() { Address: *address, Jid: *jid, Password: *password, + // PacketLogger: os.Stdout, Insecure: true, } @@ -91,7 +92,7 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) { playSCURL(player, url) setResponse := new(xmpp.ControlSetResponse) // FIXME: Broken - reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse} + reply := xmpp.IQ{Attrs: xmpp.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse} _ = s.Send(reply) // TODO add Soundclound artist / title retrieval sendUserTune(s, "Radiohead", "Spectre") @@ -102,7 +103,7 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) { func sendUserTune(s xmpp.Sender, artist string, title string) { tune := xmpp.Tune{Artist: artist, Title: title} - iq := xmpp.NewIQ("set", "", "", "usertune-1", "en") + iq := xmpp.NewIQ(xmpp.Attrs{Type: "set", Id: "usertune-1", Lang: "en"}) payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}} iq.Payload = &payload _ = s.Send(iq) diff --git a/auth.go b/auth.go index 90b73e5..497258d 100644 --- a/auth.go +++ b/auth.go @@ -33,7 +33,7 @@ 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. - val, err := next(decoder) + val, err := nextPacket(decoder) if err != nil { return err } diff --git a/backoff.go b/backoff.go index caa53d1..2dfec8d 100644 --- a/backoff.go +++ b/backoff.go @@ -13,7 +13,7 @@ It can be used in several ways: - Using ticker channel to trigger callback function on tick The functions for Backoff are not threadsafe, but you can: -- Keep the attempt counter on your end and use DurationForAttempt(int) +- Keep the attempt counter on your end and use durationForAttempt(int) - Use lock in your own code to protect the Backoff structure. TODO: Implement Backoff Ticker channel @@ -34,11 +34,11 @@ const ( defaultCap int = 180000 // 3 minutes ) -// Backoff can provide increasing duration with the number of attempt +// backoff provides increasing duration with the number of attempt // performed. The structure is used to support exponential backoff on // connection attempts to avoid hammering the server we are connecting // to. -type Backoff struct { +type backoff struct { NoJitter bool Base int Factor int @@ -47,20 +47,20 @@ type Backoff struct { attempt int } -// Duration returns the duration to apply to the current attempt. -func (b *Backoff) Duration() time.Duration { - d := b.DurationForAttempt(b.attempt) +// duration returns the duration to apply to the current attempt. +func (b *backoff) duration() time.Duration { + d := b.durationForAttempt(b.attempt) b.attempt++ return d } -// Wait sleeps for backoff duration for current attempt. -func (b *Backoff) Wait() { - time.Sleep(b.Duration()) +// wait sleeps for backoff duration for current attempt. +func (b *backoff) wait() { + time.Sleep(b.duration()) } -// DurationForAttempt returns a duration for an attempt number, in a stateless way. -func (b *Backoff) DurationForAttempt(attempt int) time.Duration { +// durationForAttempt returns a duration for an attempt number, in a stateless way. +func (b *backoff) durationForAttempt(attempt int) time.Duration { b.setDefault() expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt))) d := int(math.Trunc(expBackoff)) @@ -70,13 +70,13 @@ func (b *Backoff) DurationForAttempt(attempt int) time.Duration { return time.Duration(d) * time.Millisecond } -// Reset sets back the number of attempts to 0. This is to be called after a successfull operation has been performed, +// reset sets back the number of attempts to 0. This is to be called after a successful operation has been performed, // to reset the exponential backoff interval. -func (b *Backoff) Reset() { +func (b *backoff) reset() { b.attempt = 0 } -func (b *Backoff) setDefault() { +func (b *backoff) setDefault() { if b.Base == 0 { b.Base = defaultBase } diff --git a/backoff_test.go b/backoff_test.go index 9ef7ce0..9a7fde7 100644 --- a/backoff_test.go +++ b/backoff_test.go @@ -1,21 +1,19 @@ -package xmpp_test +package xmpp import ( "testing" "time" - - "gosrc.io/xmpp" ) func TestDurationForAttempt_NoJitter(t *testing.T) { - b := xmpp.Backoff{Base: 25, NoJitter: true} + b := backoff{Base: 25, NoJitter: true} bInMS := time.Duration(b.Base) * time.Millisecond - if b.DurationForAttempt(0) != bInMS { - t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.DurationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond) + if b.durationForAttempt(0) != bInMS { + t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond) } var prevDuration, d time.Duration for i := 0; i < 10; i++ { - d = b.DurationForAttempt(i) + d = b.durationForAttempt(i) if !(d >= prevDuration) { t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration) } diff --git a/check_cert.go b/check_cert.go index 05e44e1..5addd87 100644 --- a/check_cert.go +++ b/check_cert.go @@ -54,14 +54,14 @@ func (c *ServerCheck) Check() error { } // Set xml decoder and extract streamID from reply (not used for now) - _, err = initDecoder(decoder) + _, err = initStream(decoder) if err != nil { return err } // extract stream features var f StreamFeatures - packet, err := next(decoder) + packet, err := nextPacket(decoder) if err != nil { err = fmt.Errorf("stream open decode features: %s", err) return err diff --git a/client.go b/client.go index 2fa6b03..bb62ce8 100644 --- a/client.go +++ b/client.go @@ -200,7 +200,7 @@ func (c *Client) SendRaw(packet string) error { // Loop: Receive data from server func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) { for { - val, err := next(c.Session.decoder) + val, err := nextPacket(c.Session.decoder) if err != nil { close(keepaliveQuit) c.updateState(StateDisconnected) diff --git a/component.go b/component.go index 8ae8c2e..5fedc9a 100644 --- a/component.go +++ b/component.go @@ -78,7 +78,7 @@ func (c *Component) Connect() error { c.decoder = xml.NewDecoder(conn) // 2. Initialize xml decoder and extract streamID from reply - streamId, err := initDecoder(c.decoder) + streamId, err := initStream(c.decoder) if err != nil { return errors.New("cannot init decoder " + err.Error()) } @@ -89,7 +89,7 @@ func (c *Component) Connect() error { } // 4. Check server response for authentication - val, err := next(c.decoder) + val, err := nextPacket(c.decoder) if err != nil { return err } @@ -119,7 +119,7 @@ func (c *Component) SetHandler(handler EventHandler) { // Receiver Go routine receiver func (c *Component) recv() (err error) { for { - val, err := next(c.decoder) + val, err := nextPacket(c.decoder) if err != nil { c.updateState(StateDisconnected) return err diff --git a/doc.go b/doc.go index 5e1c074..40f4f6a 100644 --- a/doc.go +++ b/doc.go @@ -1,13 +1,23 @@ /* -Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT. +Fluux XMPP is an modern and full-featured XMPP library that can be used to build clients or +server components. -The goal is to make simple to write simple adhoc XMPP clients: +The goal is to make simple to write modern compliant XMPP software: - For automation (like for example monitoring of an XMPP service), - For building connected "things" by plugging them on an XMPP server, - For writing simple chatbots to control a service or a thing. + - For writing XMPP servers components. Fluux XMPP supports: + - XEP-0114: Jabber Component Protocol + - XEP-0355: Namespace Delegation + - XEP-0356: Privileged Entity -Fluux XMPP can be used to build XMPP clients or XMPP components. +The library is designed to have minimal dependencies. For now, the library does not depend on any other library. + +The library includes a StreamManager that provides features like autoreconnect exponential back-off. + +The library is implementing latest versions of the XMPP specifications (RFC 6120 and RFC 6121), and includes +support for many extensions. Clients diff --git a/error.go b/error.go new file mode 100644 index 0000000..2fe542f --- /dev/null +++ b/error.go @@ -0,0 +1,118 @@ +package xmpp + +import ( + "encoding/xml" + "strconv" +) + +/* +TODO support ability to put Raw payload inside IQ +*/ + +// ============================================================================ +// XMPP Errors + +// Err is an XMPP stanza payload that is used to report error on message, +// presence or iq stanza. +// It is intended to be added in the payload of the erroneous stanza. +type Err struct { + XMLName xml.Name `xml:"error"` + Code int `xml:"code,attr,omitempty"` + Type ErrorType `xml:"type,attr"` // required + Reason string + Text string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"` +} + +func (x *Err) Namespace() string { + return x.XMLName.Space +} + +// 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 = ErrorType(attr.Value) + } + if attr.Name.Local == "code" { + if code, err := strconv.Atoi(attr.Value); err == nil { + x.Code = code + } + } + } + + // Check subelements to extract error text and reason (from local namespace). + 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) { + if x.Code == 0 { + return nil + } + + // Encode start element and attributes + start.Name = xml.Name{Local: "error"} + + code := xml.Attr{ + Name: xml.Name{Local: "code"}, + Value: strconv.Itoa(x.Code), + } + start.Attr = append(start.Attr, code) + + if len(x.Type) > 0 { + typ := xml.Attr{ + Name: xml.Name{Local: "type"}, + Value: string(x.Type), + } + start.Attr = append(start.Attr, 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}) +} diff --git a/error_enum.go b/error_enum.go new file mode 100644 index 0000000..b89b925 --- /dev/null +++ b/error_enum.go @@ -0,0 +1,13 @@ +package xmpp + +// ErrorType is a Enum of error attribute type +type ErrorType string + +// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace +const ( + ErrorTypeAuth ErrorType = "auth" + ErrorTypeCancel ErrorType = "cancel" + ErrorTypeContinue ErrorType = "continue" + ErrorTypeModify ErrorType = "motify" + ErrorTypeWait ErrorType = "wait" +) diff --git a/iq.go b/iq.go index ec165eb..4597609 100644 --- a/iq.go +++ b/iq.go @@ -3,127 +3,20 @@ package xmpp import ( "encoding/xml" "fmt" - "strconv" ) /* TODO support ability to put Raw payload inside IQ */ -// ============================================================================ -// XMPP Errors - -// Err is an XMPP stanza payload that is used to report error on message, -// presence or iq stanza. -// It is intended to be added in the payload of the erroneous stanza. -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 (x *Err) Namespace() string { - return x.XMLName.Space -} - -// 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 - } - } - } - - // Check subelements to extract error text and reason (from local namespace). - 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) { - if x.Code == 0 { - return nil - } - - // Encode start element and attributes - start.Name = xml.Name{Local: "error"} - - code := xml.Attr{ - Name: xml.Name{Local: "code"}, - Value: strconv.Itoa(x.Code), - } - start.Attr = append(start.Attr, code) - - if len(x.Type) > 0 { - typ := xml.Attr{ - Name: xml.Name{Local: "type"}, - Value: x.Type, - } - start.Attr = append(start.Attr, typ) - } - err = e.EncodeToken(start) - - // SubTags - // Reason - if x.Reason != "" { - reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason} - e.EncodeToken(xml.StartElement{Name: reason}) - e.EncodeToken(xml.EndElement{Name: reason}) - } - - // Text - if x.Text != "" { - text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"} - e.EncodeToken(xml.StartElement{Name: text}) - e.EncodeToken(xml.CharData(x.Text)) - e.EncodeToken(xml.EndElement{Name: text}) - } - - return e.EncodeToken(xml.EndElement{Name: start.Name}) -} - // ============================================================================ // IQ Packet +// IQ implements RFC 6120 - A.5 Client Namespace (a part) type IQ struct { // Info/Query XMLName xml.Name `xml:"iq"` - PacketAttrs + // MUST have a ID + Attrs // We can only have one payload on IQ: // "An IQ stanza of type "get" or "set" MUST contain exactly one // child element, which specifies the semantics of the particular @@ -133,16 +26,16 @@ type IQ struct { // Info/Query RawXML string `xml:",innerxml"` } -func NewIQ(iqtype, from, to, id, lang string) IQ { +type IQPayload interface { + Namespace() string +} + +func NewIQ(a Attrs) IQ { + // TODO generate IQ ID if not set + // TODO ensure that type is set, as it is required return IQ{ XMLName: xml.Name{Local: "iq"}, - PacketAttrs: PacketAttrs{ - Id: id, - From: from, - To: to, - Type: iqtype, - Lang: lang, - }, + Attrs: a, } } @@ -182,7 +75,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { iq.Id = attr.Value } if attr.Name.Local == "type" { - iq.Type = attr.Value + iq.Type = StanzaType(attr.Value) } if attr.Name.Local == "to" { iq.To = attr.Value @@ -190,9 +83,6 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if attr.Name.Local == "from" { iq.From = attr.Value } - if attr.Name.Local == "lang" { - iq.Lang = attr.Value - } } // decode inner elements @@ -223,6 +113,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { iq.Payload = iqExt continue } + // TODO: If unknown decode as generic node return fmt.Errorf("unexpected element in iq: %s %s", tt.Name.Space, tt.Name.Local) case xml.EndElement: if tt == start.End() { @@ -233,11 +124,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } // ============================================================================ -// Generic IQ Payload - -type IQPayload interface { - Namespace() string -} +// Generic / unknown content // Node is a generic structure to represent XML data. It is used to parse // unreferenced or custom stanza payload. diff --git a/iq_test.go b/iq_test.go index 790d173..0a8aae6 100644 --- a/iq_test.go +++ b/iq_test.go @@ -16,7 +16,7 @@ func TestUnmarshalIqs(t *testing.T) { parsedIQ xmpp.IQ }{ {"", - xmpp.IQ{XMLName: xml.Name{Space: "", Local: "iq"}, PacketAttrs: xmpp.PacketAttrs{To: "test@localhost", Type: "set", Id: "1"}}}, + xmpp.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: xmpp.Attrs{Type: xmpp.IQTypeSet, To: "test@localhost", Id: "1"}}}, //{"", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}}, } @@ -35,7 +35,7 @@ func TestUnmarshalIqs(t *testing.T) { } func TestGenerateIq(t *testing.T) { - iq := xmpp.NewIQ("result", "admin@localhost", "test@localhost", "1", "en") + iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) payload := xmpp.DiscoInfo{ Identity: xmpp.Identity{ Name: "Test Gateway", @@ -93,7 +93,7 @@ func TestErrorTag(t *testing.T) { } func TestDiscoItems(t *testing.T) { - iq := xmpp.NewIQ("get", "romeo@montague.net/orchard", "catalog.shakespeare.lit", "items3", "en") + iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) payload := xmpp.DiscoItems{ Node: "music", } diff --git a/message.go b/message.go index 906f6e3..07b3379 100644 --- a/message.go +++ b/message.go @@ -7,9 +7,11 @@ import ( // ============================================================================ // Message Packet +// Message implements RFC 6120 - A.5 Client Namespace (a part) type Message struct { XMLName xml.Name `xml:"message"` - PacketAttrs + Attrs + Subject string `xml:"subject,omitempty"` Body string `xml:"body,omitempty"` Thread string `xml:"thread,omitempty"` @@ -21,16 +23,10 @@ func (Message) Name() string { return "message" } -func NewMessage(msgtype, from, to, id, lang string) Message { +func NewMessage(a Attrs) Message { return Message{ XMLName: xml.Name{Local: "message"}, - PacketAttrs: PacketAttrs{ - Id: id, - From: from, - To: to, - Type: msgtype, - Lang: lang, - }, + Attrs: a, } } @@ -63,7 +59,7 @@ func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { msg.Id = attr.Value } if attr.Name.Local == "type" { - msg.Type = attr.Value + msg.Type = StanzaType(attr.Value) } if attr.Name.Local == "to" { msg.To = attr.Value diff --git a/message_test.go b/message_test.go index fb30029..b7775f4 100644 --- a/message_test.go +++ b/message_test.go @@ -9,7 +9,7 @@ import ( ) func TestGenerateMessage(t *testing.T) { - message := xmpp.NewMessage("chat", "admin@localhost", "test@localhost", "1", "en") + message := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"}) message.Body = "Hi" message.Subject = "Msg Subject" diff --git a/packet.go b/packet.go index 4b6f519..ce42236 100644 --- a/packet.go +++ b/packet.go @@ -4,13 +4,13 @@ type Packet interface { Name() string } -// 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"` - Type string `xml:"type,attr,omitempty"` - Lang string `xml:"lang,attr,omitempty"` +// Attrs represents the common structure for base XMPP packets. +type Attrs struct { + Type StanzaType `xml:"type,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` + From string `xml:"from,attr,omitempty"` + To string `xml:"to,attr,omitempty"` + Lang string `xml:"lang,attr,omitempty"` } type packetFormatter interface { diff --git a/packet_enum.go b/packet_enum.go new file mode 100644 index 0000000..9bc30e4 --- /dev/null +++ b/packet_enum.go @@ -0,0 +1,25 @@ +package xmpp + +type StanzaType string + +// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace +const ( + IQTypeError StanzaType = "error" + IQTypeGet StanzaType = "get" + IQTypeResult StanzaType = "result" + IQTypeSet StanzaType = "set" + + MessageTypeChat StanzaType = "chat" + MessageTypeError StanzaType = "error" + MessageTypeGroupchat StanzaType = "groupchat" + MessageTypeHeadline StanzaType = "headline" + MessageTypeNormal StanzaType = "normal" // Default + + PresenceTypeError StanzaType = "error" + PresenceTypeProbe StanzaType = "probe" + PresenceTypeSubscribe StanzaType = "subscribe" + PresenceTypeSubscribed StanzaType = "subscribed" + PresenceTypeUnavailable StanzaType = "unavailable" + PresenceTypeUnsubscribe StanzaType = "unsubscribe" + PresenceTypeUnsubscribed StanzaType = "unsubscribed" +) diff --git a/parser.go b/parser.go index bc17dd5..465db5d 100644 --- a/parser.go +++ b/parser.go @@ -14,29 +14,29 @@ import ( // reattach features (allowing to resume an existing stream at the point the connection was interrupted, without // getting through the authentication process. // TODO We should handle stream error from XEP-0114 ( or ) -func initDecoder(p *xml.Decoder) (sessionID string, err error) { +func initStream(p *xml.Decoder) (sessionID string, err error) { for { var t xml.Token t, err = p.Token() if err != nil { - return + return sessionID, err } switch elem := t.(type) { case xml.StartElement: if elem.Name.Space != NSStream || elem.Name.Local != "stream" { err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space) - return + return sessionID, err } - // Parse Stream attributes + // Parse XMPP stream attributes for _, attrs := range elem.Attr { switch attrs.Name.Local { case "id": sessionID = attrs.Value } } - return + return sessionID, err } } } @@ -58,10 +58,12 @@ 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. +// nextPacket scans XML token stream for next complete XMPP stanza. +// Once the type of stanza has been identified, a structure is created to decode +// that stanza and returned. // TODO Use an interface to return packets interface xmppDecoder -func next(p *xml.Decoder) (Packet, error) { +// TODO make auth and bind use nextPacket instead of directly nextStart +func nextPacket(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 { @@ -84,6 +86,13 @@ func next(p *xml.Decoder) (Packet, error) { } } +/* +TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that + type. + That way, we have a consistent way to do type assertion, always matching against pointers. +*/ + +// decodeStream will fully decode a stream packet func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "error": @@ -96,6 +105,7 @@ func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) { } } +// decodeSASL decodes a packet related to SASL authentication. func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "success": @@ -108,6 +118,7 @@ func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) { } } +// decodeClient decodes all known packets in the client namespace. func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { case "message": @@ -122,9 +133,10 @@ func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) { } } +// decodeClient decodes all known packets in the component namespace. func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) { switch se.Name.Local { - case "handshake": + case "handshake": // handshake is used to authenticate components return handshake.decode(p, se) case "message": return message.decode(p, se) diff --git a/presence.go b/presence.go index 95c0a2f..be9c6f2 100644 --- a/presence.go +++ b/presence.go @@ -5,28 +5,24 @@ import "encoding/xml" // ============================================================================ // Presence Packet +// Presence implements RFC 6120 - A.5 Client Namespace (a part) type Presence struct { XMLName xml.Name `xml:"presence"` - PacketAttrs - Show string `xml:"show,omitempty"` // away, chat, dnd, xa - Status string `xml:"status,omitempty"` - Priority int `xml:"priority,omitempty"` - Error Err `xml:"error,omitempty"` + Attrs + Show PresenceShow `xml:"show,omitempty"` + Status string `xml:"status,omitempty"` + Priority int8 `xml:"priority,omitempty"` // default: 0 + Error Err `xml:"error,omitempty"` } func (Presence) Name() string { return "presence" } -func NewPresence(from, to, id, lang string) Presence { +func NewPresence(a Attrs) Presence { return Presence{ XMLName: xml.Name{Local: "presence"}, - PacketAttrs: PacketAttrs{ - Id: id, - From: from, - To: to, - Lang: lang, - }, + Attrs: a, } } diff --git a/presence_enum.go b/presence_enum.go new file mode 100644 index 0000000..c0723bd --- /dev/null +++ b/presence_enum.go @@ -0,0 +1,12 @@ +package xmpp + +// PresenceShow is a Enum of presence element show +type PresenceShow string + +// RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace +const ( + PresenceShowAway PresenceShow = "away" + PresenceShowChat PresenceShow = "chat" + PresenceShowDND PresenceShow = "dnd" + PresenceShowXA PresenceShow = "xa" +) diff --git a/presence_test.go b/presence_test.go index 9d33cc1..fb8e09a 100644 --- a/presence_test.go +++ b/presence_test.go @@ -10,8 +10,8 @@ import ( ) func TestGeneratePresence(t *testing.T) { - presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en") - presence.Show = "chat" + presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) + presence.Show = xmpp.PresenceShowChat data, err := xml.Marshal(presence) if err != nil { @@ -32,13 +32,13 @@ func TestPresenceSubElt(t *testing.T) { // Test structure to ensure that show, status and priority are correctly defined as presence // package sub-elements type pres struct { - Show string `xml:"show"` - Status string `xml:"status"` - Priority int `xml:"priority"` + Show xmpp.PresenceShow `xml:"show"` + Status string `xml:"status"` + Priority int8 `xml:"priority"` } - presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en") - presence.Show = "xa" + presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) + presence.Show = xmpp.PresenceShowXA presence.Status = "Coding" presence.Priority = 10 diff --git a/router_test.go b/router_test.go index 4b6efe6..02ff2b1 100644 --- a/router_test.go +++ b/router_test.go @@ -19,8 +19,7 @@ func TestNameMatcher(t *testing.T) { // Check that a message packet is properly matched conn := NewSenderMock() - // TODO: We want packet creation code to use struct to use default values - msg := xmpp.NewMessage("chat", "", "test@localhost", "1", "") + msg := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, To: "test@localhost", Id: "1"}) msg.Body = "Hello" router.Route(conn, msg) if conn.String() != successFlag { @@ -29,7 +28,7 @@ func TestNameMatcher(t *testing.T) { // Check that an IQ packet is not matched conn = NewSenderMock() - iq := xmpp.NewIQ("get", "", "localhost", "1", "") + iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"}) iq.Payload = &xmpp.DiscoInfo{} router.Route(conn, iq) if conn.String() == successFlag { @@ -47,7 +46,8 @@ func TestIQNSMatcher(t *testing.T) { // Check that an IQ with proper namespace does match conn := NewSenderMock() - iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "") + iqDisco := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"}) + // TODO: Add a function to generate payload with proper namespace initialisation iqDisco.Payload = &xmpp.DiscoInfo{ XMLName: xml.Name{ Space: xmpp.NSDiscoInfo, @@ -60,7 +60,8 @@ func TestIQNSMatcher(t *testing.T) { // Check that another namespace is not matched conn = NewSenderMock() - iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "") + iqVersion := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"}) + // TODO: Add a function to generate payload with proper namespace initialisation iqVersion.Payload = &xmpp.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", @@ -240,7 +241,7 @@ func (s SenderMock) String() string { func TestSenderMock(t *testing.T) { conn := NewSenderMock() - msg := xmpp.NewMessage("", "", "test@localhost", "1", "") + msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost", Id: "1"}) msg.Body = "Hello" if err := conn.Send(msg); err != nil { t.Error("Could not send message") diff --git a/session.go b/session.go index 48dc02f..88486c6 100644 --- a/session.go +++ b/session.go @@ -92,7 +92,7 @@ func (s *Session) open(domain string) (f StreamFeatures) { } // Set xml decoder and extract streamID from reply - s.StreamId, s.err = initDecoder(s.decoder) // TODO refactor / rename + s.StreamId, s.err = initStream(s.decoder) // TODO refactor / rename if s.err != nil { return } diff --git a/stream_manager.go b/stream_manager.go index 0b8cda7..9c7e020 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -104,7 +104,7 @@ func (sm *StreamManager) Stop() { // connect manages the reconnection loop and apply the define backoff to avoid overloading the server. func (sm *StreamManager) connect() error { - var backoff Backoff // TODO: Group backoff calculation features with connection manager? + var backoff backoff // TODO: Group backoff calculation features with connection manager? for { var err error @@ -118,7 +118,7 @@ func (sm *StreamManager) connect() error { return xerrors.Errorf("unrecoverable connect error %w", actualErr) } } - backoff.Wait() + backoff.wait() } else { // We are connected, we can leave the retry loop break }