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
This commit is contained in:
genofire 2019-06-22 11:13:33 +02:00 committed by Mickaël Rémond
parent 145fce6b3f
commit d9fdff0839
28 changed files with 299 additions and 225 deletions

View File

@ -83,7 +83,7 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
return 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 { switch info.Node {
case "": case "":
@ -192,7 +192,7 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
if pubsub.Publish.XMLName.Local == "publish" { if pubsub.Publish.XMLName.Local == "publish" {
// Prepare pubsub IQ reply // 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{ payload := xmpp.PubSub{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "http://jabber.org/protocol/pubsub", Space: "http://jabber.org/protocol/pubsub",
@ -201,7 +201,7 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) {
} }
iqResp.Payload = &payload iqResp.Payload = &payload
// Wrap the reply in delegation 'forward' // 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{ delegPayload := xmpp.Delegation{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "urn:xmpp:delegation:1", Space: "urn:xmpp:delegation:1",

View File

@ -8,3 +8,5 @@ require (
github.com/processone/soundcloud v1.0.0 github.com/processone/soundcloud v1.0.0
gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52 gosrc.io/xmpp v0.1.1-0.20190619120342-a6cbc0c08f52
) )
replace gosrc.io/xmpp => gosrc.io/xmpp v0.1.1-0.20190619153249-b1dde2330764

View File

@ -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/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 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.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=

View File

@ -59,7 +59,7 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
return 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{ identity := xmpp.Identity{
Name: opts.Name, Name: opts.Name,
Category: opts.Category, Category: opts.Category,
@ -95,7 +95,7 @@ func discoItems(c xmpp.Sender, p xmpp.Packet) {
return 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 var payload xmpp.DiscoItems
if discoItems.Node == "" { if discoItems.Node == "" {
@ -116,7 +116,7 @@ func handleVersion(c xmpp.Sender, p xmpp.Packet) {
return 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 var payload xmpp.Version
payload.Name = "Fluux XMPP Component" payload.Name = "Fluux XMPP Component"
payload.Version = "0.0.1" payload.Version = "0.0.1"

View File

@ -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) _, _ = 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) _ = s.Send(reply)
} }

View File

@ -34,6 +34,7 @@ func main() {
Address: *address, Address: *address,
Jid: *jid, Jid: *jid,
Password: *password, Password: *password,
// PacketLogger: os.Stdout,
Insecure: true, Insecure: true,
} }
@ -91,7 +92,7 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) {
playSCURL(player, url) playSCURL(player, url)
setResponse := new(xmpp.ControlSetResponse) setResponse := new(xmpp.ControlSetResponse)
// FIXME: Broken // 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) _ = s.Send(reply)
// TODO add Soundclound artist / title retrieval // TODO add Soundclound artist / title retrieval
sendUserTune(s, "Radiohead", "Spectre") 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) { func sendUserTune(s xmpp.Sender, artist string, title string) {
tune := xmpp.Tune{Artist: artist, Title: title} 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}}} payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
iq.Payload = &payload iq.Payload = &payload
_ = s.Send(iq) _ = s.Send(iq)

View File

@ -33,7 +33,7 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password
fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc) fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='PLAIN'>%s</auth>", nsSASL, enc)
// Next message should be either success or failure. // Next message should be either success or failure.
val, err := next(decoder) val, err := nextPacket(decoder)
if err != nil { if err != nil {
return err return err
} }

View File

@ -13,7 +13,7 @@ It can be used in several ways:
- Using ticker channel to trigger callback function on tick - Using ticker channel to trigger callback function on tick
The functions for Backoff are not threadsafe, but you can: 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. - Use lock in your own code to protect the Backoff structure.
TODO: Implement Backoff Ticker channel TODO: Implement Backoff Ticker channel
@ -34,11 +34,11 @@ const (
defaultCap int = 180000 // 3 minutes 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 // performed. The structure is used to support exponential backoff on
// connection attempts to avoid hammering the server we are connecting // connection attempts to avoid hammering the server we are connecting
// to. // to.
type Backoff struct { type backoff struct {
NoJitter bool NoJitter bool
Base int Base int
Factor int Factor int
@ -47,20 +47,20 @@ type Backoff struct {
attempt int attempt int
} }
// Duration returns the duration to apply to the current attempt. // duration returns the duration to apply to the current attempt.
func (b *Backoff) Duration() time.Duration { func (b *backoff) duration() time.Duration {
d := b.DurationForAttempt(b.attempt) d := b.durationForAttempt(b.attempt)
b.attempt++ b.attempt++
return d return d
} }
// Wait sleeps for backoff duration for current attempt. // wait sleeps for backoff duration for current attempt.
func (b *Backoff) Wait() { func (b *backoff) wait() {
time.Sleep(b.Duration()) time.Sleep(b.duration())
} }
// DurationForAttempt returns a duration for an attempt number, in a stateless way. // durationForAttempt returns a duration for an attempt number, in a stateless way.
func (b *Backoff) DurationForAttempt(attempt int) time.Duration { func (b *backoff) durationForAttempt(attempt int) time.Duration {
b.setDefault() b.setDefault()
expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt))) expBackoff := math.Min(float64(b.Cap), float64(b.Base)*math.Pow(float64(b.Factor), float64(b.attempt)))
d := int(math.Trunc(expBackoff)) d := int(math.Trunc(expBackoff))
@ -70,13 +70,13 @@ func (b *Backoff) DurationForAttempt(attempt int) time.Duration {
return time.Duration(d) * time.Millisecond 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. // to reset the exponential backoff interval.
func (b *Backoff) Reset() { func (b *backoff) reset() {
b.attempt = 0 b.attempt = 0
} }
func (b *Backoff) setDefault() { func (b *backoff) setDefault() {
if b.Base == 0 { if b.Base == 0 {
b.Base = defaultBase b.Base = defaultBase
} }

View File

@ -1,21 +1,19 @@
package xmpp_test package xmpp
import ( import (
"testing" "testing"
"time" "time"
"gosrc.io/xmpp"
) )
func TestDurationForAttempt_NoJitter(t *testing.T) { 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 bInMS := time.Duration(b.Base) * time.Millisecond
if b.DurationForAttempt(0) != bInMS { if b.durationForAttempt(0) != bInMS {
t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.DurationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond) t.Errorf("incorrect default duration for attempt #0 (%d) = %d", b.durationForAttempt(0)/time.Millisecond, bInMS/time.Millisecond)
} }
var prevDuration, d time.Duration var prevDuration, d time.Duration
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
d = b.DurationForAttempt(i) d = b.durationForAttempt(i)
if !(d >= prevDuration) { if !(d >= prevDuration) {
t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration) t.Errorf("duration should be increasing between attempts. #%d (%d) > %d", i, d, prevDuration)
} }

View File

@ -54,14 +54,14 @@ func (c *ServerCheck) Check() error {
} }
// Set xml decoder and extract streamID from reply (not used for now) // Set xml decoder and extract streamID from reply (not used for now)
_, err = initDecoder(decoder) _, err = initStream(decoder)
if err != nil { if err != nil {
return err return err
} }
// extract stream features // extract stream features
var f StreamFeatures var f StreamFeatures
packet, err := next(decoder) packet, err := nextPacket(decoder)
if err != nil { if err != nil {
err = fmt.Errorf("stream open decode features: %s", err) err = fmt.Errorf("stream open decode features: %s", err)
return err return err

View File

@ -200,7 +200,7 @@ func (c *Client) SendRaw(packet string) error {
// Loop: Receive data from server // Loop: Receive data from server
func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) { func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
for { for {
val, err := next(c.Session.decoder) val, err := nextPacket(c.Session.decoder)
if err != nil { if err != nil {
close(keepaliveQuit) close(keepaliveQuit)
c.updateState(StateDisconnected) c.updateState(StateDisconnected)

View File

@ -78,7 +78,7 @@ func (c *Component) Connect() error {
c.decoder = xml.NewDecoder(conn) c.decoder = xml.NewDecoder(conn)
// 2. Initialize xml decoder and extract streamID from reply // 2. Initialize xml decoder and extract streamID from reply
streamId, err := initDecoder(c.decoder) streamId, err := initStream(c.decoder)
if err != nil { if err != nil {
return errors.New("cannot init decoder " + err.Error()) return errors.New("cannot init decoder " + err.Error())
} }
@ -89,7 +89,7 @@ func (c *Component) Connect() error {
} }
// 4. Check server response for authentication // 4. Check server response for authentication
val, err := next(c.decoder) val, err := nextPacket(c.decoder)
if err != nil { if err != nil {
return err return err
} }
@ -119,7 +119,7 @@ func (c *Component) SetHandler(handler EventHandler) {
// Receiver Go routine receiver // Receiver Go routine receiver
func (c *Component) recv() (err error) { func (c *Component) recv() (err error) {
for { for {
val, err := next(c.decoder) val, err := nextPacket(c.decoder)
if err != nil { if err != nil {
c.updateState(StateDisconnected) c.updateState(StateDisconnected)
return err return err

16
doc.go
View File

@ -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 automation (like for example monitoring of an XMPP service),
- For building connected "things" by plugging them on an XMPP server, - For building connected "things" by plugging them on an XMPP server,
- For writing simple chatbots to control a service or a thing. - 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 Clients

118
error.go Normal file
View File

@ -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})
}

13
error_enum.go Normal file
View File

@ -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"
)

141
iq.go
View File

@ -3,127 +3,20 @@ package xmpp
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"strconv"
) )
/* /*
TODO support ability to put Raw payload inside IQ 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 Packet
// IQ implements RFC 6120 - A.5 Client Namespace (a part)
type IQ struct { // Info/Query type IQ struct { // Info/Query
XMLName xml.Name `xml:"iq"` XMLName xml.Name `xml:"iq"`
PacketAttrs // MUST have a ID
Attrs
// We can only have one payload on IQ: // We can only have one payload on IQ:
// "An IQ stanza of type "get" or "set" MUST contain exactly one // "An IQ stanza of type "get" or "set" MUST contain exactly one
// child element, which specifies the semantics of the particular // child element, which specifies the semantics of the particular
@ -133,16 +26,16 @@ type IQ struct { // Info/Query
RawXML string `xml:",innerxml"` 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{ return IQ{
XMLName: xml.Name{Local: "iq"}, XMLName: xml.Name{Local: "iq"},
PacketAttrs: PacketAttrs{ Attrs: a,
Id: id,
From: from,
To: to,
Type: iqtype,
Lang: lang,
},
} }
} }
@ -182,7 +75,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.Id = attr.Value iq.Id = attr.Value
} }
if attr.Name.Local == "type" { if attr.Name.Local == "type" {
iq.Type = attr.Value iq.Type = StanzaType(attr.Value)
} }
if attr.Name.Local == "to" { if attr.Name.Local == "to" {
iq.To = attr.Value iq.To = attr.Value
@ -190,9 +83,6 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if attr.Name.Local == "from" { if attr.Name.Local == "from" {
iq.From = attr.Value iq.From = attr.Value
} }
if attr.Name.Local == "lang" {
iq.Lang = attr.Value
}
} }
// decode inner elements // decode inner elements
@ -223,6 +113,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
iq.Payload = iqExt iq.Payload = iqExt
continue continue
} }
// TODO: If unknown decode as generic node
return fmt.Errorf("unexpected element in iq: %s %s", tt.Name.Space, tt.Name.Local) return fmt.Errorf("unexpected element in iq: %s %s", tt.Name.Space, tt.Name.Local)
case xml.EndElement: case xml.EndElement:
if tt == start.End() { if tt == start.End() {
@ -233,11 +124,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
} }
// ============================================================================ // ============================================================================
// Generic IQ Payload // Generic / unknown content
type IQPayload interface {
Namespace() string
}
// Node is a generic structure to represent XML data. It is used to parse // Node is a generic structure to represent XML data. It is used to parse
// unreferenced or custom stanza payload. // unreferenced or custom stanza payload.

View File

@ -16,7 +16,7 @@ func TestUnmarshalIqs(t *testing.T) {
parsedIQ xmpp.IQ parsedIQ xmpp.IQ
}{ }{
{"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>", {"<iq id=\"1\" type=\"set\" to=\"test@localhost\"/>",
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 xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", IQ{XMLName: xml.Name{Space: "jabber:client", Local: "iq"}, PacketAttrs: PacketAttrs{To: "test@localhost", From: "server", Type: "set", Id: "2"}, Payload: cs1}}, //{"<iq xmlns=\"jabber:client\" id=\"2\" type=\"set\" to=\"test@localhost\" from=\"server\"><set xmlns=\"urn:xmpp:iot:control\"/></iq>", 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) { 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{ payload := xmpp.DiscoInfo{
Identity: xmpp.Identity{ Identity: xmpp.Identity{
Name: "Test Gateway", Name: "Test Gateway",
@ -93,7 +93,7 @@ func TestErrorTag(t *testing.T) {
} }
func TestDiscoItems(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{ payload := xmpp.DiscoItems{
Node: "music", Node: "music",
} }

View File

@ -7,9 +7,11 @@ import (
// ============================================================================ // ============================================================================
// Message Packet // Message Packet
// Message implements RFC 6120 - A.5 Client Namespace (a part)
type Message struct { type Message struct {
XMLName xml.Name `xml:"message"` XMLName xml.Name `xml:"message"`
PacketAttrs Attrs
Subject string `xml:"subject,omitempty"` Subject string `xml:"subject,omitempty"`
Body string `xml:"body,omitempty"` Body string `xml:"body,omitempty"`
Thread string `xml:"thread,omitempty"` Thread string `xml:"thread,omitempty"`
@ -21,16 +23,10 @@ func (Message) Name() string {
return "message" return "message"
} }
func NewMessage(msgtype, from, to, id, lang string) Message { func NewMessage(a Attrs) Message {
return Message{ return Message{
XMLName: xml.Name{Local: "message"}, XMLName: xml.Name{Local: "message"},
PacketAttrs: PacketAttrs{ Attrs: a,
Id: id,
From: from,
To: to,
Type: msgtype,
Lang: lang,
},
} }
} }
@ -63,7 +59,7 @@ func (msg *Message) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
msg.Id = attr.Value msg.Id = attr.Value
} }
if attr.Name.Local == "type" { if attr.Name.Local == "type" {
msg.Type = attr.Value msg.Type = StanzaType(attr.Value)
} }
if attr.Name.Local == "to" { if attr.Name.Local == "to" {
msg.To = attr.Value msg.To = attr.Value

View File

@ -9,7 +9,7 @@ import (
) )
func TestGenerateMessage(t *testing.T) { 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.Body = "Hi"
message.Subject = "Msg Subject" message.Subject = "Msg Subject"

View File

@ -4,13 +4,13 @@ type Packet interface {
Name() string Name() string
} }
// PacketAttrs represents the common structure for base XMPP packets. // Attrs represents the common structure for base XMPP packets.
type PacketAttrs struct { type Attrs struct {
Id string `xml:"id,attr,omitempty"` Type StanzaType `xml:"type,attr,omitempty"`
From string `xml:"from,attr,omitempty"` Id string `xml:"id,attr,omitempty"`
To string `xml:"to,attr,omitempty"` From string `xml:"from,attr,omitempty"`
Type string `xml:"type,attr,omitempty"` To string `xml:"to,attr,omitempty"`
Lang string `xml:"lang,attr,omitempty"` Lang string `xml:"lang,attr,omitempty"`
} }
type packetFormatter interface { type packetFormatter interface {

25
packet_enum.go Normal file
View File

@ -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"
)

View File

@ -14,29 +14,29 @@ import (
// reattach features (allowing to resume an existing stream at the point the connection was interrupted, without // reattach features (allowing to resume an existing stream at the point the connection was interrupted, without
// getting through the authentication process. // getting through the authentication process.
// TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> ) // TODO We should handle stream error from XEP-0114 ( <conflict/> or <host-unknown/> )
func initDecoder(p *xml.Decoder) (sessionID string, err error) { func initStream(p *xml.Decoder) (sessionID string, err error) {
for { for {
var t xml.Token var t xml.Token
t, err = p.Token() t, err = p.Token()
if err != nil { if err != nil {
return return sessionID, err
} }
switch elem := t.(type) { switch elem := t.(type) {
case xml.StartElement: case xml.StartElement:
if elem.Name.Space != NSStream || elem.Name.Local != "stream" { if elem.Name.Space != NSStream || elem.Name.Local != "stream" {
err = errors.New("xmpp: expected <stream> but got <" + elem.Name.Local + "> in " + elem.Name.Space) err = errors.New("xmpp: expected <stream> 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 { for _, attrs := range elem.Attr {
switch attrs.Name.Local { switch attrs.Name.Local {
case "id": case "id":
sessionID = attrs.Value 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 // nextPacket scans XML token stream for next complete XMPP stanza.
// that elements. // 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 // 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 // Read start element to find out how we want to parse the XMPP packet
se, err := nextStart(p) se, err := nextStart(p)
if err != nil { 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) { func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local { switch se.Name.Local {
case "error": 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) { func decodeSASL(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local { switch se.Name.Local {
case "success": 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) { func decodeClient(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local { switch se.Name.Local {
case "message": 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) { func decodeComponent(p *xml.Decoder, se xml.StartElement) (Packet, error) {
switch se.Name.Local { switch se.Name.Local {
case "handshake": case "handshake": // handshake is used to authenticate components
return handshake.decode(p, se) return handshake.decode(p, se)
case "message": case "message":
return message.decode(p, se) return message.decode(p, se)

View File

@ -5,28 +5,24 @@ import "encoding/xml"
// ============================================================================ // ============================================================================
// Presence Packet // Presence Packet
// Presence implements RFC 6120 - A.5 Client Namespace (a part)
type Presence struct { type Presence struct {
XMLName xml.Name `xml:"presence"` XMLName xml.Name `xml:"presence"`
PacketAttrs Attrs
Show string `xml:"show,omitempty"` // away, chat, dnd, xa Show PresenceShow `xml:"show,omitempty"`
Status string `xml:"status,omitempty"` Status string `xml:"status,omitempty"`
Priority int `xml:"priority,omitempty"` Priority int8 `xml:"priority,omitempty"` // default: 0
Error Err `xml:"error,omitempty"` Error Err `xml:"error,omitempty"`
} }
func (Presence) Name() string { func (Presence) Name() string {
return "presence" return "presence"
} }
func NewPresence(from, to, id, lang string) Presence { func NewPresence(a Attrs) Presence {
return Presence{ return Presence{
XMLName: xml.Name{Local: "presence"}, XMLName: xml.Name{Local: "presence"},
PacketAttrs: PacketAttrs{ Attrs: a,
Id: id,
From: from,
To: to,
Lang: lang,
},
} }
} }

12
presence_enum.go Normal file
View File

@ -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"
)

View File

@ -10,8 +10,8 @@ import (
) )
func TestGeneratePresence(t *testing.T) { func TestGeneratePresence(t *testing.T) {
presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en") presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = "chat" presence.Show = xmpp.PresenceShowChat
data, err := xml.Marshal(presence) data, err := xml.Marshal(presence)
if err != nil { 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 // Test structure to ensure that show, status and priority are correctly defined as presence
// package sub-elements // package sub-elements
type pres struct { type pres struct {
Show string `xml:"show"` Show xmpp.PresenceShow `xml:"show"`
Status string `xml:"status"` Status string `xml:"status"`
Priority int `xml:"priority"` Priority int8 `xml:"priority"`
} }
presence := xmpp.NewPresence("admin@localhost", "test@localhost", "1", "en") presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"})
presence.Show = "xa" presence.Show = xmpp.PresenceShowXA
presence.Status = "Coding" presence.Status = "Coding"
presence.Priority = 10 presence.Priority = 10

View File

@ -19,8 +19,7 @@ func TestNameMatcher(t *testing.T) {
// Check that a message packet is properly matched // Check that a message packet is properly matched
conn := NewSenderMock() conn := NewSenderMock()
// TODO: We want packet creation code to use struct to use default values msg := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, To: "test@localhost", Id: "1"})
msg := xmpp.NewMessage("chat", "", "test@localhost", "1", "")
msg.Body = "Hello" msg.Body = "Hello"
router.Route(conn, msg) router.Route(conn, msg)
if conn.String() != successFlag { if conn.String() != successFlag {
@ -29,7 +28,7 @@ func TestNameMatcher(t *testing.T) {
// Check that an IQ packet is not matched // Check that an IQ packet is not matched
conn = NewSenderMock() conn = NewSenderMock()
iq := xmpp.NewIQ("get", "", "localhost", "1", "") iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, To: "localhost", Id: "1"})
iq.Payload = &xmpp.DiscoInfo{} iq.Payload = &xmpp.DiscoInfo{}
router.Route(conn, iq) router.Route(conn, iq)
if conn.String() == successFlag { if conn.String() == successFlag {
@ -47,7 +46,8 @@ func TestIQNSMatcher(t *testing.T) {
// Check that an IQ with proper namespace does match // Check that an IQ with proper namespace does match
conn := NewSenderMock() 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{ iqDisco.Payload = &xmpp.DiscoInfo{
XMLName: xml.Name{ XMLName: xml.Name{
Space: xmpp.NSDiscoInfo, Space: xmpp.NSDiscoInfo,
@ -60,7 +60,8 @@ func TestIQNSMatcher(t *testing.T) {
// Check that another namespace is not matched // Check that another namespace is not matched
conn = NewSenderMock() 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{ iqVersion.Payload = &xmpp.DiscoInfo{
XMLName: xml.Name{ XMLName: xml.Name{
Space: "jabber:iq:version", Space: "jabber:iq:version",
@ -240,7 +241,7 @@ func (s SenderMock) String() string {
func TestSenderMock(t *testing.T) { func TestSenderMock(t *testing.T) {
conn := NewSenderMock() conn := NewSenderMock()
msg := xmpp.NewMessage("", "", "test@localhost", "1", "") msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost", Id: "1"})
msg.Body = "Hello" msg.Body = "Hello"
if err := conn.Send(msg); err != nil { if err := conn.Send(msg); err != nil {
t.Error("Could not send message") t.Error("Could not send message")

View File

@ -92,7 +92,7 @@ func (s *Session) open(domain string) (f StreamFeatures) {
} }
// Set xml decoder and extract streamID from reply // 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 { if s.err != nil {
return return
} }

View File

@ -104,7 +104,7 @@ func (sm *StreamManager) Stop() {
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server. // connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
func (sm *StreamManager) connect() error { 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 { for {
var err error var err error
@ -118,7 +118,7 @@ func (sm *StreamManager) connect() error {
return xerrors.Errorf("unrecoverable connect error %w", actualErr) return xerrors.Errorf("unrecoverable connect error %w", actualErr)
} }
} }
backoff.Wait() backoff.wait()
} else { // We are connected, we can leave the retry loop } else { // We are connected, we can leave the retry loop
break break
} }