From 428787d7abbe86a00c840e0e87581365ca75189a Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Wed, 26 Jun 2019 17:14:52 +0200 Subject: [PATCH 1/9] Refactor and move parsing and stanza to a separate package --- README.md | 7 +- _examples/delegation/delegation.go | 71 ++++++------ _examples/xmpp_component/xmpp_component.go | 51 ++++----- _examples/xmpp_echo/xmpp_echo.go | 7 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 25 ++--- auth.go | 81 ++------------ check_cert.go | 20 ++-- client.go | 8 +- client_test.go | 18 ++-- component.go | 102 ++---------------- component_test.go | 74 ------------- router.go | 48 +++++---- router_test.go | 64 +++++------ session.go | 30 +++--- stanza/auth_sasl.go | 72 +++++++++++++ stanza/component.go | 89 +++++++++++++++ stanza/component_test.go | 79 ++++++++++++++ error.go => stanza/error.go | 6 +- error_enum.go => stanza/error_enum.go | 2 +- iot_control.go => stanza/iot.go | 2 +- iot_control_test.go => stanza/iot_test.go | 2 +- iq.go => stanza/iq.go | 50 +-------- iq_test.go => stanza/iq_test.go | 39 ++++--- message.go => stanza/message.go | 2 +- message_test.go => stanza/message_test.go | 17 ++- .../msg_chat_markers.go | 6 +- msg_chat_state.go => stanza/msg_chat_state.go | 6 +- msg_html.go => stanza/msg_html.go | 6 +- msg_html_test.go => stanza/msg_html_test.go | 14 ++- msg_oob.go => stanza/msg_oob.go | 6 +- msg_receipts.go => stanza/msg_receipts.go | 6 +- .../msg_receipts_test.go | 8 +- stanza/node.go | 51 +++++++++ ns.go => stanza/ns.go | 8 +- packet.go => stanza/packet.go | 2 +- packet_enum.go => stanza/packet_enum.go | 2 +- parser.go => stanza/parser.go | 48 ++++----- pep.go => stanza/pep.go | 2 +- pres_muc.go => stanza/pres_muc.go | 2 +- pres_muc_test.go => stanza/pres_muc_test.go | 12 +-- presence.go => stanza/presence.go | 2 +- presence_enum.go => stanza/presence_enum.go | 2 +- presence_test.go => stanza/presence_test.go | 20 ++-- pubsub.go => stanza/pubsub.go | 2 +- registry.go => stanza/registry.go | 2 +- registry_test.go => stanza/registry_test.go | 2 +- starttls.go => stanza/starttls.go | 4 +- stream.go => stanza/stream.go | 2 +- xmpp_test.go => stanza/xmpp_test.go | 2 +- stream_manager.go | 3 +- stream_test.go | 8 +- 51 files changed, 614 insertions(+), 580 deletions(-) create mode 100644 stanza/auth_sasl.go create mode 100644 stanza/component.go create mode 100644 stanza/component_test.go rename error.go => stanza/error.go (97%) rename error_enum.go => stanza/error_enum.go (95%) rename iot_control.go => stanza/iot.go (97%) rename iot_control_test.go => stanza/iot_test.go (97%) rename iq.go => stanza/iq.go (76%) rename iq_test.go => stanza/iq_test.go (83%) rename message.go => stanza/message.go (99%) rename message_test.go => stanza/message_test.go (82%) rename msg_chat_markers.go => stanza/msg_chat_markers.go (96%) rename msg_chat_state.go => stanza/msg_chat_state.go (96%) rename msg_html.go => stanza/msg_html.go (92%) rename msg_html_test.go => stanza/msg_html_test.go (82%) rename msg_oob.go => stanza/msg_oob.go (88%) rename msg_receipts.go => stanza/msg_receipts.go (94%) rename msg_receipts_test.go => stanza/msg_receipts_test.go (90%) create mode 100644 stanza/node.go rename ns.go => stanza/ns.go (52%) rename packet.go => stanza/packet.go (96%) rename packet_enum.go => stanza/packet_enum.go (98%) rename parser.go => stanza/parser.go (90%) rename pep.go => stanza/pep.go (98%) rename pres_muc.go => stanza/pres_muc.go (98%) rename pres_muc_test.go => stanza/pres_muc_test.go (89%) rename presence.go => stanza/presence.go (99%) rename presence_enum.go => stanza/presence_enum.go (95%) rename presence_test.go => stanza/presence_test.go (73%) rename pubsub.go => stanza/pubsub.go (98%) rename registry.go => stanza/registry.go (99%) rename registry_test.go => stanza/registry_test.go (98%) rename starttls.go => stanza/starttls.go (88%) rename stream.go => stanza/stream.go (99%) rename xmpp_test.go => stanza/xmpp_test.go (97%) diff --git a/README.md b/README.md index accab4a..8305327 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ import ( "os" "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" ) func main() { @@ -57,15 +58,15 @@ func main() { log.Fatal(cm.Run()) } -func handleMessage(s xmpp.Sender, p xmpp.Packet) { - msg, ok := p.(xmpp.Message) +func handleMessage(s xmpp.Sender, p stanza.Packet) { + msg, ok := p.(stanza.Message) if !ok { _, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p) return } _, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From) - reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body} + reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body} _ = s.Send(reply) } ``` diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index 473fefa..9e26b69 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -6,6 +6,7 @@ import ( "log" "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" ) func main() { @@ -23,8 +24,8 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) router.NewRoute(). - IQNamespaces(xmpp.NSDiscoInfo). - HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + IQNamespaces(stanza.NSDiscoInfo). + HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { discoInfo(s, p, opts) }) router.NewRoute(). @@ -43,14 +44,14 @@ func main() { log.Fatal(cm.Run()) } -func handleMessage(_ xmpp.Sender, p xmpp.Packet) { - msg, ok := p.(xmpp.Message) +func handleMessage(_ xmpp.Sender, p stanza.Packet) { + msg, ok := p.(stanza.Message) if !ok { return } var msgProcessed bool for _, ext := range msg.Extensions { - delegation, ok := ext.(*xmpp.Delegation) + delegation, ok := ext.(*stanza.Delegation) if ok { msgProcessed = true fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace) @@ -72,18 +73,18 @@ const ( // TODO: replace xmpp.Sender by ctx xmpp.Context ? // ctx.Stream.Send / SendRaw // ctx.Opts -func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) { +func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { // Type conversion & sanity checks - iq, ok := p.(xmpp.IQ) + iq, ok := p.(stanza.IQ) if !ok { return } - info, ok := iq.Payload.(*xmpp.DiscoInfo) + info, ok := iq.Payload.(*stanza.DiscoInfo) if !ok { return } - iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) + iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) switch info.Node { case "": @@ -97,22 +98,22 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) { _ = c.Send(iqResp) } -func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) { +func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) { // Higher level discovery - identity := xmpp.Identity{ + identity := stanza.Identity{ Name: opts.Name, Category: opts.Category, Type: opts.Type, } - payload := xmpp.DiscoInfo{ + payload := stanza.DiscoInfo{ XMLName: xml.Name{ - Space: xmpp.NSDiscoInfo, + Space: stanza.NSDiscoInfo, Local: "query", }, Identity: identity, - Features: []xmpp.Feature{ - {Var: xmpp.NSDiscoInfo}, - {Var: xmpp.NSDiscoItems}, + Features: []stanza.Feature{ + {Var: stanza.NSDiscoInfo}, + {Var: stanza.NSDiscoItems}, {Var: "jabber:iq:version"}, {Var: "urn:xmpp:delegation:1"}, }, @@ -120,14 +121,14 @@ func discoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) { iqResp.Payload = &payload } -func discoInfoPubSub(iqResp *xmpp.IQ) { - payload := xmpp.DiscoInfo{ +func discoInfoPubSub(iqResp *stanza.IQ) { + payload := stanza.DiscoInfo{ XMLName: xml.Name{ - Space: xmpp.NSDiscoInfo, + Space: stanza.NSDiscoInfo, Local: "query", }, Node: pubsubNode, - Features: []xmpp.Feature{ + Features: []stanza.Feature{ {Var: "http://jabber.org/protocol/pubsub"}, {Var: "http://jabber.org/protocol/pubsub#publish"}, {Var: "http://jabber.org/protocol/pubsub#subscribe"}, @@ -137,19 +138,19 @@ func discoInfoPubSub(iqResp *xmpp.IQ) { iqResp.Payload = &payload } -func discoInfoPEP(iqResp *xmpp.IQ) { - identity := xmpp.Identity{ +func discoInfoPEP(iqResp *stanza.IQ) { + identity := stanza.Identity{ Category: "pubsub", Type: "pep", } - payload := xmpp.DiscoInfo{ + payload := stanza.DiscoInfo{ XMLName: xml.Name{ - Space: xmpp.NSDiscoInfo, + Space: stanza.NSDiscoInfo, Local: "query", }, Identity: identity, Node: pepNode, - Features: []xmpp.Feature{ + Features: []stanza.Feature{ {Var: "http://jabber.org/protocol/pubsub#access-presence"}, {Var: "http://jabber.org/protocol/pubsub#auto-create"}, {Var: "http://jabber.org/protocol/pubsub#auto-subscribe"}, @@ -166,25 +167,25 @@ func discoInfoPEP(iqResp *xmpp.IQ) { iqResp.Payload = &payload } -func handleDelegation(s xmpp.Sender, p xmpp.Packet) { +func handleDelegation(s xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks - iq, ok := p.(xmpp.IQ) + iq, ok := p.(stanza.IQ) if !ok { return } - delegation, ok := iq.Payload.(*xmpp.Delegation) + delegation, ok := iq.Payload.(*stanza.Delegation) if !ok { return } forwardedPacket := delegation.Forwarded.Stanza fmt.Println(forwardedPacket) - forwardedIQ, ok := forwardedPacket.(xmpp.IQ) + forwardedIQ, ok := forwardedPacket.(stanza.IQ) if !ok { return } - pubsub, ok := forwardedIQ.Payload.(*xmpp.PubSub) + pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub) if !ok { // We only support pubsub delegation return @@ -192,8 +193,8 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) { if pubsub.Publish.XMLName.Local == "publish" { // Prepare pubsub IQ reply - iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) - payload := xmpp.PubSub{ + iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) + payload := stanza.PubSub{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/pubsub", Local: "pubsub", @@ -201,13 +202,13 @@ func handleDelegation(s xmpp.Sender, p xmpp.Packet) { } iqResp.Payload = &payload // Wrap the reply in delegation 'forward' - iqForward := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) - delegPayload := xmpp.Delegation{ + iqForward := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) + delegPayload := stanza.Delegation{ XMLName: xml.Name{ Space: "urn:xmpp:delegation:1", Local: "delegation", }, - Forwarded: &xmpp.Forwarded{ + Forwarded: &stanza.Forwarded{ XMLName: xml.Name{ Space: "urn:xmpp:forward:0", Local: "forward", diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index 17cfc1a..f07704b 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -6,6 +6,7 @@ import ( "log" "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" ) func main() { @@ -21,12 +22,12 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) router.NewRoute(). - IQNamespaces(xmpp.NSDiscoInfo). - HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + IQNamespaces(stanza.NSDiscoInfo). + HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { discoInfo(s, p, opts) }) router.NewRoute(). - IQNamespaces(xmpp.NSDiscoItems). + IQNamespaces(stanza.NSDiscoItems). HandlerFunc(discoItems) router.NewRoute(). IQNamespaces("jabber:iq:version"). @@ -44,36 +45,36 @@ func main() { log.Fatal(cm.Run()) } -func handleMessage(_ xmpp.Sender, p xmpp.Packet) { - msg, ok := p.(xmpp.Message) +func handleMessage(_ xmpp.Sender, p stanza.Packet) { + msg, ok := p.(stanza.Message) if !ok { return } fmt.Println("Received message:", msg.Body) } -func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) { +func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { // Type conversion & sanity checks - iq, ok := p.(xmpp.IQ) + iq, ok := p.(stanza.IQ) if !ok || iq.Type != "get" { return } - iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) - identity := xmpp.Identity{ + iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + identity := stanza.Identity{ Name: opts.Name, Category: opts.Category, Type: opts.Type, } - payload := xmpp.DiscoInfo{ + payload := stanza.DiscoInfo{ XMLName: xml.Name{ - Space: xmpp.NSDiscoInfo, + Space: stanza.NSDiscoInfo, Local: "query", }, Identity: identity, - Features: []xmpp.Feature{ - {Var: xmpp.NSDiscoInfo}, - {Var: xmpp.NSDiscoItems}, + Features: []stanza.Feature{ + {Var: stanza.NSDiscoInfo}, + {Var: stanza.NSDiscoItems}, {Var: "jabber:iq:version"}, {Var: "urn:xmpp:delegation:1"}, }, @@ -83,24 +84,24 @@ func discoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) { } // TODO: Handle iq error responses -func discoItems(c xmpp.Sender, p xmpp.Packet) { +func discoItems(c xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks - iq, ok := p.(xmpp.IQ) + iq, ok := p.(stanza.IQ) if !ok || iq.Type != "get" { return } - discoItems, ok := iq.Payload.(*xmpp.DiscoItems) + discoItems, ok := iq.Payload.(*stanza.DiscoItems) if !ok { return } - iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) - var payload xmpp.DiscoItems + var payload stanza.DiscoItems if discoItems.Node == "" { - payload = xmpp.DiscoItems{ - Items: []xmpp.DiscoItem{ + payload = stanza.DiscoItems{ + Items: []stanza.DiscoItem{ {Name: "test node", JID: "service.localhost", Node: "node1"}, }, } @@ -109,15 +110,15 @@ func discoItems(c xmpp.Sender, p xmpp.Packet) { _ = c.Send(iqResp) } -func handleVersion(c xmpp.Sender, p xmpp.Packet) { +func handleVersion(c xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks - iq, ok := p.(xmpp.IQ) + iq, ok := p.(stanza.IQ) if !ok { return } - iqResp := xmpp.NewIQ(xmpp.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) - var payload xmpp.Version + iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + var payload stanza.Version payload.Name = "Fluux XMPP Component" payload.Version = "0.0.1" iq.Payload = &payload diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go index 3725f08..b4da207 100644 --- a/_examples/xmpp_echo/xmpp_echo.go +++ b/_examples/xmpp_echo/xmpp_echo.go @@ -10,6 +10,7 @@ import ( "os" "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" ) func main() { @@ -35,15 +36,15 @@ func main() { log.Fatal(cm.Run()) } -func handleMessage(s xmpp.Sender, p xmpp.Packet) { - msg, ok := p.(xmpp.Message) +func handleMessage(s xmpp.Sender, p stanza.Packet) { + msg, ok := p.(stanza.Message) if !ok { _, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p) return } _, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From) - reply := xmpp.Message{Attrs: xmpp.Attrs{To: msg.From}, Body: msg.Body} + reply := stanza.Message{Attrs: stanza.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 2218279..02e2c76 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -12,6 +12,7 @@ import ( "github.com/processone/mpg123" "github.com/processone/soundcloud" "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" ) // Get the actual song Stream URL from SoundCloud website song URL and play it with mpg123 player. @@ -41,12 +42,12 @@ func main() { router := xmpp.NewRouter() router.NewRoute(). Packet("message"). - HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { handleMessage(s, p, player) }) router.NewRoute(). Packet("message"). - HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { handleIQ(s, p, player) }) @@ -59,8 +60,8 @@ func main() { log.Fatal(cm.Run()) } -func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) { - msg, ok := p.(xmpp.Message) +func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { + msg, ok := p.(stanza.Message) if !ok { return } @@ -73,14 +74,14 @@ func handleMessage(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) { } } -func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) { - iq, ok := p.(xmpp.IQ) +func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { + iq, ok := p.(stanza.IQ) if !ok { return } switch payload := iq.Payload.(type) { // We support IOT Control IQ - case *xmpp.ControlSet: + case *stanza.ControlSet: var url string for _, element := range payload.Fields { if element.XMLName.Local == "string" && element.Name == "url" { @@ -90,9 +91,9 @@ func handleIQ(s xmpp.Sender, p xmpp.Packet, player *mpg123.Player) { } playSCURL(player, url) - setResponse := new(xmpp.ControlSetResponse) + setResponse := new(stanza.ControlSetResponse) // FIXME: Broken - reply := xmpp.IQ{Attrs: xmpp.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse} + reply := stanza.IQ{Attrs: stanza.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,9 +103,9 @@ 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(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}}} + tune := stanza.Tune{Artist: artist, Title: title} + iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"}) + payload := stanza.PubSub{Publish: &stanza.Publish{Node: "http://jabber.org/protocol/tune", Item: stanza.Item{Tune: &tune}}} iq.Payload = &payload _ = s.Send(iq) } diff --git a/auth.go b/auth.go index 497258d..926426d 100644 --- a/auth.go +++ b/auth.go @@ -6,9 +6,11 @@ import ( "errors" "fmt" "io" + + "gosrc.io/xmpp/stanza" ) -func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f StreamFeatures, user string, password string) (err error) { +func authSASL(socket io.ReadWriter, decoder *xml.Decoder, f stanza.StreamFeatures, user string, password string) (err error) { // TODO: Implement other type of SASL Authentication havePlain := false for _, m := range f.Mechanisms.Mechanism { @@ -30,17 +32,17 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password raw := "\x00" + user + "\x00" + password enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) base64.StdEncoding.Encode(enc, []byte(raw)) - fmt.Fprintf(socket, "%s", nsSASL, enc) + fmt.Fprintf(socket, "%s", stanza.NSSASL, enc) // Next message should be either success or failure. - val, err := nextPacket(decoder) + val, err := stanza.NextPacket(decoder) if err != nil { return err } switch v := val.(type) { - case SASLSuccess: - case SASLFailure: + case stanza.SASLSuccess: + case stanza.SASLFailure: // v.Any is type of sub-element in failure, which gives a description of what failed. err := errors.New("auth failure: " + v.Any.Local) return NewConnError(err, true) @@ -49,72 +51,3 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, user string, password } return err } - -// ============================================================================ -// SASLSuccess - -type SASLSuccess struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"` -} - -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"` - Value string `xml:",innerxml"` -} - -type BindBind struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` - Resource string `xml:"resource,omitempty"` - Jid string `xml:"jid,omitempty"` -} - -func (b *BindBind) Namespace() string { - return b.XMLName.Space -} - -// Session is obsolete in RFC 6121. -// Added for compliance with RFC 3121. -// Remove when ejabberd purely conforms to RFC 6121. -type sessionSession struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` - optional xml.Name // If it does exist, it mean we are not required to open session -} diff --git a/check_cert.go b/check_cert.go index 5addd87..e33d9f9 100644 --- a/check_cert.go +++ b/check_cert.go @@ -8,6 +8,8 @@ import ( "net" "strings" "time" + + "gosrc.io/xmpp/stanza" ) // TODO: Should I move this as an extension of the client? @@ -49,28 +51,28 @@ func (c *ServerCheck) Check() error { decoder := xml.NewDecoder(tcpconn) // Send stream open tag - if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, NSClient, NSStream); err != nil { + if _, err = fmt.Fprintf(tcpconn, xmppStreamOpen, c.domain, stanza.NSClient, stanza.NSStream); err != nil { return err } // Set xml decoder and extract streamID from reply (not used for now) - _, err = initStream(decoder) + _, err = stanza.InitStream(decoder) if err != nil { return err } // extract stream features - var f StreamFeatures - packet, err := nextPacket(decoder) + var f stanza.StreamFeatures + packet, err := stanza.NextPacket(decoder) if err != nil { err = fmt.Errorf("stream open decode features: %s", err) return err } switch p := packet.(type) { - case StreamFeatures: + case stanza.StreamFeatures: f = p - case StreamError: + case stanza.StreamError: return errors.New("open stream error: " + p.Error.Local) default: return errors.New("expected packet received while expecting features, got " + p.Name()) @@ -79,13 +81,13 @@ func (c *ServerCheck) Check() error { if _, ok := f.DoesStartTLS(); ok { fmt.Fprintf(tcpconn, "") - var k tlsProceed + var k stanza.TLSProceed if err = decoder.DecodeElement(&k, nil); err != nil { return fmt.Errorf("expecting starttls proceed: %s", err) } - DefaultTlsConfig.ServerName = c.domain - tlsConn := tls.Client(tcpconn, &DefaultTlsConfig) + stanza.DefaultTlsConfig.ServerName = c.domain + tlsConn := tls.Client(tcpconn, &stanza.DefaultTlsConfig) // We convert existing connection to TLS if err = tlsConn.Handshake(); err != nil { return err diff --git a/client.go b/client.go index 1a63041..2bd8224 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,8 @@ import ( "fmt" "net" "time" + + "gosrc.io/xmpp/stanza" ) //============================================================================= @@ -153,7 +155,7 @@ func (c *Client) SetHandler(handler EventHandler) { } // Send marshals XMPP stanza and sends it to the server. -func (c *Client) Send(packet Packet) error { +func (c *Client) Send(packet stanza.Packet) error { conn := c.conn if conn == nil { return errors.New("client is not connected") @@ -191,7 +193,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 := nextPacket(c.Session.decoder) + val, err := stanza.NextPacket(c.Session.decoder) if err != nil { close(keepaliveQuit) c.updateState(StateDisconnected) @@ -200,7 +202,7 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) { // Handle stream errors switch packet := val.(type) { - case StreamError: + case stanza.StreamError: c.router.route(c, val) close(keepaliveQuit) c.streamError(packet.Error.Local, packet.Text) diff --git a/client_test.go b/client_test.go index 813f2be..a599d5b 100644 --- a/client_test.go +++ b/client_test.go @@ -7,6 +7,8 @@ import ( "net" "testing" "time" + + "gosrc.io/xmpp/stanza" ) const ( @@ -126,11 +128,11 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { switch elem := token.(type) { // Wait for first startElement case xml.StartElement: - if elem.Name.Space != NSStream || elem.Name.Local != "stream" { + if elem.Name.Space != stanza.NSStream || elem.Name.Local != "stream" { err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space) return } - if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", NSClient, NSStream); err != nil { + if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil { t.Errorf("cannot write server stream open: %s", err) } return @@ -152,14 +154,14 @@ func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) { // TODO return err in case of error reading the auth params func readAuth(t *testing.T, decoder *xml.Decoder) string { - se, err := nextStart(decoder) + se, err := stanza.NextStart(decoder) if err != nil { t.Errorf("cannot read auth: %s", err) return "" } var nv interface{} - nv = &auth{} + nv = &stanza.Auth{} // Decode element into pointer storage if err = decoder.DecodeElement(nv, &se); err != nil { t.Errorf("cannot decode auth: %s", err) @@ -167,7 +169,7 @@ func readAuth(t *testing.T, decoder *xml.Decoder) string { } switch v := nv.(type) { - case *auth: + case *stanza.Auth: return v.Value } return "" @@ -184,13 +186,13 @@ func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { } func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := nextStart(decoder) + se, err := stanza.NextStart(decoder) if err != nil { t.Errorf("cannot read bind: %s", err) return } - iq := &IQ{} + iq := &stanza.IQ{} // Decode element into pointer storage if err = decoder.DecodeElement(&iq, &se); err != nil { t.Errorf("cannot decode bind iq: %s", err) @@ -199,7 +201,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { // TODO Check all elements switch iq.Payload.(type) { - case *BindBind: + case *stanza.BindBind: result := ` %s diff --git a/component.go b/component.go index d13822b..0176371 100644 --- a/component.go +++ b/component.go @@ -9,6 +9,8 @@ import ( "io" "net" "time" + + "gosrc.io/xmpp/stanza" ) const componentStreamOpen = "" @@ -72,13 +74,13 @@ func (c *Component) Connect() error { c.conn = conn // 1. Send stream open tag - if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, NSComponent, NSStream); err != nil { + if _, err := fmt.Fprintf(conn, componentStreamOpen, c.Domain, stanza.NSComponent, stanza.NSStream); err != nil { return errors.New("cannot send stream open " + err.Error()) } c.decoder = xml.NewDecoder(conn) // 2. Initialize xml decoder and extract streamID from reply - streamId, err := initStream(c.decoder) + streamId, err := stanza.InitStream(c.decoder) if err != nil { return errors.New("cannot init decoder " + err.Error()) } @@ -89,15 +91,15 @@ func (c *Component) Connect() error { } // 4. Check server response for authentication - val, err := nextPacket(c.decoder) + val, err := stanza.NextPacket(c.decoder) if err != nil { return err } switch v := val.(type) { - case StreamError: + case stanza.StreamError: return errors.New("handshake failed " + v.Error.Local) - case Handshake: + case stanza.Handshake: // Start the receiver go routine go c.recv() return nil @@ -119,7 +121,7 @@ func (c *Component) SetHandler(handler EventHandler) { // Receiver Go routine receiver func (c *Component) recv() (err error) { for { - val, err := nextPacket(c.decoder) + val, err := stanza.NextPacket(c.decoder) if err != nil { c.updateState(StateDisconnected) return err @@ -127,7 +129,7 @@ func (c *Component) recv() (err error) { // Handle stream errors switch p := val.(type) { - case StreamError: + case stanza.StreamError: c.router.route(c, val) c.streamError(p.Error.Local, p.Text) return errors.New("stream error: " + p.Error.Local) @@ -137,7 +139,7 @@ func (c *Component) recv() (err error) { } // Send marshalls XMPP stanza and sends it to the server. -func (c *Component) Send(packet Packet) error { +func (c *Component) Send(packet stanza.Packet) error { conn := c.conn if conn == nil { return errors.New("component is not connected") @@ -186,90 +188,6 @@ func (c *Component) handshake(streamId string) string { return encodedStr } -// ============================================================================ -// Handshake Stanza - -// Handshake is a stanza used by XMPP components to authenticate on XMPP -// component port. -type Handshake struct { - XMLName xml.Name `xml:"jabber:component:accept handshake"` - // TODO Add handshake value with test for proper serialization - // Value string `xml:",innerxml"` -} - -func (Handshake) Name() string { - return "component:handshake" -} - -// Handshake decoding wrapper - -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 -} - -// ============================================================================ -// Component delegation -// XEP-0355 - -// Delegation can be used both on message (for delegated) and IQ (for Forwarded), -// depending on the context. -type Delegation struct { - MsgExtension - XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"` - Forwarded *Forwarded // This is used in iq to wrap delegated iqs - Delegated *Delegated // This is used in a message to confirm delegated namespace -} - -func (d *Delegation) Namespace() string { - return d.XMLName.Space -} - -type Forwarded struct { - XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"` - Stanza Packet -} - -// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to -// transform generic XML content into hierarchical Node structure. -func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - // Check subelements to extract required field as boolean - for { - t, err := d.Token() - if err != nil { - return err - } - - switch tt := t.(type) { - - case xml.StartElement: - if packet, err := decodeClient(d, tt); err == nil { - f.Stanza = packet - } - - case xml.EndElement: - if tt == start.End() { - return nil - } - } - } -} - -type Delegated struct { - XMLName xml.Name `xml:"delegated"` - Namespace string `xml:"namespace,attr,omitempty"` -} - -func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{}) -} - /* TODO: Add support for discovery management directly in component TODO: Support multiple identities on disco info diff --git a/component_test.go b/component_test.go index fd7ae3c..222b471 100644 --- a/component_test.go +++ b/component_test.go @@ -1,7 +1,6 @@ package xmpp import ( - "encoding/xml" "testing" ) @@ -24,76 +23,3 @@ func TestHandshake(t *testing.T) { func TestGenerateHandshake(t *testing.T) { // TODO } - -// We should be able to properly parse delegation confirmation messages -func TestParsingDelegationMessage(t *testing.T) { - packetStr := ` - - - -` - var msg Message - data := []byte(packetStr) - if err := xml.Unmarshal(data, &msg); err != nil { - t.Errorf("Unmarshal(%s) returned error", data) - } - - // Check that we have extracted the delegation info as MsgExtension - var nsDelegated string - for _, ext := range msg.Extensions { - if delegation, ok := ext.(*Delegation); ok { - nsDelegated = delegation.Delegated.Namespace - } - } - if nsDelegated != "http://jabber.org/protocol/pubsub" { - t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg) - } -} - -// Check that we can parse a delegation IQ. -// The most important thing is to be able to -func TestParsingDelegationIQ(t *testing.T) { - packetStr := ` - - - - - - - - - - - - - - - -` - var iq IQ - data := []byte(packetStr) - if err := xml.Unmarshal(data, &iq); err != nil { - t.Errorf("Unmarshal(%s) returned error", data) - } - - // Check that we have extracted the delegation info as IQPayload - var node string - if iq.Payload != nil { - if delegation, ok := iq.Payload.(*Delegation); ok { - packet := delegation.Forwarded.Stanza - forwardedIQ, ok := packet.(IQ) - if !ok { - t.Errorf("Could not extract packet IQ") - return - } - if forwardedIQ.Payload != nil { - if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok { - node = pubsub.Publish.Node - } - } - } - } - if node != "http://jabber.org/protocol/mood" { - t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq) - } -} diff --git a/router.go b/router.go index b252db6..e8bf4b5 100644 --- a/router.go +++ b/router.go @@ -3,6 +3,8 @@ package xmpp import ( "encoding/xml" "strings" + + "gosrc.io/xmpp/stanza" ) /* @@ -32,7 +34,7 @@ func NewRouter() *Router { // route is called by the XMPP client to dispatch stanza received using the set up routes. // It is also used by test, but is not supposed to be used directly by users of the library. -func (r *Router) route(s Sender, p Packet) { +func (r *Router) route(s Sender, p stanza.Packet) { var match RouteMatch if r.Match(p, &match) { @@ -41,15 +43,15 @@ func (r *Router) route(s Sender, p Packet) { return } // If there is no match and we receive an iq set or get, we need to send a reply - if iq, ok := p.(IQ); ok { - if iq.Type == IQTypeGet || iq.Type == IQTypeSet { + if iq, ok := p.(stanza.IQ); ok { + if iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet { iqNotImplemented(s, iq) } } } -func iqNotImplemented(s Sender, iq IQ) { - err := Err{ +func iqNotImplemented(s Sender, iq stanza.IQ) { + err := stanza.Err{ XMLName: xml.Name{Local: "error"}, Code: 501, Type: "cancel", @@ -66,7 +68,7 @@ func (r *Router) NewRoute() *Route { return route } -func (r *Router) Match(p Packet, match *RouteMatch) bool { +func (r *Router) Match(p stanza.Packet, match *RouteMatch) bool { for _, route := range r.routes { if route.Match(p, match) { return true @@ -83,14 +85,14 @@ func (r *Router) Handle(name string, handler Handler) *Route { // HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence) // See Route.Path() and Route.HandlerFunc(). -func (r *Router) HandleFunc(name string, f func(s Sender, p Packet)) *Route { +func (r *Router) HandleFunc(name string, f func(s Sender, p stanza.Packet)) *Route { return r.NewRoute().Packet(name).HandlerFunc(f) } // ============================================================================ // Route type Handler interface { - HandlePacket(s Sender, p Packet) + HandlePacket(s Sender, p stanza.Packet) } type Route struct { @@ -108,10 +110,10 @@ func (r *Route) Handler(handler Handler) *Route { // ordinary functions as XMPP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler that calls f. -type HandlerFunc func(s Sender, p Packet) +type HandlerFunc func(s Sender, p stanza.Packet) // HandlePacket calls f(s, p) -func (f HandlerFunc) HandlePacket(s Sender, p Packet) { +func (f HandlerFunc) HandlePacket(s Sender, p stanza.Packet) { f(s, p) } @@ -126,7 +128,7 @@ func (r *Route) addMatcher(m matcher) *Route { return r } -func (r *Route) Match(p Packet, match *RouteMatch) bool { +func (r *Route) Match(p stanza.Packet, match *RouteMatch) bool { for _, m := range r.matchers { if matched := m.Match(p, match); !matched { return false @@ -144,18 +146,18 @@ func (r *Route) Match(p Packet, match *RouteMatch) bool { type nameMatcher string -func (n nameMatcher) Match(p Packet, match *RouteMatch) bool { +func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool { var name string // TODO: To avoid type switch everywhere in matching, I think we will need to have // to move to a concrete type for packets, to make matching and comparison more natural. // Current code structure is probably too rigid. // Maybe packet types should even be from an enum. switch p.(type) { - case Message: + case stanza.Message: name = "message" - case IQ: + case stanza.IQ: name = "iq" - case Presence: + case stanza.Presence: name = "presence" } if name == string(n) { @@ -177,14 +179,14 @@ func (r *Route) Packet(name string) *Route { // nsTypeMather matches on a list of IQ payload namespaces type nsTypeMatcher []string -func (m nsTypeMatcher) Match(p Packet, match *RouteMatch) bool { - var stanzaType StanzaType +func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool { + var stanzaType stanza.StanzaType switch packet := p.(type) { - case IQ: + case stanza.IQ: stanzaType = packet.Type - case Presence: + case stanza.Presence: stanzaType = packet.Type - case Message: + case stanza.Message: if packet.Type == "" { // optional on message, normal is the default type stanzaType = "normal" @@ -211,8 +213,8 @@ func (r *Route) StanzaType(types ...string) *Route { // nsIqMather matches on a list of IQ payload namespaces type nsIQMatcher []string -func (m nsIQMatcher) Match(p Packet, match *RouteMatch) bool { - iq, ok := p.(IQ) +func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool { + iq, ok := p.(stanza.IQ) if !ok { return false } @@ -235,7 +237,7 @@ func (r *Route) IQNamespaces(namespaces ...string) *Route { // Matchers are used to "specialize" routes and focus on specific packet features type matcher interface { - Match(Packet, *RouteMatch) bool + Match(stanza.Packet, *RouteMatch) bool } // RouteMatch extracts and gather match information diff --git a/router_test.go b/router_test.go index c1049bf..98a4697 100644 --- a/router_test.go +++ b/router_test.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/xml" "testing" + + "gosrc.io/xmpp/stanza" ) // ============================================================================ @@ -11,13 +13,13 @@ import ( func TestNameMatcher(t *testing.T) { router := NewRouter() - router.HandleFunc("message", func(s Sender, p Packet) { + router.HandleFunc("message", func(s Sender, p stanza.Packet) { _ = s.SendRaw(successFlag) }) // Check that a message packet is properly matched conn := NewSenderMock() - msg := NewMessage(Attrs{Type: MessageTypeChat, To: "test@localhost", Id: "1"}) + msg := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, To: "test@localhost", Id: "1"}) msg.Body = "Hello" router.route(conn, msg) if conn.String() != successFlag { @@ -26,8 +28,8 @@ func TestNameMatcher(t *testing.T) { // Check that an IQ packet is not matched conn = NewSenderMock() - iq := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"}) - iq.Payload = &DiscoInfo{} + iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + iq.Payload = &stanza.DiscoInfo{} router.route(conn, iq) if conn.String() == successFlag { t.Error("IQ should not have been matched and routed") @@ -37,18 +39,18 @@ func TestNameMatcher(t *testing.T) { func TestIQNSMatcher(t *testing.T) { router := NewRouter() router.NewRoute(). - IQNamespaces(NSDiscoInfo, NSDiscoItems). - HandlerFunc(func(s Sender, p Packet) { + IQNamespaces(stanza.NSDiscoInfo, stanza.NSDiscoItems). + HandlerFunc(func(s Sender, p stanza.Packet) { _ = s.SendRaw(successFlag) }) // Check that an IQ with proper namespace does match conn := NewSenderMock() - iqDisco := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"}) + iqDisco := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) // TODO: Add a function to generate payload with proper namespace initialisation - iqDisco.Payload = &DiscoInfo{ + iqDisco.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ - Space: NSDiscoInfo, + Space: stanza.NSDiscoInfo, Local: "query", }} router.route(conn, iqDisco) @@ -58,9 +60,9 @@ func TestIQNSMatcher(t *testing.T) { // Check that another namespace is not matched conn = NewSenderMock() - iqVersion := NewIQ(Attrs{Type: IQTypeGet, To: "localhost", Id: "1"}) + iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) // TODO: Add a function to generate payload with proper namespace initialisation - iqVersion.Payload = &DiscoInfo{ + iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", @@ -75,13 +77,13 @@ func TestTypeMatcher(t *testing.T) { router := NewRouter() router.NewRoute(). StanzaType("normal"). - HandlerFunc(func(s Sender, p Packet) { + HandlerFunc(func(s Sender, p stanza.Packet) { _ = s.SendRaw(successFlag) }) // Check that a packet with the proper type matches conn := NewSenderMock() - message := NewMessage(Attrs{Type: "normal", To: "test@localhost", Id: "1"}) + message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"}) message.Body = "hello" router.route(conn, message) @@ -91,7 +93,7 @@ func TestTypeMatcher(t *testing.T) { // We should match on default type 'normal' for message without a type conn = NewSenderMock() - message = NewMessage(Attrs{To: "test@localhost", Id: "1"}) + message = stanza.NewMessage(stanza.Attrs{To: "test@localhost", Id: "1"}) message.Body = "hello" router.route(conn, message) @@ -101,8 +103,8 @@ func TestTypeMatcher(t *testing.T) { // We do not match on other types conn = NewSenderMock() - iqVersion := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) - iqVersion.Payload = &DiscoInfo{ + iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", @@ -119,38 +121,38 @@ func TestCompositeMatcher(t *testing.T) { router.NewRoute(). IQNamespaces("jabber:iq:version"). StanzaType("get"). - HandlerFunc(func(s Sender, p Packet) { + HandlerFunc(func(s Sender, p stanza.Packet) { _ = s.SendRaw(successFlag) }) // Data set - getVersionIq := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) - getVersionIq.Payload = &Version{ + getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + getVersionIq.Payload = &stanza.Version{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", }} - setVersionIq := NewIQ(Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"}) - setVersionIq.Payload = &Version{ + setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"}) + setVersionIq.Payload = &stanza.Version{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", }} - GetDiscoIq := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) - GetDiscoIq.Payload = &DiscoInfo{ + GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + GetDiscoIq.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/disco#info", Local: "query", }} - message := NewMessage(Attrs{Type: "normal", To: "test@localhost", Id: "1"}) + message := stanza.NewMessage(stanza.Attrs{Type: "normal", To: "test@localhost", Id: "1"}) message.Body = "hello" tests := []struct { name string - input Packet + input stanza.Packet want bool }{ {name: "match get version iq", input: getVersionIq, want: true}, @@ -178,13 +180,13 @@ func TestCompositeMatcher(t *testing.T) { func TestCatchallMatcher(t *testing.T) { router := NewRouter() router.NewRoute(). - HandlerFunc(func(s Sender, p Packet) { + HandlerFunc(func(s Sender, p stanza.Packet) { _ = s.SendRaw(successFlag) }) // Check that we match on several packets conn := NewSenderMock() - message := NewMessage(Attrs{Type: "chat", To: "test@localhost", Id: "1"}) + message := stanza.NewMessage(stanza.Attrs{Type: "chat", To: "test@localhost", Id: "1"}) message.Body = "hello" router.route(conn, message) @@ -193,8 +195,8 @@ func TestCatchallMatcher(t *testing.T) { } conn = NewSenderMock() - iqVersion := NewIQ(Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) - iqVersion.Payload = &DiscoInfo{ + iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", @@ -219,7 +221,7 @@ func NewSenderMock() SenderMock { return SenderMock{buffer: new(bytes.Buffer)} } -func (s SenderMock) Send(packet Packet) error { +func (s SenderMock) Send(packet stanza.Packet) error { out, err := xml.Marshal(packet) if err != nil { return err @@ -239,7 +241,7 @@ func (s SenderMock) String() string { func TestSenderMock(t *testing.T) { conn := NewSenderMock() - msg := NewMessage(Attrs{To: "test@localhost", Id: "1"}) + msg := stanza.NewMessage(stanza.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 88486c6..2077e7a 100644 --- a/session.go +++ b/session.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "net" + + "gosrc.io/xmpp/stanza" ) const xmppStreamOpen = "" @@ -15,7 +17,7 @@ type Session struct { // Session info BindJid string // Jabber ID as provided by XMPP server StreamId string - Features StreamFeatures + Features stanza.StreamFeatures TlsEnabled bool lastPacketId int @@ -85,14 +87,14 @@ func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Config) { s.decoder.CharsetReader = o.CharsetReader } -func (s *Session) open(domain string) (f StreamFeatures) { +func (s *Session) open(domain string) (f stanza.StreamFeatures) { // Send stream open tag - if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, NSClient, NSStream); s.err != nil { + if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, stanza.NSClient, stanza.NSStream); s.err != nil { return } // Set xml decoder and extract streamID from reply - s.StreamId, s.err = initStream(s.decoder) // TODO refactor / rename + s.StreamId, s.err = stanza.InitStream(s.decoder) // TODO refactor / rename if s.err != nil { return } @@ -112,7 +114,7 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn { if _, ok := s.Features.DoesStartTLS(); ok { fmt.Fprintf(s.socketProxy, "") - var k tlsProceed + var k stanza.TLSProceed if s.err = s.decoder.DecodeElement(&k, nil); s.err != nil { s.err = errors.New("expecting starttls proceed: " + s.err.Error()) return conn @@ -120,8 +122,8 @@ func (s *Session) startTlsIfSupported(conn net.Conn, domain string) net.Conn { s.TlsEnabled = true // TODO: add option to accept all TLS certificates: insecureSkipTlsVerify (DefaultTlsConfig.InsecureSkipVerify) - DefaultTlsConfig.ServerName = domain - tlsConn := tls.Client(conn, &DefaultTlsConfig) + stanza.DefaultTlsConfig.ServerName = domain + tlsConn := tls.Client(conn, &stanza.DefaultTlsConfig) // We convert existing connection to TLS if s.err = tlsConn.Handshake(); s.err != nil { return tlsConn @@ -153,12 +155,12 @@ func (s *Session) bind(o Config) { var resource = o.parsedJid.Resource if resource != "" { fmt.Fprintf(s.socketProxy, "%s", - s.PacketId(), nsBind, resource) + s.PacketId(), stanza.NSBind, resource) } else { - fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsBind) + fmt.Fprintf(s.socketProxy, "", s.PacketId(), stanza.NSBind) } - var iq IQ + var iq stanza.IQ if s.err = s.decoder.Decode(&iq); s.err != nil { s.err = errors.New("error decoding iq bind result: " + s.err.Error()) return @@ -166,7 +168,7 @@ func (s *Session) bind(o Config) { // TODO Check all elements switch payload := iq.Payload.(type) { - case *BindBind: + case *stanza.BindBind: s.BindJid = payload.Jid // our local id (with possibly randomly generated resource default: s.err = errors.New("iq bind result missing") @@ -182,9 +184,9 @@ func (s *Session) rfc3921Session(o Config) { return } - var iq IQ - if s.Features.Session.optional.Local != "" { - fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsSession) + var iq stanza.IQ + if s.Features.Session.Optional.Local != "" { + fmt.Fprintf(s.socketProxy, "", s.PacketId(), stanza.NSSession) if s.err = s.decoder.Decode(&iq); s.err != nil { s.err = errors.New("expecting iq result after session open: " + s.err.Error()) return diff --git a/stanza/auth_sasl.go b/stanza/auth_sasl.go new file mode 100644 index 0000000..a5ea907 --- /dev/null +++ b/stanza/auth_sasl.go @@ -0,0 +1,72 @@ +package stanza + +import "encoding/xml" + +// ============================================================================ +// SASLSuccess + +type SASLSuccess struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl success"` +} + +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"` + Value string `xml:",innerxml"` +} + +type BindBind struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` + Resource string `xml:"resource,omitempty"` + Jid string `xml:"jid,omitempty"` +} + +func (b *BindBind) Namespace() string { + return b.XMLName.Space +} + +// Session is obsolete in RFC 6121. +// Added for compliance with RFC 3121. +// Remove when ejabberd purely conforms to RFC 6121. +type sessionSession struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` + Optional xml.Name // If it does exist, it mean we are not required to open session +} diff --git a/stanza/component.go b/stanza/component.go new file mode 100644 index 0000000..196faea --- /dev/null +++ b/stanza/component.go @@ -0,0 +1,89 @@ +package stanza + +import ( + "encoding/xml" +) + +// ============================================================================ +// Handshake Stanza + +// Handshake is a stanza used by XMPP components to authenticate on XMPP +// component port. +type Handshake struct { + XMLName xml.Name `xml:"jabber:component:accept handshake"` + // TODO Add handshake value with test for proper serialization + // Value string `xml:",innerxml"` +} + +func (Handshake) Name() string { + return "component:handshake" +} + +// Handshake decoding wrapper + +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 +} + +// ============================================================================ +// Component delegation +// XEP-0355 + +// Delegation can be used both on message (for delegated) and IQ (for Forwarded), +// depending on the context. +type Delegation struct { + MsgExtension + XMLName xml.Name `xml:"urn:xmpp:delegation:1 delegation"` + Forwarded *Forwarded // This is used in iq to wrap delegated iqs + Delegated *Delegated // This is used in a message to confirm delegated namespace +} + +func (d *Delegation) Namespace() string { + return d.XMLName.Space +} + +type Forwarded struct { + XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"` + Stanza Packet +} + +// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to +// transform generic XML content into hierarchical Node structure. +func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + // Check subelements to extract required field as boolean + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + if packet, err := decodeClient(d, tt); err == nil { + f.Stanza = packet + } + + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + +type Delegated struct { + XMLName xml.Name `xml:"delegated"` + Namespace string `xml:"namespace,attr,omitempty"` +} + +func init() { + TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{}) +} diff --git a/stanza/component_test.go b/stanza/component_test.go new file mode 100644 index 0000000..d02531e --- /dev/null +++ b/stanza/component_test.go @@ -0,0 +1,79 @@ +package stanza + +import ( + "encoding/xml" + "testing" +) + +// We should be able to properly parse delegation confirmation messages +func TestParsingDelegationMessage(t *testing.T) { + packetStr := ` + + + +` + var msg Message + data := []byte(packetStr) + if err := xml.Unmarshal(data, &msg); err != nil { + t.Errorf("Unmarshal(%s) returned error", data) + } + + // Check that we have extracted the delegation info as MsgExtension + var nsDelegated string + for _, ext := range msg.Extensions { + if delegation, ok := ext.(*Delegation); ok { + nsDelegated = delegation.Delegated.Namespace + } + } + if nsDelegated != "http://jabber.org/protocol/pubsub" { + t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg) + } +} + +// Check that we can parse a delegation IQ. +// The most important thing is to be able to +func TestParsingDelegationIQ(t *testing.T) { + packetStr := ` + + + + + + + + + + + + + + + +` + var iq IQ + data := []byte(packetStr) + if err := xml.Unmarshal(data, &iq); err != nil { + t.Errorf("Unmarshal(%s) returned error", data) + } + + // Check that we have extracted the delegation info as IQPayload + var node string + if iq.Payload != nil { + if delegation, ok := iq.Payload.(*Delegation); ok { + packet := delegation.Forwarded.Stanza + forwardedIQ, ok := packet.(IQ) + if !ok { + t.Errorf("Could not extract packet IQ") + return + } + if forwardedIQ.Payload != nil { + if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok { + node = pubsub.Publish.Node + } + } + } + } + if node != "http://jabber.org/protocol/mood" { + t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq) + } +} diff --git a/error.go b/stanza/error.go similarity index 97% rename from error.go rename to stanza/error.go index 2fe542f..d93c324 100644 --- a/error.go +++ b/stanza/error.go @@ -1,14 +1,10 @@ -package xmpp +package stanza import ( "encoding/xml" "strconv" ) -/* -TODO support ability to put Raw payload inside IQ -*/ - // ============================================================================ // XMPP Errors diff --git a/error_enum.go b/stanza/error_enum.go similarity index 95% rename from error_enum.go rename to stanza/error_enum.go index 314df4c..710de0e 100644 --- a/error_enum.go +++ b/stanza/error_enum.go @@ -1,4 +1,4 @@ -package xmpp +package stanza // ErrorType is a Enum of error attribute type type ErrorType string diff --git a/iot_control.go b/stanza/iot.go similarity index 97% rename from iot_control.go rename to stanza/iot.go index 67a54a1..5235089 100644 --- a/iot_control.go +++ b/stanza/iot.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/iot_control_test.go b/stanza/iot_test.go similarity index 97% rename from iot_control_test.go rename to stanza/iot_test.go index aaf26c9..43a60ec 100644 --- a/iot_control_test.go +++ b/stanza/iot_test.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/iq.go b/stanza/iq.go similarity index 76% rename from iq.go rename to stanza/iq.go index d04ab36..24b65cc 100644 --- a/iq.go +++ b/stanza/iq.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" @@ -129,54 +129,6 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } } -// ============================================================================ -// Generic / unknown content - -// Node is a generic structure to represent XML data. It is used to parse -// unreferenced or custom stanza payload. -type Node struct { - XMLName xml.Name - Attrs []xml.Attr `xml:"-"` - Content string `xml:",innerxml"` - Nodes []Node `xml:",any"` -} - -func (n *Node) Namespace() string { - return n.XMLName.Space -} - -// Attr represents generic XML attributes, as used on the generic XML Node -// representation. -type Attr struct { - K string - V string -} - -// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to -// transform generic XML content into hierarchical Node structure. -func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - // Assign "n.Attrs = start.Attr", without repeating xmlns in attributes: - for _, attr := range start.Attr { - // Do not repeat xmlns, it is already in XMLName - if attr.Name.Local != "xmlns" { - n.Attrs = append(n.Attrs, attr) - } - } - type node Node - return d.DecodeElement((*node)(n), &start) -} - -// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a -// Node structure to XML. -func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { - start.Attr = n.Attrs - start.Name = n.XMLName - - err = e.EncodeToken(start) - e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName}) - return e.EncodeToken(xml.EndElement{Name: start.Name}) -} - // ============================================================================ // Disco diff --git a/iq_test.go b/stanza/iq_test.go similarity index 83% rename from iq_test.go rename to stanza/iq_test.go index df9b360..a880192 100644 --- a/iq_test.go +++ b/stanza/iq_test.go @@ -1,4 +1,4 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" @@ -6,22 +6,21 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "gosrc.io/xmpp" ) func TestUnmarshalIqs(t *testing.T) { //var cs1 = new(iot.ControlSet) var tests = []struct { iqString string - parsedIQ xmpp.IQ + parsedIQ IQ }{ {"", - xmpp.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: xmpp.Attrs{Type: xmpp.IQTypeSet, To: "test@localhost", Id: "1"}}}, + IQ{XMLName: xml.Name{Local: "iq"}, Attrs: Attrs{Type: 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}}, } for _, test := range tests { - parsedIQ := xmpp.IQ{} + parsedIQ := IQ{} err := xml.Unmarshal([]byte(test.iqString), &parsedIQ) if err != nil { t.Errorf("Unmarshal(%s) returned error", test.iqString) @@ -35,16 +34,16 @@ func TestUnmarshalIqs(t *testing.T) { } func TestGenerateIq(t *testing.T) { - iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) - payload := xmpp.DiscoInfo{ - Identity: xmpp.Identity{ + iq := NewIQ(Attrs{Type: IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) + payload := DiscoInfo{ + Identity: Identity{ Name: "Test Gateway", Category: "gateway", Type: "mqtt", }, - Features: []xmpp.Feature{ - {Var: xmpp.NSDiscoInfo}, - {Var: xmpp.NSDiscoItems}, + Features: []Feature{ + {Var: NSDiscoInfo}, + {Var: NSDiscoItems}, }, } iq.Payload = &payload @@ -58,7 +57,7 @@ func TestGenerateIq(t *testing.T) { t.Error("empty error should not be serialized") } - parsedIQ := xmpp.IQ{} + parsedIQ := IQ{} if err = xml.Unmarshal(data, &parsedIQ); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -69,7 +68,7 @@ func TestGenerateIq(t *testing.T) { } func TestErrorTag(t *testing.T) { - xError := xmpp.Err{ + xError := Err{ XMLName: xml.Name{Local: "error"}, Code: 503, Type: "cancel", @@ -82,7 +81,7 @@ func TestErrorTag(t *testing.T) { t.Errorf("cannot marshal xml structure: %s", err) } - parsedError := xmpp.Err{} + parsedError := Err{} if err = xml.Unmarshal(data, &parsedError); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -93,8 +92,8 @@ func TestErrorTag(t *testing.T) { } func TestDiscoItems(t *testing.T) { - iq := xmpp.NewIQ(xmpp.Attrs{Type: xmpp.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) - payload := xmpp.DiscoItems{ + iq := NewIQ(Attrs{Type: IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) + payload := DiscoItems{ Node: "music", } iq.Payload = &payload @@ -104,7 +103,7 @@ func TestDiscoItems(t *testing.T) { t.Errorf("cannot marshal xml structure") } - parsedIQ := xmpp.IQ{} + parsedIQ := IQ{} if err = xml.Unmarshal(data, &parsedIQ); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -117,7 +116,7 @@ func TestDiscoItems(t *testing.T) { func TestUnmarshalPayload(t *testing.T) { query := "" - parsedIQ := xmpp.IQ{} + parsedIQ := IQ{} err := xml.Unmarshal([]byte(query), &parsedIQ) if err != nil { t.Errorf("Unmarshal(%s) returned error", query) @@ -142,7 +141,7 @@ func TestPayloadWithError(t *testing.T) { ` - parsedIQ := xmpp.IQ{} + parsedIQ := IQ{} err := xml.Unmarshal([]byte(iq), &parsedIQ) if err != nil { t.Errorf("Unmarshal error: %s", iq) @@ -158,7 +157,7 @@ func TestUnknownPayload(t *testing.T) { iq := ` ` - parsedIQ := xmpp.IQ{} + parsedIQ := IQ{} err := xml.Unmarshal([]byte(iq), &parsedIQ) if err != nil { t.Errorf("Unmarshal error: %#v (%s)", err, iq) diff --git a/message.go b/stanza/message.go similarity index 99% rename from message.go rename to stanza/message.go index 6b57f65..35e44c1 100644 --- a/message.go +++ b/stanza/message.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/message_test.go b/stanza/message_test.go similarity index 82% rename from message_test.go rename to stanza/message_test.go index 8f18d39..9427c4a 100644 --- a/message_test.go +++ b/stanza/message_test.go @@ -1,15 +1,14 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" "testing" "github.com/google/go-cmp/cmp" - "gosrc.io/xmpp" ) func TestGenerateMessage(t *testing.T) { - message := xmpp.NewMessage(xmpp.Attrs{Type: xmpp.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"}) + message := NewMessage(Attrs{Type: MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"}) message.Body = "Hi" message.Subject = "Msg Subject" @@ -18,7 +17,7 @@ func TestGenerateMessage(t *testing.T) { t.Errorf("cannot marshal xml structure") } - parsedMessage := xmpp.Message{} + parsedMessage := Message{} if err = xml.Unmarshal(data, &parsedMessage); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -38,7 +37,7 @@ func TestDecodeError(t *testing.T) { ` - parsedMessage := xmpp.Message{} + parsedMessage := Message{} if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { t.Errorf("message error stanza unmarshall error: %v", err) return @@ -50,15 +49,15 @@ func TestDecodeError(t *testing.T) { func TestGetOOB(t *testing.T) { image := "https://localhost/image.png" - msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost"}) - ext := xmpp.OOB{ + msg := NewMessage(Attrs{To: "test@localhost"}) + ext := OOB{ XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"}, URL: image, } msg.Extensions = append(msg.Extensions, &ext) // OOB can properly be found - var oob xmpp.OOB + var oob OOB // Try to find and if ok := msg.Get(&oob); !ok { t.Error("could not find oob extension") @@ -69,7 +68,7 @@ func TestGetOOB(t *testing.T) { } // Markable is not found - var m xmpp.Markable + var m Markable if ok := msg.Get(&m); ok { t.Error("we should not have found markable extension") } diff --git a/msg_chat_markers.go b/stanza/msg_chat_markers.go similarity index 96% rename from msg_chat_markers.go rename to stanza/msg_chat_markers.go index b33aa3c..f226379 100644 --- a/msg_chat_markers.go +++ b/stanza/msg_chat_markers.go @@ -1,6 +1,8 @@ -package xmpp +package stanza -import "encoding/xml" +import ( + "encoding/xml" +) /* Support for: diff --git a/msg_chat_state.go b/stanza/msg_chat_state.go similarity index 96% rename from msg_chat_state.go rename to stanza/msg_chat_state.go index 7f2f89e..728a52e 100644 --- a/msg_chat_state.go +++ b/stanza/msg_chat_state.go @@ -1,6 +1,8 @@ -package xmpp +package stanza -import "encoding/xml" +import ( + "encoding/xml" +) /* Support for: diff --git a/msg_html.go b/stanza/msg_html.go similarity index 92% rename from msg_html.go rename to stanza/msg_html.go index fbb4aae..1b4016c 100644 --- a/msg_html.go +++ b/stanza/msg_html.go @@ -1,6 +1,8 @@ -package xmpp +package stanza -import "encoding/xml" +import ( + "encoding/xml" +) type HTML struct { MsgExtension diff --git a/msg_html_test.go b/stanza/msg_html_test.go similarity index 82% rename from msg_html_test.go rename to stanza/msg_html_test.go index b88104a..1dbaa00 100644 --- a/msg_html_test.go +++ b/stanza/msg_html_test.go @@ -1,20 +1,18 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" "testing" - - "gosrc.io/xmpp" ) func TestHTMLGen(t *testing.T) { htmlBody := "

Hello World

" - msg := xmpp.NewMessage(xmpp.Attrs{To: "test@localhost"}) + msg := NewMessage(Attrs{To: "test@localhost"}) msg.Body = "Hello World" - body := xmpp.HTMLBody{ + body := HTMLBody{ InnerXML: htmlBody, } - html := xmpp.HTML{Body: body} + html := HTML{Body: body} msg.Extensions = append(msg.Extensions, html) result := msg.XMPPFormat() @@ -23,7 +21,7 @@ func TestHTMLGen(t *testing.T) { t.Errorf("incorrect serialize message:\n%s", result) } - parsedMessage := xmpp.Message{} + parsedMessage := Message{} if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { t.Errorf("message HTML unmarshall error: %v", err) return @@ -33,7 +31,7 @@ func TestHTMLGen(t *testing.T) { t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body) } - var h xmpp.HTML + var h HTML if ok := parsedMessage.Get(&h); !ok { t.Error("could not extract HTML body") } diff --git a/msg_oob.go b/stanza/msg_oob.go similarity index 88% rename from msg_oob.go rename to stanza/msg_oob.go index 38b6aeb..039fac1 100644 --- a/msg_oob.go +++ b/stanza/msg_oob.go @@ -1,6 +1,8 @@ -package xmpp +package stanza -import "encoding/xml" +import ( + "encoding/xml" +) /* Support for: diff --git a/msg_receipts.go b/stanza/msg_receipts.go similarity index 94% rename from msg_receipts.go rename to stanza/msg_receipts.go index 87a1f73..85c2783 100644 --- a/msg_receipts.go +++ b/stanza/msg_receipts.go @@ -1,6 +1,8 @@ -package xmpp +package stanza -import "encoding/xml" +import ( + "encoding/xml" +) /* Support for: diff --git a/msg_receipts_test.go b/stanza/msg_receipts_test.go similarity index 90% rename from msg_receipts_test.go rename to stanza/msg_receipts_test.go index 0db0c64..4b9971c 100644 --- a/msg_receipts_test.go +++ b/stanza/msg_receipts_test.go @@ -1,10 +1,8 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" "testing" - - "gosrc.io/xmpp" ) func TestDecodeRequest(t *testing.T) { @@ -15,7 +13,7 @@ func TestDecodeRequest(t *testing.T) { My lord, dispatch; read o'er these articles. ` - parsedMessage := xmpp.Message{} + parsedMessage := Message{} if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { t.Errorf("message receipt unmarshall error: %v", err) return @@ -31,7 +29,7 @@ func TestDecodeRequest(t *testing.T) { } switch ext := parsedMessage.Extensions[0].(type) { - case *xmpp.ReceiptRequest: + case *ReceiptRequest: if ext.XMLName.Local != "request" { t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local) } diff --git a/stanza/node.go b/stanza/node.go new file mode 100644 index 0000000..26a3e03 --- /dev/null +++ b/stanza/node.go @@ -0,0 +1,51 @@ +package stanza + +import "encoding/xml" + +// ============================================================================ +// Generic / unknown content + +// Node is a generic structure to represent XML data. It is used to parse +// unreferenced or custom stanza payload. +type Node struct { + XMLName xml.Name + Attrs []xml.Attr `xml:"-"` + Content string `xml:",innerxml"` + Nodes []Node `xml:",any"` +} + +func (n *Node) Namespace() string { + return n.XMLName.Space +} + +// Attr represents generic XML attributes, as used on the generic XML Node +// representation. +type Attr struct { + K string + V string +} + +// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to +// transform generic XML content into hierarchical Node structure. +func (n *Node) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + // Assign "n.Attrs = start.Attr", without repeating xmlns in attributes: + for _, attr := range start.Attr { + // Do not repeat xmlns, it is already in XMLName + if attr.Name.Local != "xmlns" { + n.Attrs = append(n.Attrs, attr) + } + } + type node Node + return d.DecodeElement((*node)(n), &start) +} + +// MarshalXML is a custom XML serializer used by xml.Marshal to serialize a +// Node structure to XML. +func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { + start.Attr = n.Attrs + start.Name = n.XMLName + + err = e.EncodeToken(start) + e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName}) + return e.EncodeToken(xml.EndElement{Name: start.Name}) +} diff --git a/ns.go b/stanza/ns.go similarity index 52% rename from ns.go rename to stanza/ns.go index d36e3db..e955a35 100644 --- a/ns.go +++ b/stanza/ns.go @@ -1,11 +1,11 @@ -package xmpp +package stanza const ( NSStream = "http://etherx.jabber.org/streams" nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" - nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" - nsBind = "urn:ietf:params:xml:ns:xmpp-bind" - nsSession = "urn:ietf:params:xml:ns:xmpp-session" + NSSASL = "urn:ietf:params:xml:ns:xmpp-sasl" + NSBind = "urn:ietf:params:xml:ns:xmpp-bind" + NSSession = "urn:ietf:params:xml:ns:xmpp-session" NSClient = "jabber:client" NSComponent = "jabber:component:accept" ) diff --git a/packet.go b/stanza/packet.go similarity index 96% rename from packet.go rename to stanza/packet.go index ce42236..567796d 100644 --- a/packet.go +++ b/stanza/packet.go @@ -1,4 +1,4 @@ -package xmpp +package stanza type Packet interface { Name() string diff --git a/packet_enum.go b/stanza/packet_enum.go similarity index 98% rename from packet_enum.go rename to stanza/packet_enum.go index 9bc30e4..103966a 100644 --- a/packet_enum.go +++ b/stanza/packet_enum.go @@ -1,4 +1,4 @@ -package xmpp +package stanza type StanzaType string diff --git a/parser.go b/stanza/parser.go similarity index 90% rename from parser.go rename to stanza/parser.go index 465db5d..c83e17e 100644 --- a/parser.go +++ b/stanza/parser.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" @@ -14,7 +14,7 @@ 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 initStream(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() @@ -41,31 +41,14 @@ func initStream(p *xml.Decoder) (sessionID string, err error) { } } -// Scan XML token stream to find next StartElement. -func nextStart(p *xml.Decoder) (xml.StartElement, error) { - for { - t, err := p.Token() - if err == io.EOF { - return xml.StartElement{}, errors.New("connection closed") - } - if err != nil { - return xml.StartElement{}, fmt.Errorf("nextStart %s", err) - } - switch t := t.(type) { - case xml.StartElement: - return t, nil - } - } -} - -// nextPacket scans XML token stream for next complete XMPP stanza. +// 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 -// TODO make auth and bind use nextPacket instead of directly nextStart -func nextPacket(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) + se, err := NextStart(p) if err != nil { return nil, err } @@ -74,7 +57,7 @@ func nextPacket(p *xml.Decoder) (Packet, error) { switch se.Name.Space { case NSStream: return decodeStream(p, se) - case nsSASL: + case NSSASL: return decodeSASL(p, se) case NSClient: return decodeClient(p, se) @@ -86,6 +69,23 @@ func nextPacket(p *xml.Decoder) (Packet, error) { } } +// Scan XML token stream to find next StartElement. +func NextStart(p *xml.Decoder) (xml.StartElement, error) { + for { + t, err := p.Token() + if err == io.EOF { + return xml.StartElement{}, errors.New("connection closed") + } + if err != nil { + return xml.StartElement{}, fmt.Errorf("NextStart %s", err) + } + switch t := t.(type) { + case xml.StartElement: + return t, nil + } + } +} + /* TODO: From all the decoder, we can return a pointer to the actual concrete type, instead of directly that type. diff --git a/pep.go b/stanza/pep.go similarity index 98% rename from pep.go rename to stanza/pep.go index 7223dc4..7de57ea 100644 --- a/pep.go +++ b/stanza/pep.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/pres_muc.go b/stanza/pres_muc.go similarity index 98% rename from pres_muc.go rename to stanza/pres_muc.go index 10d947e..e944fb6 100644 --- a/pres_muc.go +++ b/stanza/pres_muc.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/pres_muc_test.go b/stanza/pres_muc_test.go similarity index 89% rename from pres_muc_test.go rename to stanza/pres_muc_test.go index b2120e3..6ea2ab6 100644 --- a/pres_muc_test.go +++ b/stanza/pres_muc_test.go @@ -1,10 +1,8 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" "testing" - - "gosrc.io/xmpp" ) // https://xmpp.org/extensions/xep-0045.html#example-27 @@ -18,12 +16,12 @@ func TestMucPassword(t *testing.T) { ` - var parsedPresence xmpp.Presence + var parsedPresence Presence if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { t.Errorf("Unmarshal(%s) returned error", str) } - var muc xmpp.MucPresence + var muc MucPresence if ok := parsedPresence.Get(&muc); !ok { t.Error("muc presence extension was not found") } @@ -44,12 +42,12 @@ func TestMucHistory(t *testing.T) { ` - var parsedPresence xmpp.Presence + var parsedPresence Presence if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { t.Errorf("Unmarshal(%s) returned error", str) } - var muc xmpp.MucPresence + var muc MucPresence if ok := parsedPresence.Get(&muc); !ok { t.Error("muc presence extension was not found") } diff --git a/presence.go b/stanza/presence.go similarity index 99% rename from presence.go rename to stanza/presence.go index 466b9e1..d3a8d1a 100644 --- a/presence.go +++ b/stanza/presence.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/presence_enum.go b/stanza/presence_enum.go similarity index 95% rename from presence_enum.go rename to stanza/presence_enum.go index c0723bd..c92691f 100644 --- a/presence_enum.go +++ b/stanza/presence_enum.go @@ -1,4 +1,4 @@ -package xmpp +package stanza // PresenceShow is a Enum of presence element show type PresenceShow string diff --git a/presence_test.go b/stanza/presence_test.go similarity index 73% rename from presence_test.go rename to stanza/presence_test.go index fb8e09a..0433857 100644 --- a/presence_test.go +++ b/stanza/presence_test.go @@ -1,24 +1,22 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" "testing" - "gosrc.io/xmpp" - "github.com/google/go-cmp/cmp" ) func TestGeneratePresence(t *testing.T) { - presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) - presence.Show = xmpp.PresenceShowChat + presence := NewPresence(Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) + presence.Show = PresenceShowChat data, err := xml.Marshal(presence) if err != nil { t.Errorf("cannot marshal xml structure") } - var parsedPresence xmpp.Presence + var parsedPresence Presence if err = xml.Unmarshal(data, &parsedPresence); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -32,13 +30,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 xmpp.PresenceShow `xml:"show"` - Status string `xml:"status"` - Priority int8 `xml:"priority"` + Show PresenceShow `xml:"show"` + Status string `xml:"status"` + Priority int8 `xml:"priority"` } - presence := xmpp.NewPresence(xmpp.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) - presence.Show = xmpp.PresenceShowXA + presence := NewPresence(Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) + presence.Show = PresenceShowXA presence.Status = "Coding" presence.Priority = 10 diff --git a/pubsub.go b/stanza/pubsub.go similarity index 98% rename from pubsub.go rename to stanza/pubsub.go index 42b28e0..010f8b0 100644 --- a/pubsub.go +++ b/stanza/pubsub.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/registry.go b/stanza/registry.go similarity index 99% rename from registry.go rename to stanza/registry.go index 8edacb4..beb35df 100644 --- a/registry.go +++ b/stanza/registry.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/registry_test.go b/stanza/registry_test.go similarity index 98% rename from registry_test.go rename to stanza/registry_test.go index 63feea3..02d321b 100644 --- a/registry_test.go +++ b/stanza/registry_test.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/starttls.go b/stanza/starttls.go similarity index 88% rename from starttls.go rename to stanza/starttls.go index edb20ec..e0a187c 100644 --- a/starttls.go +++ b/stanza/starttls.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "crypto/tls" @@ -8,7 +8,7 @@ import ( var DefaultTlsConfig tls.Config // Used during stream initiation / session establishment -type tlsProceed struct { +type TLSProceed struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"` } diff --git a/stream.go b/stanza/stream.go similarity index 99% rename from stream.go rename to stanza/stream.go index 0173254..221e7fb 100644 --- a/stream.go +++ b/stanza/stream.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "encoding/xml" diff --git a/xmpp_test.go b/stanza/xmpp_test.go similarity index 97% rename from xmpp_test.go rename to stanza/xmpp_test.go index 7f7e6de..2b24526 100644 --- a/xmpp_test.go +++ b/stanza/xmpp_test.go @@ -1,4 +1,4 @@ -package xmpp_test +package stanza_test import ( "encoding/xml" diff --git a/stream_manager.go b/stream_manager.go index 9c7e020..c2dd78b 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -6,6 +6,7 @@ import ( "time" "golang.org/x/xerrors" + "gosrc.io/xmpp/stanza" ) // The Fluux XMPP lib can manage client or component XMPP streams. @@ -29,7 +30,7 @@ type StreamClient interface { // Sender is an interface provided by Stream clients to allow sending XMPP data. type Sender interface { - Send(packet Packet) error + Send(packet stanza.Packet) error SendRaw(packet string) error } diff --git a/stream_test.go b/stream_test.go index fac1446..663cad9 100644 --- a/stream_test.go +++ b/stream_test.go @@ -4,14 +4,14 @@ import ( "encoding/xml" "testing" - "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" ) func TestNoStartTLS(t *testing.T) { streamFeatures := ` ` - var parsedSF xmpp.StreamFeatures + var parsedSF stanza.StreamFeatures if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil { t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err) } @@ -32,7 +32,7 @@ func TestStartTLS(t *testing.T) {
` - var parsedSF xmpp.StreamFeatures + var parsedSF stanza.StreamFeatures if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil { t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err) } @@ -52,7 +52,7 @@ func TestStreamManagement(t *testing.T) { ` - var parsedSF xmpp.StreamFeatures + var parsedSF stanza.StreamFeatures if err := xml.Unmarshal([]byte(streamFeatures), &parsedSF); err != nil { t.Errorf("Unmarshal(%s) returned error: %v", streamFeatures, err) } From 5ed66de79e1b76a637f30e9c059452294cab2c16 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Wed, 26 Jun 2019 17:28:54 +0200 Subject: [PATCH 2/9] Fix tests after refactor --- stanza/iq_test.go | 37 +++++++++++++++++++------------------ stanza/message_test.go | 15 ++++++++------- stanza/msg_html_test.go | 12 +++++++----- stanza/msg_receipts_test.go | 6 ++++-- stanza/pres_muc_test.go | 10 ++++++---- stanza/presence_test.go | 17 +++++++++-------- 6 files changed, 53 insertions(+), 44 deletions(-) diff --git a/stanza/iq_test.go b/stanza/iq_test.go index a880192..39df2aa 100644 --- a/stanza/iq_test.go +++ b/stanza/iq_test.go @@ -6,21 +6,22 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "gosrc.io/xmpp/stanza" ) func TestUnmarshalIqs(t *testing.T) { //var cs1 = new(iot.ControlSet) var tests = []struct { iqString string - parsedIQ IQ + parsedIQ stanza.IQ }{ {"", - IQ{XMLName: xml.Name{Local: "iq"}, Attrs: Attrs{Type: IQTypeSet, To: "test@localhost", Id: "1"}}}, + stanza.IQ{XMLName: xml.Name{Local: "iq"}, Attrs: stanza.Attrs{Type: stanza.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}}, } for _, test := range tests { - parsedIQ := IQ{} + parsedIQ := stanza.IQ{} err := xml.Unmarshal([]byte(test.iqString), &parsedIQ) if err != nil { t.Errorf("Unmarshal(%s) returned error", test.iqString) @@ -34,16 +35,16 @@ func TestUnmarshalIqs(t *testing.T) { } func TestGenerateIq(t *testing.T) { - iq := NewIQ(Attrs{Type: IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) - payload := DiscoInfo{ - Identity: Identity{ + iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) + payload := stanza.DiscoInfo{ + Identity: stanza.Identity{ Name: "Test Gateway", Category: "gateway", Type: "mqtt", }, - Features: []Feature{ - {Var: NSDiscoInfo}, - {Var: NSDiscoItems}, + Features: []stanza.Feature{ + {Var: stanza.NSDiscoInfo}, + {Var: stanza.NSDiscoItems}, }, } iq.Payload = &payload @@ -57,7 +58,7 @@ func TestGenerateIq(t *testing.T) { t.Error("empty error should not be serialized") } - parsedIQ := IQ{} + parsedIQ := stanza.IQ{} if err = xml.Unmarshal(data, &parsedIQ); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -68,7 +69,7 @@ func TestGenerateIq(t *testing.T) { } func TestErrorTag(t *testing.T) { - xError := Err{ + xError := stanza.Err{ XMLName: xml.Name{Local: "error"}, Code: 503, Type: "cancel", @@ -81,7 +82,7 @@ func TestErrorTag(t *testing.T) { t.Errorf("cannot marshal xml structure: %s", err) } - parsedError := Err{} + parsedError := stanza.Err{} if err = xml.Unmarshal(data, &parsedError); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -92,8 +93,8 @@ func TestErrorTag(t *testing.T) { } func TestDiscoItems(t *testing.T) { - iq := NewIQ(Attrs{Type: IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) - payload := DiscoItems{ + iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) + payload := stanza.DiscoItems{ Node: "music", } iq.Payload = &payload @@ -103,7 +104,7 @@ func TestDiscoItems(t *testing.T) { t.Errorf("cannot marshal xml structure") } - parsedIQ := IQ{} + parsedIQ := stanza.IQ{} if err = xml.Unmarshal(data, &parsedIQ); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -116,7 +117,7 @@ func TestDiscoItems(t *testing.T) { func TestUnmarshalPayload(t *testing.T) { query := "" - parsedIQ := IQ{} + parsedIQ := stanza.IQ{} err := xml.Unmarshal([]byte(query), &parsedIQ) if err != nil { t.Errorf("Unmarshal(%s) returned error", query) @@ -141,7 +142,7 @@ func TestPayloadWithError(t *testing.T) { ` - parsedIQ := IQ{} + parsedIQ := stanza.IQ{} err := xml.Unmarshal([]byte(iq), &parsedIQ) if err != nil { t.Errorf("Unmarshal error: %s", iq) @@ -157,7 +158,7 @@ func TestUnknownPayload(t *testing.T) { iq := ` ` - parsedIQ := IQ{} + parsedIQ := stanza.IQ{} err := xml.Unmarshal([]byte(iq), &parsedIQ) if err != nil { t.Errorf("Unmarshal error: %#v (%s)", err, iq) diff --git a/stanza/message_test.go b/stanza/message_test.go index 9427c4a..4681694 100644 --- a/stanza/message_test.go +++ b/stanza/message_test.go @@ -5,10 +5,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "gosrc.io/xmpp/stanza" ) func TestGenerateMessage(t *testing.T) { - message := NewMessage(Attrs{Type: MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"}) + message := stanza.NewMessage(stanza.Attrs{Type: stanza.MessageTypeChat, From: "admin@localhost", To: "test@localhost", Id: "1"}) message.Body = "Hi" message.Subject = "Msg Subject" @@ -17,7 +18,7 @@ func TestGenerateMessage(t *testing.T) { t.Errorf("cannot marshal xml structure") } - parsedMessage := Message{} + parsedMessage := stanza.Message{} if err = xml.Unmarshal(data, &parsedMessage); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -37,7 +38,7 @@ func TestDecodeError(t *testing.T) { ` - parsedMessage := Message{} + parsedMessage := stanza.Message{} if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { t.Errorf("message error stanza unmarshall error: %v", err) return @@ -49,15 +50,15 @@ func TestDecodeError(t *testing.T) { func TestGetOOB(t *testing.T) { image := "https://localhost/image.png" - msg := NewMessage(Attrs{To: "test@localhost"}) - ext := OOB{ + msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"}) + ext := stanza.OOB{ XMLName: xml.Name{Space: "jabber:x:oob", Local: "x"}, URL: image, } msg.Extensions = append(msg.Extensions, &ext) // OOB can properly be found - var oob OOB + var oob stanza.OOB // Try to find and if ok := msg.Get(&oob); !ok { t.Error("could not find oob extension") @@ -68,7 +69,7 @@ func TestGetOOB(t *testing.T) { } // Markable is not found - var m Markable + var m stanza.Markable if ok := msg.Get(&m); ok { t.Error("we should not have found markable extension") } diff --git a/stanza/msg_html_test.go b/stanza/msg_html_test.go index 1dbaa00..b37e9d6 100644 --- a/stanza/msg_html_test.go +++ b/stanza/msg_html_test.go @@ -3,16 +3,18 @@ package stanza_test import ( "encoding/xml" "testing" + + "gosrc.io/xmpp/stanza" ) func TestHTMLGen(t *testing.T) { htmlBody := "

Hello World

" - msg := NewMessage(Attrs{To: "test@localhost"}) + msg := stanza.NewMessage(stanza.Attrs{To: "test@localhost"}) msg.Body = "Hello World" - body := HTMLBody{ + body := stanza.HTMLBody{ InnerXML: htmlBody, } - html := HTML{Body: body} + html := stanza.HTML{Body: body} msg.Extensions = append(msg.Extensions, html) result := msg.XMPPFormat() @@ -21,7 +23,7 @@ func TestHTMLGen(t *testing.T) { t.Errorf("incorrect serialize message:\n%s", result) } - parsedMessage := Message{} + parsedMessage := stanza.Message{} if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { t.Errorf("message HTML unmarshall error: %v", err) return @@ -31,7 +33,7 @@ func TestHTMLGen(t *testing.T) { t.Errorf("incorrect parsed body: '%s'", parsedMessage.Body) } - var h HTML + var h stanza.HTML if ok := parsedMessage.Get(&h); !ok { t.Error("could not extract HTML body") } diff --git a/stanza/msg_receipts_test.go b/stanza/msg_receipts_test.go index 4b9971c..bf379a1 100644 --- a/stanza/msg_receipts_test.go +++ b/stanza/msg_receipts_test.go @@ -3,6 +3,8 @@ package stanza_test import ( "encoding/xml" "testing" + + "gosrc.io/xmpp/stanza" ) func TestDecodeRequest(t *testing.T) { @@ -13,7 +15,7 @@ func TestDecodeRequest(t *testing.T) { My lord, dispatch; read o'er these articles. ` - parsedMessage := Message{} + parsedMessage := stanza.Message{} if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { t.Errorf("message receipt unmarshall error: %v", err) return @@ -29,7 +31,7 @@ func TestDecodeRequest(t *testing.T) { } switch ext := parsedMessage.Extensions[0].(type) { - case *ReceiptRequest: + case *stanza.ReceiptRequest: if ext.XMLName.Local != "request" { t.Errorf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local) } diff --git a/stanza/pres_muc_test.go b/stanza/pres_muc_test.go index 6ea2ab6..78fe29f 100644 --- a/stanza/pres_muc_test.go +++ b/stanza/pres_muc_test.go @@ -3,6 +3,8 @@ package stanza_test import ( "encoding/xml" "testing" + + "gosrc.io/xmpp/stanza" ) // https://xmpp.org/extensions/xep-0045.html#example-27 @@ -16,12 +18,12 @@ func TestMucPassword(t *testing.T) { ` - var parsedPresence Presence + var parsedPresence stanza.Presence if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { t.Errorf("Unmarshal(%s) returned error", str) } - var muc MucPresence + var muc stanza.MucPresence if ok := parsedPresence.Get(&muc); !ok { t.Error("muc presence extension was not found") } @@ -42,12 +44,12 @@ func TestMucHistory(t *testing.T) { ` - var parsedPresence Presence + var parsedPresence stanza.Presence if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { t.Errorf("Unmarshal(%s) returned error", str) } - var muc MucPresence + var muc stanza.MucPresence if ok := parsedPresence.Get(&muc); !ok { t.Error("muc presence extension was not found") } diff --git a/stanza/presence_test.go b/stanza/presence_test.go index 0433857..94431d8 100644 --- a/stanza/presence_test.go +++ b/stanza/presence_test.go @@ -5,18 +5,19 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "gosrc.io/xmpp/stanza" ) func TestGeneratePresence(t *testing.T) { - presence := NewPresence(Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) - presence.Show = PresenceShowChat + presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) + presence.Show = stanza.PresenceShowChat data, err := xml.Marshal(presence) if err != nil { t.Errorf("cannot marshal xml structure") } - var parsedPresence Presence + var parsedPresence stanza.Presence if err = xml.Unmarshal(data, &parsedPresence); err != nil { t.Errorf("Unmarshal(%s) returned error", data) } @@ -30,13 +31,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 PresenceShow `xml:"show"` - Status string `xml:"status"` - Priority int8 `xml:"priority"` + Show stanza.PresenceShow `xml:"show"` + Status string `xml:"status"` + Priority int8 `xml:"priority"` } - presence := NewPresence(Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) - presence.Show = PresenceShowXA + presence := stanza.NewPresence(stanza.Attrs{From: "admin@localhost", To: "test@localhost", Id: "1"}) + presence.Show = stanza.PresenceShowXA presence.Status = "Coding" presence.Priority = 10 From 781b875cf1620cd7540fe936252e879bc7e91342 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Wed, 26 Jun 2019 18:42:40 +0200 Subject: [PATCH 3/9] Resync with Master Support NullableInt on MUC presence history element --- stanza/pres_muc.go | 127 ++++++++++++++++++++++++++++++++++++++-- stanza/pres_muc_test.go | 43 +++++++++++++- 2 files changed, 163 insertions(+), 7 deletions(-) diff --git a/stanza/pres_muc.go b/stanza/pres_muc.go index e944fb6..bc0e75e 100644 --- a/stanza/pres_muc.go +++ b/stanza/pres_muc.go @@ -2,6 +2,7 @@ package stanza import ( "encoding/xml" + "strconv" "time" ) @@ -16,12 +17,130 @@ type MucPresence struct { History History `xml:"history,omitempty"` } +const timeLayout = "2006-01-02T15:04:05Z" + // History implements XEP-0045: Multi-User Chat - 19.1 type History struct { - MaxChars int `xml:"maxchars,attr,omitempty"` - MaxStanzas int `xml:"maxstanzas,attr,omitempty"` - Seconds int `xml:"seconds,attr,omitempty"` - Since time.Time `xml:"since,attr,omitempty"` + XMLName xml.Name + MaxChars NullableInt `xml:"maxchars,attr,omitempty"` + MaxStanzas NullableInt `xml:"maxstanzas,attr,omitempty"` + Seconds NullableInt `xml:"seconds,attr,omitempty"` + Since time.Time `xml:"since,attr,omitempty"` +} + +type NullableInt struct { + Value int + isSet bool +} + +func NewNullableInt(val int) NullableInt { + return NullableInt{val, true} +} + +func (n NullableInt) Get() (v int, ok bool) { + return n.Value, n.isSet +} + +// UnmarshalXML implements custom parsing for history element +func (h *History) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + h.XMLName = start.Name + + // Extract attributes + for _, attr := range start.Attr { + switch attr.Name.Local { + case "maxchars": + v, err := strconv.Atoi(attr.Value) + if err != nil { + return err + } + h.MaxChars = NewNullableInt(v) + case "maxstanzas": + v, err := strconv.Atoi(attr.Value) + if err != nil { + return err + } + h.MaxStanzas = NewNullableInt(v) + case "seconds": + v, err := strconv.Atoi(attr.Value) + if err != nil { + return err + } + h.Seconds = NewNullableInt(v) + case "since": + t, err := time.Parse(timeLayout, attr.Value) + if err != nil { + return err + } + h.Since = t + } + } + + // Consume remaining data until element end + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + +func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { + mc, isMcSet := h.MaxChars.Get() + ms, isMsSet := h.MaxStanzas.Get() + s, isSSet := h.Seconds.Get() + + // We do not have any value, ignore history element + if h.Since.IsZero() && !isMcSet && !isMsSet && !isSSet { + return nil + } + + // Encode start element and attributes + start.Name = xml.Name{Local: "history"} + + if isMcSet { + attr := xml.Attr{ + Name: xml.Name{Local: "maxchars"}, + Value: strconv.Itoa(mc), + } + start.Attr = append(start.Attr, attr) + } + + if isMsSet { + attr := xml.Attr{ + Name: xml.Name{Local: "maxstanzas"}, + Value: strconv.Itoa(ms), + } + start.Attr = append(start.Attr, attr) + } + + if isSSet { + attr := xml.Attr{ + Name: xml.Name{Local: "seconds"}, + Value: strconv.Itoa(s), + } + start.Attr = append(start.Attr, attr) + } + + if !h.Since.IsZero() { + attr := xml.Attr{ + Name: xml.Name{Local: "since"}, + Value: h.Since.Format(timeLayout), + } + start.Attr = append(start.Attr, attr) + } + if err := e.EncodeToken(start); err != nil { + return err + } + + return e.EncodeToken(xml.EndElement{Name: start.Name}) + } func init() { diff --git a/stanza/pres_muc_test.go b/stanza/pres_muc_test.go index 78fe29f..209dc93 100644 --- a/stanza/pres_muc_test.go +++ b/stanza/pres_muc_test.go @@ -46,15 +46,52 @@ func TestMucHistory(t *testing.T) { var parsedPresence stanza.Presence if err := xml.Unmarshal([]byte(str), &parsedPresence); err != nil { - t.Errorf("Unmarshal(%s) returned error", str) + t.Errorf("Unmarshal(%s) returned error: %s", str, err) + return } var muc stanza.MucPresence if ok := parsedPresence.Get(&muc); !ok { t.Error("muc presence extension was not found") + return } - if muc.History.MaxStanzas != 20 { - t.Errorf("incorrect max stanza: '%d'", muc.History.MaxStanzas) + if v, ok := muc.History.MaxStanzas.Get(); !ok || v != 20 { + t.Errorf("incorrect MaxStanzas: '%#v'", muc.History.MaxStanzas) + } +} + +// https://xmpp.org/extensions/xep-0045.html#example-37 +func TestMucNoHistory(t *testing.T) { + str := "" + + "" + + "" + + "" + + "" + + maxstanzas := 0 + + pres := stanza.Presence{Attrs: stanza.Attrs{ + From: "hag66@shakespeare.lit/pda", + Id: "n13mt3l", + To: "coven@chat.shakespeare.lit/thirdwitch", + }, + Extensions: []stanza.PresExtension{ + stanza.MucPresence{ + History: stanza.History{MaxStanzas: stanza.NewNullableInt(maxstanzas)}, + }, + }, + } + data, err := xml.Marshal(&pres) + if err != nil { + t.Error("error on encode:", err) + return + } + + if string(data) != str { + t.Errorf("incorrect stanza: \n%s\n%s", str, data) } } From 3fa1a4b387fa35b53943698456da4ec990a8608e Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 27 Jun 2019 09:59:19 +0200 Subject: [PATCH 4/9] Remove useless reference to IQPayload --- stanza/iot.go | 1 - 1 file changed, 1 deletion(-) diff --git a/stanza/iot.go b/stanza/iot.go index 5235089..cd3b404 100644 --- a/stanza/iot.go +++ b/stanza/iot.go @@ -24,7 +24,6 @@ type ControlField struct { } type ControlSetResponse struct { - IQPayload XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"` } From 0c7e4588c6c82a4930a71d36243d480d9b8bd457 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 27 Jun 2019 10:21:33 +0200 Subject: [PATCH 5/9] Add initial documentation --- stanza/README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 stanza/README.md diff --git a/stanza/README.md b/stanza/README.md new file mode 100644 index 0000000..fa93acf --- /dev/null +++ b/stanza/README.md @@ -0,0 +1,80 @@ +# XMPP Stanza + +XMPP `stanza` package is use to parse, marshall and and unmarshal XMPP stanzas and nonzas. + +## Stanza creation + +When creating stanzas, you can use two approaches: + +1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the +stanza. +2. You can use `stanza` Builder providing + +The methods are equivalent and you can use whatever suits you best. The Builder will generate the same type of +struct that you can build by hand. + +### Composing stanzas manually with structs + +Here is for example how you would generate an IQ discovery result: + + iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + identity := stanza.Identity{ + Name: opts.Name, + Category: opts.Category, + Type: opts.Type, + } + payload := stanza.DiscoInfo{ + XMLName: xml.Name{ + Space: stanza.NSDiscoInfo, + Local: "query", + }, + Identity: identity, + Features: []stanza.Feature{ + {Var: stanza.NSDiscoInfo}, + {Var: stanza.NSDiscoItems}, + {Var: "jabber:iq:version"}, + {Var: "urn:xmpp:delegation:1"}, + }, + } + iqResp.Payload = &payload + +### Using Builder + +Here is for example how you would generate an IQ discovery result using Builder: + + b := stanza.NewBuilder() + iq := b.IQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"}) + + payload := b.DiscoInfo() + identity := b.Identity("Test Component", "gateway", "service") + payload.SetFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"). + SetIdentities(identity) + + iq.Payload = payload + +## Payload and extensions + +### Message + +### Presence + +### IQ + +IQ (Information Queries) contain a payload associated with the request and possibly an error. The main difference with +Message and Presence extension is that you can only have one payload per IQ. The XMPP specification does not support +having multiple payloads. + +Here is the list of structs implementing IQPayloads: + +- BindBind +- Pubsub + +Finally, when the payload of the parsed stanza is unknown, the parser will provide the unknown payload as a generic +`Node` element. You can also use the Node struct to add custom information on stanza generation. However, in both cases, +you may also consider [adding your own custom extensions on stanzas](). + + +## Adding your own custom extensions on stanzas + +Extensions are registered on launch using the `Registry`. It can be used to register you own custom payload. You may +want to do so to support extensions we did not yet implement, or to add your own custom extensions to your XMPP stanzas. From cb9016693ce18d0e2e2fed77d03225ce3986043e Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 27 Jun 2019 10:22:36 +0200 Subject: [PATCH 6/9] Move some IQ declaration in their own files --- stanza/auth_sasl.go | 7 ++++ stanza/iot.go | 7 ++++ stanza/iq.go | 76 -------------------------------------------- stanza/iq_disco.go | 61 +++++++++++++++++++++++++++++++++++ stanza/iq_version.go | 25 +++++++++++++++ 5 files changed, 100 insertions(+), 76 deletions(-) create mode 100644 stanza/iq_disco.go create mode 100644 stanza/iq_version.go diff --git a/stanza/auth_sasl.go b/stanza/auth_sasl.go index a5ea907..5961d9f 100644 --- a/stanza/auth_sasl.go +++ b/stanza/auth_sasl.go @@ -70,3 +70,10 @@ type sessionSession struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` Optional xml.Name // If it does exist, it mean we are not required to open session } + +// ============================================================================ +// Registry init + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{}) +} diff --git a/stanza/iot.go b/stanza/iot.go index cd3b404..5e15056 100644 --- a/stanza/iot.go +++ b/stanza/iot.go @@ -30,3 +30,10 @@ type ControlSetResponse struct { func (c *ControlSetResponse) Namespace() string { return c.XMLName.Space } + +// ============================================================================ +// Registry init + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{}) +} diff --git a/stanza/iq.go b/stanza/iq.go index 24b65cc..34c74bb 100644 --- a/stanza/iq.go +++ b/stanza/iq.go @@ -128,79 +128,3 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } } } - -// ============================================================================ -// Disco - -const ( - NSDiscoInfo = "http://jabber.org/protocol/disco#info" - NSDiscoItems = "http://jabber.org/protocol/disco#items" -) - -// Disco Info -type DiscoInfo struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` - Node string `xml:"node,attr,omitempty"` - Identity Identity `xml:"identity"` - Features []Feature `xml:"feature"` -} - -func (d *DiscoInfo) Namespace() string { - return d.XMLName.Space -} - -type Identity struct { - XMLName xml.Name `xml:"identity,omitempty"` - Name string `xml:"name,attr,omitempty"` - Category string `xml:"category,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` -} - -type Feature struct { - XMLName xml.Name `xml:"feature"` - Var string `xml:"var,attr"` -} - -// Disco Items -type DiscoItems struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` - Node string `xml:"node,attr,omitempty"` - Items []DiscoItem `xml:"item"` -} - -func (d *DiscoItems) Namespace() string { - return d.XMLName.Space -} - -type DiscoItem struct { - XMLName xml.Name `xml:"item"` - Name string `xml:"name,attr,omitempty"` - JID string `xml:"jid,attr,omitempty"` - Node string `xml:"node,attr,omitempty"` -} - -// ============================================================================ -// Software Version (XEP-0092) - -// Version -type Version struct { - XMLName xml.Name `xml:"jabber:iq:version query"` - Name string `xml:"name,omitempty"` - Version string `xml:"version,omitempty"` - OS string `xml:"os,omitempty"` -} - -func (v *Version) Namespace() string { - return v.XMLName.Space -} - -// ============================================================================ -// Registry init - -func init() { - TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, BindBind{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{}) -} diff --git a/stanza/iq_disco.go b/stanza/iq_disco.go new file mode 100644 index 0000000..da161d1 --- /dev/null +++ b/stanza/iq_disco.go @@ -0,0 +1,61 @@ +package stanza + +import "encoding/xml" + +// ============================================================================ +// Disco + +const ( + NSDiscoInfo = "http://jabber.org/protocol/disco#info" + NSDiscoItems = "http://jabber.org/protocol/disco#items" +) + +// Disco Info +type DiscoInfo struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` + Node string `xml:"node,attr,omitempty"` + Identity Identity `xml:"identity"` + Features []Feature `xml:"feature"` +} + +func (d *DiscoInfo) Namespace() string { + return d.XMLName.Space +} + +type Identity struct { + XMLName xml.Name `xml:"identity,omitempty"` + Name string `xml:"name,attr,omitempty"` + Category string `xml:"category,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` +} + +type Feature struct { + XMLName xml.Name `xml:"feature"` + Var string `xml:"var,attr"` +} + +// Disco Items +type DiscoItems struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` + Node string `xml:"node,attr,omitempty"` + Items []DiscoItem `xml:"item"` +} + +func (d *DiscoItems) Namespace() string { + return d.XMLName.Space +} + +type DiscoItem struct { + XMLName xml.Name `xml:"item"` + Name string `xml:"name,attr,omitempty"` + JID string `xml:"jid,attr,omitempty"` + Node string `xml:"node,attr,omitempty"` +} + +// ============================================================================ +// Registry init + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoInfo, "query"}, DiscoInfo{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{NSDiscoItems, "query"}, DiscoItems{}) +} diff --git a/stanza/iq_version.go b/stanza/iq_version.go new file mode 100644 index 0000000..ebf4452 --- /dev/null +++ b/stanza/iq_version.go @@ -0,0 +1,25 @@ +package stanza + +import "encoding/xml" + +// ============================================================================ +// Software Version (XEP-0092) + +// Version +type Version struct { + XMLName xml.Name `xml:"jabber:iq:version query"` + Name string `xml:"name,omitempty"` + Version string `xml:"version,omitempty"` + OS string `xml:"os,omitempty"` +} + +func (v *Version) Namespace() string { + return v.XMLName.Space +} + +// ============================================================================ +// Registry init + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{}) +} From 1dacc663d3404fc8cbf292a52286e5efeb533663 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 27 Jun 2019 10:23:49 +0200 Subject: [PATCH 7/9] Add basic builder support --- stanza/builder.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 stanza/builder.go diff --git a/stanza/builder.go b/stanza/builder.go new file mode 100644 index 0000000..23d131f --- /dev/null +++ b/stanza/builder.go @@ -0,0 +1,62 @@ +package stanza + +import ( + "encoding/xml" +) + +type builder struct{ lang string } + +// NewBuilder create a builder structure. It act as an interface for packet generation. +// The goal is to work well with code completion to more easily. +// +// Using the builder to format and create packets is optional. You can always prepare +// your packet dealing with the struct manually and initializing them with the right values. +func NewBuilder() *builder { + return &builder{} +} + +// Set default language +func (b *builder) Lang(lang string) *builder { + b.lang = lang + return b +} + +func (b *builder) IQ(a Attrs) IQ { + return IQ{ + XMLName: xml.Name{Local: "iq"}, + Attrs: a, + } +} + +func (b *builder) Message(a Attrs) Message { + return Message{ + XMLName: xml.Name{Local: "message"}, + Attrs: a, + } +} + +func (b *builder) Presence(a Attrs) Presence { + return Presence{ + XMLName: xml.Name{Local: "presence"}, + Attrs: a, + } +} + +// ====================================================================================== +// IQ payloads + +// DiscoInfo builds a default DiscoInfo payload +func (*builder) DiscoInfo() *DiscoInfo { + d := DiscoInfo{ + XMLName: xml.Name{ + Space: NSDiscoInfo, + Local: "query", + }, + } + return &d +} + +// Identity builds a identity struct for use in Disco +func (*builder) Identity(name, category, typ string) *Identity { + return &Identity{} +} From 20a66dc47dc30c9fdb727a09625020fdf0109dd5 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 27 Jun 2019 14:30:23 +0200 Subject: [PATCH 8/9] Use an approach to build stanza that do not require a "builder" abstraction --- _examples/delegation/delegation.go | 4 +- stanza/README.md | 24 ++++----- stanza/builder.go | 62 ---------------------- stanza/iq_disco.go | 85 +++++++++++++++++++++++++----- stanza/iq_disco_test.go | 55 +++++++++++++++++++ stanza/iq_test.go | 14 ++--- stanza/xmpp_test.go | 15 +++++- 7 files changed, 159 insertions(+), 100 deletions(-) delete mode 100644 stanza/builder.go create mode 100644 stanza/iq_disco_test.go diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index 9e26b69..31c18d3 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -110,7 +110,7 @@ func discoInfoRoot(iqResp *stanza.IQ, opts xmpp.ComponentOptions) { Space: stanza.NSDiscoInfo, Local: "query", }, - Identity: identity, + Identity: []stanza.Identity{identity}, Features: []stanza.Feature{ {Var: stanza.NSDiscoInfo}, {Var: stanza.NSDiscoItems}, @@ -148,7 +148,7 @@ func discoInfoPEP(iqResp *stanza.IQ) { Space: stanza.NSDiscoInfo, Local: "query", }, - Identity: identity, + Identity: []stanza.Identity{identity}, Node: pepNode, Features: []stanza.Feature{ {Var: "http://jabber.org/protocol/pubsub#access-presence"}, diff --git a/stanza/README.md b/stanza/README.md index fa93acf..466f594 100644 --- a/stanza/README.md +++ b/stanza/README.md @@ -8,10 +8,11 @@ When creating stanzas, you can use two approaches: 1. You can create IQ, Presence or Message structs, set the fields and manually prepare extensions struct to add to the stanza. -2. You can use `stanza` Builder providing +2. You can use `stanza` build helper to be guided when creating the stanza, and have more controls performed on the +final stanza. -The methods are equivalent and you can use whatever suits you best. The Builder will generate the same type of -struct that you can build by hand. +The methods are equivalent and you can use whatever suits you best. The helpers will finally generate the same type of +struct that you can build by hand. ### Composing stanzas manually with structs @@ -28,7 +29,7 @@ Here is for example how you would generate an IQ discovery result: Space: stanza.NSDiscoInfo, Local: "query", }, - Identity: identity, + Identity: []stanza.Identity{identity}, Features: []stanza.Feature{ {Var: stanza.NSDiscoInfo}, {Var: stanza.NSDiscoItems}, @@ -38,19 +39,14 @@ Here is for example how you would generate an IQ discovery result: } iqResp.Payload = &payload -### Using Builder +### Using helpers Here is for example how you would generate an IQ discovery result using Builder: - b := stanza.NewBuilder() - iq := b.IQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"}) - - payload := b.DiscoInfo() - identity := b.Identity("Test Component", "gateway", "service") - payload.SetFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"). - SetIdentities(identity) - - iq.Payload = payload + iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"}) + disco := iq.DiscoInfo() + disco.AddIdentity("Test Component", "gateway", "service") + disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1") ## Payload and extensions diff --git a/stanza/builder.go b/stanza/builder.go deleted file mode 100644 index 23d131f..0000000 --- a/stanza/builder.go +++ /dev/null @@ -1,62 +0,0 @@ -package stanza - -import ( - "encoding/xml" -) - -type builder struct{ lang string } - -// NewBuilder create a builder structure. It act as an interface for packet generation. -// The goal is to work well with code completion to more easily. -// -// Using the builder to format and create packets is optional. You can always prepare -// your packet dealing with the struct manually and initializing them with the right values. -func NewBuilder() *builder { - return &builder{} -} - -// Set default language -func (b *builder) Lang(lang string) *builder { - b.lang = lang - return b -} - -func (b *builder) IQ(a Attrs) IQ { - return IQ{ - XMLName: xml.Name{Local: "iq"}, - Attrs: a, - } -} - -func (b *builder) Message(a Attrs) Message { - return Message{ - XMLName: xml.Name{Local: "message"}, - Attrs: a, - } -} - -func (b *builder) Presence(a Attrs) Presence { - return Presence{ - XMLName: xml.Name{Local: "presence"}, - Attrs: a, - } -} - -// ====================================================================================== -// IQ payloads - -// DiscoInfo builds a default DiscoInfo payload -func (*builder) DiscoInfo() *DiscoInfo { - d := DiscoInfo{ - XMLName: xml.Name{ - Space: NSDiscoInfo, - Local: "query", - }, - } - return &d -} - -// Identity builds a identity struct for use in Disco -func (*builder) Identity(name, category, typ string) *Identity { - return &Identity{} -} diff --git a/stanza/iq_disco.go b/stanza/iq_disco.go index da161d1..0097c05 100644 --- a/stanza/iq_disco.go +++ b/stanza/iq_disco.go @@ -1,27 +1,80 @@ package stanza -import "encoding/xml" - -// ============================================================================ -// Disco - -const ( - NSDiscoInfo = "http://jabber.org/protocol/disco#info" - NSDiscoItems = "http://jabber.org/protocol/disco#items" +import ( + "encoding/xml" ) +// ============================================================================ // Disco Info + +const ( + NSDiscoInfo = "http://jabber.org/protocol/disco#info" +) + +// ---------- +// Namespaces + type DiscoInfo struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` - Node string `xml:"node,attr,omitempty"` - Identity Identity `xml:"identity"` - Features []Feature `xml:"feature"` + XMLName xml.Name `xml:"http://jabber.org/protocol/disco#info query"` + Node string `xml:"node,attr,omitempty"` + Identity []Identity `xml:"identity"` + Features []Feature `xml:"feature"` } func (d *DiscoInfo) Namespace() string { return d.XMLName.Space } +// --------------- +// Builder helpers + +// DiscoInfo builds a default DiscoInfo payload +func (iq *IQ) DiscoInfo() *DiscoInfo { + d := DiscoInfo{ + XMLName: xml.Name{ + Space: NSDiscoInfo, + Local: "query", + }, + } + iq.Payload = &d + return &d +} + +func (d *DiscoInfo) AddIdentity(name, category, typ string) { + identity := Identity{ + XMLName: xml.Name{Local: "identity"}, + Name: name, + Category: category, + Type: typ, + } + d.Identity = append(d.Identity, identity) +} + +func (d *DiscoInfo) AddFeatures(namespace ...string) { + for _, ns := range namespace { + d.Features = append(d.Features, Feature{Var: ns}) + } +} + +func (d *DiscoInfo) SetNode(node string) { + d.Node = node +} + +func (d *DiscoInfo) SetIdentities(ident ...Identity) *DiscoInfo { + d.Identity = ident + return d +} + +func (d *DiscoInfo) SetFeatures(namespace ...string) *DiscoInfo { + for _, ns := range namespace { + d.Features = append(d.Features, Feature{Var: ns}) + } + return d +} + +// ----------- +// SubElements + type Identity struct { XMLName xml.Name `xml:"identity,omitempty"` Name string `xml:"name,attr,omitempty"` @@ -34,7 +87,13 @@ type Feature struct { Var string `xml:"var,attr"` } -// Disco Items +// ============================================================================ +// Disco Info + +const ( + NSDiscoItems = "http://jabber.org/protocol/disco#items" +) + type DiscoItems struct { XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` Node string `xml:"node,attr,omitempty"` diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go new file mode 100644 index 0000000..8b76767 --- /dev/null +++ b/stanza/iq_disco_test.go @@ -0,0 +1,55 @@ +package stanza_test + +import ( + "encoding/xml" + "testing" + + "gosrc.io/xmpp/stanza" +) + +func TestDiscoInfoBuilder(t *testing.T) { + iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"}) + disco := iq.DiscoInfo() + disco.AddIdentity("Test Component", "gateway", "service") + disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1") + + // Marshall + data, err := xml.Marshal(iq) + if err != nil { + t.Errorf("cannot marshal xml structure: %s", err) + return + } + + // Unmarshall + var parsedIQ stanza.IQ + if err = xml.Unmarshal(data, &parsedIQ); err != nil { + t.Errorf("Unmarshal(%s) returned error: %s", data, err) + } + + // Check result + pp, ok := parsedIQ.Payload.(*stanza.DiscoInfo) + if !ok { + t.Errorf("Parsed stanza does not contain an IQ payload") + } + + // Check features + features := []string{stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1"} + if len(pp.Features) != len(features) { + t.Errorf("Features length mismatch: %#v", pp.Features) + } else { + for i, f := range pp.Features { + if f.Var != features[i] { + t.Errorf("Missing feature: %s", features[i]) + } + } + } + + // Check identity + if len(pp.Identity) != 1 { + t.Errorf("Identity length mismatch: %#v", pp.Identity) + } else { + if pp.Identity[0].Name != "Test Component" { + t.Errorf("Incorrect identity name: %#v", pp.Identity[0].Name) + } + } +} diff --git a/stanza/iq_test.go b/stanza/iq_test.go index 39df2aa..04a868a 100644 --- a/stanza/iq_test.go +++ b/stanza/iq_test.go @@ -37,11 +37,11 @@ func TestUnmarshalIqs(t *testing.T) { func TestGenerateIq(t *testing.T) { iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) payload := stanza.DiscoInfo{ - Identity: stanza.Identity{ - Name: "Test Gateway", - Category: "gateway", - Type: "mqtt", - }, + Identity: []stanza.Identity{ + {Name: "Test Gateway", + Category: "gateway", + Type: "mqtt", + }}, Features: []stanza.Feature{ {Var: stanza.NSDiscoInfo}, {Var: stanza.NSDiscoItems}, @@ -63,8 +63,8 @@ func TestGenerateIq(t *testing.T) { t.Errorf("Unmarshal(%s) returned error", data) } - if !xmlEqual(parsedIQ.Payload, iq.Payload) { - t.Errorf("non matching items\n%s", cmp.Diff(parsedIQ.Payload, iq.Payload)) + if !xmlEqual(iq.Payload, parsedIQ.Payload) { + t.Errorf("non matching items\n%s", xmlDiff(iq.Payload, parsedIQ.Payload)) } } diff --git a/stanza/xmpp_test.go b/stanza/xmpp_test.go index 2b24526..611948a 100644 --- a/stanza/xmpp_test.go +++ b/stanza/xmpp_test.go @@ -10,6 +10,15 @@ import ( // marshal / unmarshal. There is no need to manage them on the manually // crafted structure. func xmlEqual(x, y interface{}) bool { + return cmp.Equal(x, y, xmlOpts()) +} + +// xmlDiff compares xml structures ignoring namespace preferences +func xmlDiff(x, y interface{}) string { + return cmp.Diff(x, y, xmlOpts()) +} + +func xmlOpts() cmp.Options { alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) opts := cmp.Options{ cmp.FilterValues(func(x, y interface{}) bool { @@ -20,10 +29,12 @@ func xmlEqual(x, y interface{}) bool { if xx == zero || yy == zero { return true } + if xx.Space == "" || yy.Space == "" { + return true + } } return false }, alwaysEqual), } - - return cmp.Equal(x, y, opts) + return opts } From 5db9a80605ec802344d826cbc16dd12cd094700d Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Thu, 27 Jun 2019 14:35:03 +0200 Subject: [PATCH 9/9] Move example to new data structure --- _examples/xmpp_component/xmpp_component.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index f07704b..4bc906b 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -71,7 +71,7 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { Space: stanza.NSDiscoInfo, Local: "query", }, - Identity: identity, + Identity: []stanza.Identity{identity}, Features: []stanza.Feature{ {Var: stanza.NSDiscoInfo}, {Var: stanza.NSDiscoItems},