From 1539e4f193595f6c185a6d2a2e02bf0aac8c8bd1 Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Tue, 5 Nov 2019 14:03:56 +0100 Subject: [PATCH 01/54] Setup GitHub actions to run tests --- .github/workflows/test.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..f36a1df --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,27 @@ +name: Run tests + +on: [push, pull_request] + pull_request: + paths: + - '**.go' + - '**/go.*' + - .github/workflows/test.yaml + + push: + paths: + - '**.go' + - '**/go.*' + - .github/workflows/test.yaml + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + - uses: actions/checkout@v1 + - name: Run tests + run: go test ./... From 6aa1e668eea5b988f5b355a206fc05e32ff56a85 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Fri, 8 Nov 2019 12:07:55 +0100 Subject: [PATCH 02/54] Fix issues after refactor Relates to #126 --- _examples/go.sum | 2 ++ _examples/muc_bot/README.md | 3 +++ client.go | 1 + component.go | 13 +++++++------ stream_manager.go | 3 +++ xmpp_transport.go | 15 +++++++++++++-- 6 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 _examples/muc_bot/README.md diff --git a/_examples/go.sum b/_examples/go.sum index 6e3b0bc..19c4ace 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -23,6 +23,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -102,4 +103,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= diff --git a/_examples/muc_bot/README.md b/_examples/muc_bot/README.md new file mode 100644 index 0000000..24414fa --- /dev/null +++ b/_examples/muc_bot/README.md @@ -0,0 +1,3 @@ +# XMPP Multi-User (MUC) chat bot example + +This code shows how to build a simple basic XMPP Multi-User chat bot using Fluux Go XMPP library. diff --git a/client.go b/client.go index a7e6c7d..b7111f2 100644 --- a/client.go +++ b/client.go @@ -25,6 +25,7 @@ const ( StateConnected StateSessionEstablished StateStreamError + StatePermanentError ) // Event is a structure use to convey event changes related to client state. This diff --git a/component.go b/component.go index d459c00..d3c890e 100644 --- a/component.go +++ b/component.go @@ -64,6 +64,7 @@ func (c *Component) Connect() error { var state SMState return c.Resume(state) } + func (c *Component) Resume(sm SMState) error { var err error var streamId string @@ -72,13 +73,13 @@ func (c *Component) Resume(sm SMState) error { } c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) if err != nil { - c.updateState(StateStreamError) - return err + c.updateState(StatePermanentError) + return NewConnError(err, true) } if streamId, err = c.transport.Connect(); err != nil { - c.updateState(StateStreamError) - return err + c.updateState(StatePermanentError) + return NewConnError(err, true) } c.updateState(StateConnected) @@ -91,7 +92,7 @@ func (c *Component) Resume(sm SMState) error { // Check server response for authentication val, err := stanza.NextPacket(c.decoder) if err != nil { - c.updateState(StateDisconnected) + c.updateState(StatePermanentError) return NewConnError(err, true) } @@ -105,7 +106,7 @@ func (c *Component) Resume(sm SMState) error { go c.recv() return nil default: - c.updateState(StateStreamError) + c.updateState(StatePermanentError) return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true) } } diff --git a/stream_manager.go b/stream_manager.go index 1011f6e..bf7fba8 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -86,9 +86,12 @@ func (sm *StreamManager) Run() error { case StateStreamError: sm.client.Disconnect() // Only try reconnecting if we have not been kicked by another session to avoid connection loop. + // TODO: Make this conflict exception a permanent error if e.StreamError != "conflict" { sm.connect() } + case StatePermanentError: + // Do not attempt to reconnect } } sm.client.SetHandler(handler) diff --git a/xmpp_transport.go b/xmpp_transport.go index a67d5bc..99e0f56 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -116,16 +116,27 @@ func (t XMPPTransport) Ping() error { } func (t XMPPTransport) Read(p []byte) (n int, err error) { + if t.readWriter == nil { + return 0, errors.New("cannot read: not connected, no readwriter") + } return t.readWriter.Read(p) } func (t XMPPTransport) Write(p []byte) (n int, err error) { + if t.readWriter == nil { + return 0, errors.New("cannot write: not connected, no readwriter") + } return t.readWriter.Write(p) } func (t XMPPTransport) Close() error { - _, _ = t.readWriter.Write([]byte("")) - return t.conn.Close() + if t.readWriter != nil { + _, _ = t.readWriter.Write([]byte("")) + } + if t.conn != nil { + return t.conn.Close() + } + return nil } func (t *XMPPTransport) LogTraffic(logFile io.Writer) { From 7d89353156577787bb996397efd9eb92c12f730e Mon Sep 17 00:00:00 2001 From: remicorniere Date: Fri, 22 Nov 2019 15:07:40 +0100 Subject: [PATCH 03/54] Fix SIGSEGV in xmpp_component (#126) * SIGSEGV in xmpp_component example with Prosody #126 --- component.go | 8 +++----- component_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ websocket_transport.go | 1 + xmpp_transport.go | 1 + 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/component.go b/component.go index d3c890e..cb468db 100644 --- a/component.go +++ b/component.go @@ -7,9 +7,8 @@ import ( "encoding/xml" "errors" "fmt" - "io" - "gosrc.io/xmpp/stanza" + "io" ) type ComponentOptions struct { @@ -50,7 +49,6 @@ type Component struct { // read / write socketProxy io.ReadWriter // TODO - decoder *xml.Decoder } func NewComponent(opts ComponentOptions, r *Router) (*Component, error) { @@ -90,7 +88,7 @@ func (c *Component) Resume(sm SMState) error { } // Check server response for authentication - val, err := stanza.NextPacket(c.decoder) + val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.updateState(StatePermanentError) return NewConnError(err, true) @@ -125,7 +123,7 @@ func (c *Component) SetHandler(handler EventHandler) { // Receiver Go routine receiver func (c *Component) recv() (err error) { for { - val, err := stanza.NextPacket(c.decoder) + val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.updateState(StateDisconnected) return err diff --git a/component_test.go b/component_test.go index 5fbe45b..8938769 100644 --- a/component_test.go +++ b/component_test.go @@ -1,9 +1,13 @@ package xmpp import ( + "fmt" "testing" ) +const testComponentDomain = "localhost" +const testComponentPort = "15222" + func TestHandshake(t *testing.T) { opts := ComponentOptions{ Domain: "test.localhost", @@ -30,3 +34,41 @@ func TestGenerateHandshake(t *testing.T) { func TestStreamManager(t *testing.T) { NewStreamManager(&Component{}, nil) } + +// Tests that the decoder is properly initialized when connecting a component to a server. +// The decoder is expected to be built after a valid connection +// Based on the xmpp_component example. +func TestDecoder(t *testing.T) { + testComponentAddess := fmt.Sprintf("%s:%s", testComponentDomain, testComponentPort) + mock := ServerMock{} + mock.Start(t, testComponentAddess, handlerConnectSuccess) + + opts := ComponentOptions{ + TransportConfiguration: TransportConfiguration{ + Address: testComponentAddess, + Domain: "localhost", + }, + Domain: testComponentDomain, + Secret: "mypass", + Name: "Test Component", + Category: "gateway", + Type: "service", + } + router := NewRouter() + c, err := NewComponent(opts, router) + if err != nil { + t.Errorf("%+v", err) + } + c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) + if err != nil { + t.Errorf("%+v", err) + } + _, err = c.transport.Connect() + if err != nil { + t.Errorf("%+v", err) + } + if c.transport.GetDecoder() == nil { + t.Errorf("Failed to initialize decoder. Decoder is nil.") + } + +} diff --git a/websocket_transport.go b/websocket_transport.go index d7b62c4..69c0183 100644 --- a/websocket_transport.go +++ b/websocket_transport.go @@ -20,6 +20,7 @@ const pingTimeout = time.Duration(5) * time.Second var ServerDoesNotSupportXmppOverWebsocket = errors.New("The websocket server does not support the xmpp subprotocol") +// The decoder is expected to be initialized after connecting to a server. type WebsocketTransport struct { Config TransportConfiguration decoder *xml.Decoder diff --git a/xmpp_transport.go b/xmpp_transport.go index 99e0f56..34b0d3e 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -14,6 +14,7 @@ import ( ) // XMPPTransport implements the XMPP native TCP transport +// The decoder is expected to be initialized after connecting to a server. type XMPPTransport struct { openStatement string Config TransportConfiguration From 6f35ae4103d49fb787c82223777addc68edbbd75 Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Thu, 28 Nov 2019 09:51:49 +0100 Subject: [PATCH 04/54] Fix triggers --- .github/workflows/test.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f36a1df..0e45eaa 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,16 +1,16 @@ name: Run tests -on: [push, pull_request] +on: pull_request: paths: - '**.go' - - '**/go.*' + - 'go.*' - .github/workflows/test.yaml push: paths: - '**.go' - - '**/go.*' + - 'go.*' - .github/workflows/test.yaml jobs: From 1822089db65677c9b7cc418bae66b5b5afcee31d Mon Sep 17 00:00:00 2001 From: remicorniere Date: Thu, 28 Nov 2019 16:15:15 +0000 Subject: [PATCH 05/54] Tests for Component and code style fixes (#129) * Tests for Component and code style fixes --- _examples/xmpp_component/xmpp_component.go | 4 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 2 +- auth.go | 5 +- cert_checker.go | 5 +- client.go | 26 +- component.go | 23 +- component_test.go | 420 ++++++++++++++++++++- network.go | 2 +- network_test.go | 37 +- router_test.go | 12 +- stanza/component.go | 2 +- stanza/error.go | 28 +- stanza/iq.go | 43 ++- stanza/iq_test.go | 35 ++ stanza/node.go | 13 +- stanza/packet_enum.go | 6 + stanza/sasl_auth.go | 4 +- stanza/stream.go | 2 +- stanza/stream_features.go | 8 +- stream_manager.go | 7 +- test.sh | 2 +- 21 files changed, 612 insertions(+), 74 deletions(-) diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index e36b287..0452888 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -58,7 +58,7 @@ func handleMessage(_ xmpp.Sender, p stanza.Packet) { func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { // Type conversion & sanity checks iq, ok := p.(stanza.IQ) - if !ok || iq.Type != "get" { + if !ok || iq.Type != stanza.IQTypeGet { return } @@ -73,7 +73,7 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { func discoItems(c xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks iq, ok := p.(stanza.IQ) - if !ok || iq.Type != "get" { + if !ok || iq.Type != stanza.IQTypeGet { return } diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index 10e5dfc..91f453c 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -106,7 +106,7 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { func sendUserTune(s xmpp.Sender, artist string, title string) { tune := stanza.Tune{Artist: artist, Title: title} - iq := stanza.NewIQ(stanza.Attrs{Type: "set", Id: "usertune-1", Lang: "en"}) + iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, 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 726e15a..b8d20b9 100644 --- a/auth.go +++ b/auth.go @@ -60,7 +60,10 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str raw := "\x00" + user + "\x00" + secret enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) base64.StdEncoding.Encode(enc, []byte(raw)) - fmt.Fprintf(socket, "%s", stanza.NSSASL, mech, enc) + _, err := fmt.Fprintf(socket, "%s", stanza.NSSASL, mech, enc) + if err != nil { + return err + } // Next message should be either success or failure. val, err := stanza.NextPacket(decoder) diff --git a/cert_checker.go b/cert_checker.go index fcee7b1..30a265a 100644 --- a/cert_checker.go +++ b/cert_checker.go @@ -79,7 +79,10 @@ func (c *ServerCheck) Check() error { } if _, ok := f.DoesStartTLS(); ok { - fmt.Fprintf(tcpconn, "") + _, err = fmt.Fprintf(tcpconn, "") + if err != nil { + return err + } var k stanza.TLSProceed if err = decoder.DecodeElement(&k, nil); err != nil { diff --git a/client.go b/client.go index b7111f2..14537db 100644 --- a/client.go +++ b/client.go @@ -50,7 +50,7 @@ type SMState struct { // EventHandler is use to pass events about state of the connection to // client implementation. -type EventHandler func(Event) +type EventHandler func(Event) error type EventManager struct { // Store current state @@ -188,13 +188,16 @@ func (c *Client) Resume(state SMState) error { go keepalive(c.transport, keepaliveQuit) // Start the receiver go routine state = c.Session.SMState - go c.recv(state, keepaliveQuit) + // Leaving this channel here for later. Not used atm. We should return this instead of an error because right + // now the returned error is lost in limbo. + errChan := make(chan error) + go c.recv(state, keepaliveQuit, errChan) // We're connected and can now receive and send messages. //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") // TODO: Do we always want to send initial presence automatically ? // Do we need an option to avoid that or do we rely on client to send the presence itself ? - fmt.Fprintf(c.transport, "") + _, err = fmt.Fprintf(c.transport, "") return err } @@ -235,7 +238,7 @@ func (c *Client) Send(packet stanza.Packet) error { // result := <- client.SendIQ(ctx, iq) // func (c *Client) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) { - if iq.Attrs.Type != "set" && iq.Attrs.Type != "get" { + if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet { return nil, ErrCanOnlySendGetOrSetIq } if err := c.Send(iq); err != nil { @@ -267,13 +270,14 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Go routines // Loop: Receive data from server -func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) { +func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan<- error) { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { + errChan <- err close(keepaliveQuit) c.disconnected(state) - return err + return } // Handle stream errors @@ -282,18 +286,22 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) (err error) c.router.route(c, val) close(keepaliveQuit) c.streamError(packet.Error.Local, packet.Text) - return errors.New("stream error: " + packet.Error.Local) + errChan <- errors.New("stream error: " + packet.Error.Local) + return // Process Stream management nonzas case stanza.SMRequest: answer := stanza.SMAnswer{XMLName: xml.Name{ Space: stanza.NSStreamManagement, Local: "a", }, H: state.Inbound} - c.Send(answer) + err = c.Send(answer) + if err != nil { + errChan <- err + return + } default: state.Inbound++ } - // Do normal route processing in a go-routine so we can immediately // start receiving other stanzas. This also allows route handlers to // send and receive more stanzas. diff --git a/component.go b/component.go index cb468db..471f1db 100644 --- a/component.go +++ b/component.go @@ -72,11 +72,13 @@ func (c *Component) Resume(sm SMState) error { c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) if err != nil { c.updateState(StatePermanentError) + return NewConnError(err, true) } if streamId, err = c.transport.Connect(); err != nil { c.updateState(StatePermanentError) + return NewConnError(err, true) } c.updateState(StateConnected) @@ -84,6 +86,7 @@ func (c *Component) Resume(sm SMState) error { // Authentication if _, err := fmt.Fprintf(c.transport, "%s", c.handshake(streamId)); err != nil { c.updateState(StateStreamError) + return NewConnError(errors.New("cannot send handshake "+err.Error()), false) } @@ -101,12 +104,16 @@ func (c *Component) Resume(sm SMState) error { case stanza.Handshake: // Start the receiver go routine c.updateState(StateSessionEstablished) - go c.recv() - return nil + // Leaving this channel here for later. Not used atm. We should return this instead of an error because right + // now the returned error is lost in limbo. + errChan := make(chan error) + go c.recv(errChan) // Sends to errChan + return err // Should be empty at this point default: c.updateState(StatePermanentError) return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true) } + return err } func (c *Component) Disconnect() { @@ -121,20 +128,22 @@ func (c *Component) SetHandler(handler EventHandler) { } // Receiver Go routine receiver -func (c *Component) recv() (err error) { +func (c *Component) recv(errChan chan<- error) { + for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.updateState(StateDisconnected) - return err + errChan <- err + return } - // Handle stream errors switch p := val.(type) { case stanza.StreamError: c.router.route(c, val) c.streamError(p.Error.Local, p.Text) - return errors.New("stream error: " + p.Error.Local) + errChan <- errors.New("stream error: " + p.Error.Local) + return } c.router.route(c, val) } @@ -168,7 +177,7 @@ func (c *Component) Send(packet stanza.Packet) error { // result := <- client.SendIQ(ctx, iq) // func (c *Component) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) { - if iq.Attrs.Type != "set" && iq.Attrs.Type != "get" { + if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet { return nil, ErrCanOnlySendGetOrSetIq } if err := c.Send(iq); err != nil { diff --git a/component_test.go b/component_test.go index 8938769..4e115f0 100644 --- a/component_test.go +++ b/component_test.go @@ -1,12 +1,34 @@ package xmpp import ( + "context" + "encoding/xml" + "errors" "fmt" + "gosrc.io/xmpp/stanza" + "net" + "strings" "testing" + "time" ) -const testComponentDomain = "localhost" -const testComponentPort = "15222" +// Tests are ran in parallel, so each test creating a server must use a different port so we do not get any +// conflict. Using iota for this should do the trick. +const ( + testComponentDomain = "localhost" + defaultServerName = "testServer" + defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545" + defaultComponentName = "Test Component" + + // Default port is not standard XMPP port to avoid interfering + // with local running XMPP server + testHandshakePort = iota + 15222 + testDecoderPort + testSendIqPort + testSendRawPort + testDisconnectPort + testSManDisconnectPort +) func TestHandshake(t *testing.T) { opts := ComponentOptions{ @@ -24,25 +46,43 @@ func TestHandshake(t *testing.T) { } } +// Tests connection process with a handshake exchange +// Tests multiple session IDs. All connections should generate a unique stream ID func TestGenerateHandshake(t *testing.T) { - // TODO -} + // Using this array with a channel to make a queue of values to test + // These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate + // some handshake value + var uuidsArray = [5]string{ + "cc9b3249-9582-4780-825f-4311b42f9b0e", + "bba8be3c-d98e-4e26-b9bb-9ed34578a503", + "dae72822-80e8-496b-b763-ab685f53a188", + "a45d6c06-de49-4bb0-935b-1a2201b71028", + "7dc6924f-0eca-4237-9898-18654b8d891e", + } -// Test that NewStreamManager can accept a Component. -// -// This validates that Component conforms to StreamClient interface. -func TestStreamManager(t *testing.T) { - NewStreamManager(&Component{}, nil) -} + // Channel to pass stream IDs as a queue + var uchan = make(chan string, len(uuidsArray)) + // Populate test channel + for _, elt := range uuidsArray { + uchan <- elt + } -// Tests that the decoder is properly initialized when connecting a component to a server. -// The decoder is expected to be built after a valid connection -// Based on the xmpp_component example. -func TestDecoder(t *testing.T) { - testComponentAddess := fmt.Sprintf("%s:%s", testComponentDomain, testComponentPort) + // Performs a Component connection with a handshake. It expects to have an ID sent its way through the "uchan" + // channel of this file. Otherwise it will hang for ever. + h := func(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + checkOpenStreamHandshakeID(t, c, decoder, <-uchan) + readHandshakeComponent(t, decoder) + fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) + return + } + + // Init mock server + testComponentAddess := fmt.Sprintf("%s:%d", testComponentDomain, testHandshakePort) mock := ServerMock{} - mock.Start(t, testComponentAddess, handlerConnectSuccess) + mock.Start(t, testComponentAddess, h) + // Init component opts := ComponentOptions{ TransportConfiguration: TransportConfiguration{ Address: testComponentAddess, @@ -63,12 +103,352 @@ func TestDecoder(t *testing.T) { if err != nil { t.Errorf("%+v", err) } - _, err = c.transport.Connect() - if err != nil { - t.Errorf("%+v", err) + + // Try connecting, and storing the resulting streamID in a map. + m := make(map[string]bool) + for _, _ = range uuidsArray { + streamId, _ := c.transport.Connect() + m[c.handshake(streamId)] = true } + if len(uuidsArray) != len(m) { + t.Errorf("Handshake does not produce a unique id. Expected: %d unique ids, got: %d", len(uuidsArray), len(m)) + } +} + +// Test that NewStreamManager can accept a Component. +// +// This validates that Component conforms to StreamClient interface. +func TestStreamManager(t *testing.T) { + NewStreamManager(&Component{}, nil) +} + +// Tests that the decoder is properly initialized when connecting a component to a server. +// The decoder is expected to be built after a valid connection +// Based on the xmpp_component example. +func TestDecoder(t *testing.T) { + c, _ := mockConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID) if c.transport.GetDecoder() == nil { t.Errorf("Failed to initialize decoder. Decoder is nil.") } - +} + +// Tests sending an IQ to the server, and getting the response +func TestSendIq(t *testing.T) { + //Connecting to a mock server, initialized with given port and handler function + c, m := mockConnection(t, testSendIqPort, handlerForComponentIQSend) + + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + + var res chan stanza.IQ + res, _ = c.SendIQ(ctx, iqReq) + + select { + case <-res: + case <-time.After(100 * time.Millisecond): + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + + m.Stop() +} + +// Tests sending raw xml to the mock server. +// TODO : check the server response client side ? +// Right now, the server response is not checked and an err is passed in a channel if the test is supposed to err. +// In this test, we use IQs +func TestSendRaw(t *testing.T) { + // Error channel for the handler + errChan := make(chan error) + // Handler for the mock server + h := func(t *testing.T, c net.Conn) { + // Completes the connection by exchanging handshakes + handlerForComponentHandshakeDefaultID(t, c) + receiveRawIq(t, c, errChan) + return + } + + type testCase struct { + req string + shouldErr bool + } + testRequests := make(map[string]testCase) + // Sending a correct IQ of type get. Not supposed to err + testRequests["Correct IQ"] = testCase{ + req: ``, + shouldErr: false, + } + // Sending an IQ with a missing ID. Should err + testRequests["IQ with missing ID"] = testCase{ + req: ``, + shouldErr: true, + } + + // Tests for all the IQs + for name, tcase := range testRequests { + t.Run(name, func(st *testing.T) { + //Connecting to a mock server, initialized with given port and handler function + c, m := mockConnection(t, testSendRawPort, h) + + // Sending raw xml from test case + err := c.SendRaw(tcase.req) + if err != nil { + t.Errorf("Error sending Raw string") + } + // Just wait a little so the message has time to arrive + select { + case <-time.After(100 * time.Millisecond): + case err = <-errChan: + if err == nil && tcase.shouldErr { + t.Errorf("Failed to get closing stream err") + } + } + c.transport.Close() + m.Stop() + }) + } +} + +// Tests the Disconnect method for Components +func TestDisconnect(t *testing.T) { + c, m := mockConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID) + err := c.transport.Ping() + if err != nil { + t.Errorf("Could not ping but not disconnected yet") + } + c.Disconnect() + err = c.transport.Ping() + if err == nil { + t.Errorf("Did not disconnect properly") + } + m.Stop() +} + +// Tests that a streamManager successfully disconnects when a handshake fails between the component and the server. +func TestStreamManagerDisconnect(t *testing.T) { + // Init mock server + testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, testSManDisconnectPort) + mock := ServerMock{} + // Handler fails the handshake, which is currently the only option to disconnect completely when using a streamManager + // a failed handshake being a permanent error, except for a "conflict" + mock.Start(t, testComponentAddress, handlerComponentFailedHandshakeDefaultID) + + //================================== + // Create Component to connect to it + c := makeBasicComponent(defaultComponentName, testComponentAddress, t) + + //======================================== + // Connect the new Component to the server + cm := NewStreamManager(c, nil) + errChan := make(chan error) + runSMan := func(errChan chan error) { + errChan <- cm.Run() + } + + go runSMan(errChan) + select { + case <-errChan: + case <-time.After(100 * time.Millisecond): + t.Errorf("The component and server seem to still be connected while they should not.") + } + mock.Stop() +} + +//============================================================================= +// Basic XMPP Server Mock Handlers. +// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. +// Used in the mock server as a Handler +func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + checkOpenStreamHandshakeDefaultID(t, c, decoder) + readHandshakeComponent(t, decoder) + fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) + return +} + +// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. +// This handler is supposed to fail by sending a "message" stanza instead of a stanza to finalize the handshake. +func handlerComponentFailedHandshakeDefaultID(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + checkOpenStreamHandshakeDefaultID(t, c, decoder) + readHandshakeComponent(t, decoder) + + // Send a message, instead of a "" tag, to fail the handshake process dans disconnect the client. + me := stanza.Message{ + Attrs: stanza.Attrs{Type: stanza.MessageTypeChat, From: defaultServerName, To: defaultComponentName, Lang: "en"}, + Body: "Fail my handshake.", + } + s, _ := xml.Marshal(me) + fmt.Fprintln(c, string(s)) + + return +} + +// Reads from the connection with the Component. Expects a handshake request, and returns the tag. +func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read auth: %s", err) + return + } + nv := &stanza.Handshake{} + // Decode element into pointer storage + if err = decoder.DecodeElement(nv, &se); err != nil { + t.Errorf("cannot decode handshake: %s", err) + return + } + if len(strings.TrimSpace(nv.Value)) == 0 { + t.Errorf("did not receive handshake ID") + } +} + +func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) { + checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID) +} + +// Used for ID and handshake related tests +func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + + for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. + token, err := decoder.Token() + if err != nil { + t.Errorf("cannot read next token: %s", err) + } + + switch elem := token.(type) { + // Wait for first startElement + case xml.StartElement: + 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", streamID, stanza.NSComponent, stanza.NSStream); err != nil { + t.Errorf("cannot write server stream open: %s", err) + } + return + } + } +} + +//============================================================================= +// Sends IQ response to Component request. +// No parsing of the request here. We just check that it's valid, and send the default response. +func handlerForComponentIQSend(t *testing.T, c net.Conn) { + // Completes the connection by exchanging handshakes + handlerForComponentHandshakeDefaultID(t, c) + + // Decoder to parse the request + decoder := xml.NewDecoder(c) + + iqReq, err := receiveIq(t, c, decoder) + if err != nil { + t.Errorf("Error receiving the IQ stanza : %v", err) + } else if !iqReq.IsValid() { + t.Errorf("server received an IQ stanza : %v", iqReq) + } + + // Crafting response + iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) + disco := iqResp.DiscoInfo() + disco.AddFeatures("vcard-temp", + `http://jabber.org/protocol/address`) + + disco.AddIdentity("Multicast", "service", "multicast") + iqResp.Payload = disco + + // Sending response to the Component + mResp, err := xml.Marshal(iqResp) + _, err = fmt.Fprintln(c, string(mResp)) + if err != nil { + t.Errorf("Could not send response stanza : %s", err) + } + return +} + +// Reads next request coming from the Component. Expecting it to be an IQ request +func receiveIq(t *testing.T, c net.Conn, decoder *xml.Decoder) (stanza.IQ, error) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + var iqStz stanza.IQ + err := decoder.Decode(&iqStz) + if err != nil { + t.Errorf("cannot read the received IQ stanza: %s", err) + } + if !iqStz.IsValid() { + t.Errorf("received IQ stanza is invalid : %s", err) + } + return iqStz, nil +} + +func receiveRawIq(t *testing.T, c net.Conn, errChan chan error) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + decoder := xml.NewDecoder(c) + var iq stanza.IQ + err := decoder.Decode(&iq) + if err != nil || !iq.IsValid() { + s := stanza.StreamError{ + XMLName: xml.Name{Local: "stream:error"}, + Error: xml.Name{Local: "xml-not-well-formed"}, + Text: `XML was not well-formed`, + } + raw, _ := xml.Marshal(s) + fmt.Fprintln(c, string(raw)) + fmt.Fprintln(c, ``) // TODO : check this client side + errChan <- fmt.Errorf("invalid xml") + return + } + errChan <- nil + return +} + +//=============================== +// Init mock server and connection +// Creating a mock server and connecting a Component to it. Initialized with given port and handler function +// The Component and mock are both returned +func mockConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) { + // Init mock server + testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port) + mock := ServerMock{} + mock.Start(t, testComponentAddress, handler) + + //================================== + // Create Component to connect to it + c := makeBasicComponent(defaultComponentName, testComponentAddress, t) + + //======================================== + // Connect the new Component to the server + err := c.Connect() + if err != nil { + t.Errorf("%+v", err) + } + + return c, &mock +} + +func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component { + opts := ComponentOptions{ + TransportConfiguration: TransportConfiguration{ + Address: mockServerAddr, + Domain: "localhost", + }, + Domain: testComponentDomain, + Secret: "mypass", + Name: name, + Category: "gateway", + Type: "service", + } + router := NewRouter() + c, err := NewComponent(opts, router) + if err != nil { + t.Errorf("%+v", err) + } + c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) + if err != nil { + t.Errorf("%+v", err) + } + return c } diff --git a/network.go b/network.go index 75a0a60..8b03f3f 100644 --- a/network.go +++ b/network.go @@ -23,7 +23,7 @@ func ensurePort(addr string, port int) string { // This is IPV4 without port return addr + ":" + strconv.Itoa(port) case 1: - // This is IPV$ with port + // This is IPV6 with port return addr default: // This is IPV6 without port, as you need to use bracket with port in IPV6 diff --git a/network_test.go b/network_test.go index 116ecef..470f150 100644 --- a/network_test.go +++ b/network_test.go @@ -1,12 +1,10 @@ package xmpp import ( + "strings" "testing" ) -type params struct { -} - func TestParseAddr(t *testing.T) { tests := []struct { name string @@ -33,3 +31,36 @@ func TestParseAddr(t *testing.T) { }) } } + +func TestEnsurePort(t *testing.T) { + testAddresses := []string{ + "1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad", + "1ca3:6c07:ee3a:89ca:e065:9a70:71d:daad:5252", + "[::1]", + "127.0.0.1:5555", + "127.0.0.1", + "[::1]:5555", + } + + for _, oldAddr := range testAddresses { + t.Run(oldAddr, func(st *testing.T) { + newAddr := ensurePort(oldAddr, 5222) + + if len(newAddr) < len(oldAddr) { + st.Errorf("incorrect Result: transformed address is shorter than input : %v (old) > %v (new)", newAddr, oldAddr) + } + // If IPv6, the new address needs brackets to specify a port, like so : [2001:db8:85a3:0:0:8a2e:370:7334]:5222 + if strings.Count(newAddr, "[") < strings.Count(oldAddr, "[") || + strings.Count(newAddr, "]") < strings.Count(oldAddr, "]") { + + st.Errorf("incorrect Result. Transformed address seems to not have correct brakets : %v => %v", oldAddr, newAddr) + } + + // Check if we messed up the colons, or didn't properly add a port + if strings.Count(newAddr, ":") < strings.Count(oldAddr, ":") { + st.Errorf("incorrect Result: transformed address doesn't seem to have a port %v (=> %v, no port ?)", oldAddr, newAddr) + } + }) + } + +} diff --git a/router_test.go b/router_test.go index b3d253e..2b5cf82 100644 --- a/router_test.go +++ b/router_test.go @@ -146,7 +146,7 @@ func TestTypeMatcher(t *testing.T) { // We do not match on other types conn = NewSenderMock() - iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", @@ -163,27 +163,27 @@ func TestCompositeMatcher(t *testing.T) { router := NewRouter() router.NewRoute(). IQNamespaces("jabber:iq:version"). - StanzaType("get"). + StanzaType(string(stanza.IQTypeGet)). HandlerFunc(func(s Sender, p stanza.Packet) { _ = s.SendRaw(successFlag) }) // Data set - getVersionIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + getVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) getVersionIq.Payload = &stanza.Version{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", }} - setVersionIq := stanza.NewIQ(stanza.Attrs{Type: "set", From: "service.localhost", To: "test@localhost", Id: "1"}) + setVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"}) setVersionIq.Payload = &stanza.Version{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", }} - GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) GetDiscoIq.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/disco#info", @@ -238,7 +238,7 @@ func TestCatchallMatcher(t *testing.T) { } conn = NewSenderMock() - iqVersion := stanza.NewIQ(stanza.Attrs{Type: "get", From: "service.localhost", To: "test@localhost", Id: "1"}) + iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", diff --git a/stanza/component.go b/stanza/component.go index 33ced33..32a36b0 100644 --- a/stanza/component.go +++ b/stanza/component.go @@ -12,7 +12,7 @@ import ( type Handshake struct { XMLName xml.Name `xml:"jabber:component:accept handshake"` // TODO Add handshake value with test for proper serialization - // Value string `xml:",innerxml"` + Value string `xml:",innerxml"` } func (Handshake) Name() string { diff --git a/stanza/error.go b/stanza/error.go index bcc947f..0f416e4 100644 --- a/stanza/error.go +++ b/stanza/error.go @@ -54,7 +54,7 @@ func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"} if elt.XMLName == textName { - x.Text = string(elt.Content) + x.Text = elt.Content } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" { x.Reason = elt.XMLName.Local } @@ -94,16 +94,32 @@ func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { // Reason if x.Reason != "" { reason := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: x.Reason} - e.EncodeToken(xml.StartElement{Name: reason}) - e.EncodeToken(xml.EndElement{Name: reason}) + err = e.EncodeToken(xml.StartElement{Name: reason}) + if err != nil { + return err + } + err = e.EncodeToken(xml.EndElement{Name: reason}) + if err != nil { + return err + } + } // Text if x.Text != "" { text := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"} - e.EncodeToken(xml.StartElement{Name: text}) - e.EncodeToken(xml.CharData(x.Text)) - e.EncodeToken(xml.EndElement{Name: text}) + err = e.EncodeToken(xml.StartElement{Name: text}) + if err != nil { + return err + } + err = e.EncodeToken(xml.CharData(x.Text)) + if err != nil { + return err + } + err = e.EncodeToken(xml.EndElement{Name: text}) + if err != nil { + return err + } } return e.EncodeToken(xml.EndElement{Name: start.Name}) diff --git a/stanza/iq.go b/stanza/iq.go index 923cf28..499c261 100644 --- a/stanza/iq.go +++ b/stanza/iq.go @@ -2,6 +2,7 @@ package stanza import ( "encoding/xml" + "strings" "github.com/google/uuid" ) @@ -23,7 +24,7 @@ type IQ struct { // Info/Query // child element, which specifies the semantics of the particular // request." Payload IQPayload `xml:",omitempty"` - Error Err `xml:"error,omitempty"` + Error *Err `xml:"error,omitempty"` // Any is used to decode unknown payload as a generic structure Any *Node `xml:",any"` } @@ -52,7 +53,7 @@ func (iq IQ) MakeError(xerror Err) IQ { iq.Type = "error" iq.From = to iq.To = from - iq.Error = xerror + iq.Error = &xerror return iq } @@ -106,7 +107,7 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if err != nil { return err } - iq.Error = xmppError + iq.Error = &xmppError continue } if iqExt := TypeRegistry.GetIQExtension(tt.Name); iqExt != nil { @@ -132,3 +133,39 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } } } + +// Following RFC-3920 for IQs +func (iq *IQ) IsValid() bool { + // ID is required + if len(strings.TrimSpace(iq.Id)) == 0 { + return false + } + + // Type is required + if iq.Type.IsEmpty() { + return false + } + + // Type get and set must contain one and only one child element that specifies the semantics + if iq.Type == IQTypeGet || iq.Type == IQTypeSet { + if iq.Payload == nil && iq.Any == nil { + return false + } + } + + // A result must include zero or one child element + if iq.Type == IQTypeResult { + if iq.Payload != nil && iq.Any != nil { + return false + } + } + + //Error type must contain an "error" child element + if iq.Type == IQTypeError { + if iq.Error == nil { + return false + } + } + + return true +} diff --git a/stanza/iq_test.go b/stanza/iq_test.go index 54a8fc5..3223566 100644 --- a/stanza/iq_test.go +++ b/stanza/iq_test.go @@ -187,3 +187,38 @@ func TestUnknownPayload(t *testing.T) { t.Errorf("could not extract namespace: '%s'", parsedIQ.Any.XMLName.Space) } } + +func TestIsValid(t *testing.T) { + type testCase struct { + iq string + shouldErr bool + } + testIQs := make(map[string]testCase) + testIQs["Valid IQ"] = testCase{ + ` + + `, + false, + } + testIQs["Invalid IQ"] = testCase{ + ` + + `, + true, + } + + for name, tcase := range testIQs { + t.Run(name, func(st *testing.T) { + parsedIQ := stanza.IQ{} + err := xml.Unmarshal([]byte(tcase.iq), &parsedIQ) + if err != nil { + t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq) + return + } + if !parsedIQ.IsValid() && !tcase.shouldErr { + t.Errorf("failed iq validation for : %s", tcase.iq) + } + }) + } + +} diff --git a/stanza/node.go b/stanza/node.go index 6afa7bc..308729c 100644 --- a/stanza/node.go +++ b/stanza/node.go @@ -46,9 +46,18 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { start.Name = n.XMLName err = e.EncodeToken(start) - e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName}) + if err != nil { + return err + } + err = e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName}) + if err != nil { + return err + } if n.Content != "" { - e.EncodeToken(xml.CharData(n.Content)) + err = e.EncodeToken(xml.CharData(n.Content)) + if err != nil { + return err + } } return e.EncodeToken(xml.EndElement{Name: start.Name}) } diff --git a/stanza/packet_enum.go b/stanza/packet_enum.go index 103966a..84dd476 100644 --- a/stanza/packet_enum.go +++ b/stanza/packet_enum.go @@ -1,5 +1,7 @@ package stanza +import "strings" + type StanzaType string // RFC 6120: part of A.5 Client Namespace and A.6 Server Namespace @@ -23,3 +25,7 @@ const ( PresenceTypeUnsubscribe StanzaType = "unsubscribe" PresenceTypeUnsubscribed StanzaType = "unsubscribed" ) + +func (s StanzaType) IsEmpty() bool { + return len(strings.TrimSpace(string(s))) == 0 +} diff --git a/stanza/sasl_auth.go b/stanza/sasl_auth.go index d04174f..29648ee 100644 --- a/stanza/sasl_auth.go +++ b/stanza/sasl_auth.go @@ -107,6 +107,6 @@ func (s *StreamSession) IsOptional() bool { // Registry init func init() { - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-bind", "bind"}, Bind{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-bind", Local: "bind"}, Bind{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-session", Local: "session"}, StreamSession{}) } diff --git a/stanza/stream.go b/stanza/stream.go index 290abfe..203cc83 100644 --- a/stanza/stream.go +++ b/stanza/stream.go @@ -8,7 +8,7 @@ import "encoding/xml" type Stream struct { XMLName xml.Name `xml:"http://etherx.jabber.org/streams stream"` From string `xml:"from,attr"` - To string `xml:"to,attr"` + To string `xml:"to,attr"` Id string `xml:"id,attr"` Version string `xml:"version,attr"` } diff --git a/stanza/stream_features.go b/stanza/stream_features.go index 11cd96b..14358f0 100644 --- a/stanza/stream_features.go +++ b/stanza/stream_features.go @@ -15,7 +15,7 @@ type StreamFeatures struct { // Server capabilities hash Caps Caps // Stream features - StartTLS tlsStartTLS + StartTLS TlsStartTLS Mechanisms saslMechanisms Bind Bind StreamManagement streamManagement @@ -60,13 +60,13 @@ type Caps struct { // StartTLS feature // Reference: RFC 6120 - https://tools.ietf.org/html/rfc6120#section-5.4 -type tlsStartTLS struct { +type TlsStartTLS struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls starttls"` Required bool } // UnmarshalXML implements custom parsing startTLS required flag -func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (stls *TlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { stls.XMLName = start.Name // Check subelements to extract required field as boolean @@ -98,7 +98,7 @@ func (stls *tlsStartTLS) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er } } -func (sf *StreamFeatures) DoesStartTLS() (feature tlsStartTLS, isSupported bool) { +func (sf *StreamFeatures) DoesStartTLS() (feature TlsStartTLS, isSupported bool) { if sf.StartTLS.XMLName.Space+" "+sf.StartTLS.XMLName.Local == nsTLS+" starttls" { return sf.StartTLS, true } diff --git a/stream_manager.go b/stream_manager.go index bf7fba8..aebd8a4 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -74,7 +74,7 @@ func (sm *StreamManager) Run() error { return errors.New("missing stream client") } - handler := func(e Event) { + handler := func(e Event) error { switch e.State { case StateConnected: sm.Metrics.setConnectTime() @@ -82,17 +82,18 @@ func (sm *StreamManager) Run() error { sm.Metrics.setLoginTime() case StateDisconnected: // Reconnect on disconnection - sm.resume(e.SMState) + return sm.resume(e.SMState) case StateStreamError: sm.client.Disconnect() // Only try reconnecting if we have not been kicked by another session to avoid connection loop. // TODO: Make this conflict exception a permanent error if e.StreamError != "conflict" { - sm.connect() + return sm.connect() } case StatePermanentError: // Do not attempt to reconnect } + return nil } sm.client.SetHandler(handler) diff --git a/test.sh b/test.sh index 9730026..725dcaf 100755 --- a/test.sh +++ b/test.sh @@ -5,7 +5,7 @@ export GO111MODULE=on echo "" > coverage.txt for d in $(go list ./... | grep -v vendor); do - go test -race -coverprofile=profile.out -covermode=atomic ${d} + go test -race -coverprofile=profile.out -covermode=atomic "${d}" if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out From 3b84cb796ea939307ecc89a0de4e2768487012cb Mon Sep 17 00:00:00 2001 From: Jerome Sautret Date: Thu, 28 Nov 2019 17:39:10 +0100 Subject: [PATCH 06/54] Add 0.3.0 changelog --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc67d1..411590d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Fluux XMPP Changelog +## v0.3.0 + +### Changes + +- Update requirements to go1.13 +- Add a websocket transport +- Add Client.SendIQ method +- Add IQ result routes to the Router +- Fix SIGSEGV in xmpp_component (#126) +- Add tests for Component and code style fixes + ## v0.2.0 ### Changes @@ -14,4 +25,4 @@ ### Code migration guide -TODO \ No newline at end of file +TODO From c60edf4771850258e34747ebf86880ddad60809d Mon Sep 17 00:00:00 2001 From: remicorniere Date: Thu, 28 Nov 2019 17:40:19 +0100 Subject: [PATCH 07/54] Added "-race" flag to tests run command Kind of testing edits on someone else's PR too --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0e45eaa..538f9c7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,4 +24,4 @@ jobs: id: go - uses: actions/checkout@v1 - name: Run tests - run: go test ./... + run: go test ./... -race From 80ba7905559be1612803006c92d21e27601c13e8 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Fri, 29 Nov 2019 15:56:27 +0100 Subject: [PATCH 08/54] Added coverage Should detect race conditions, use the same covermove as the current mode in test.sh, and update code coverage on the repo. See : https://github.com/marketplace/actions/coveralls-github-action --- .github/workflows/test.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 538f9c7..1b36152 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,4 +24,15 @@ jobs: id: go - uses: actions/checkout@v1 - name: Run tests - run: go test ./... -race + run: | + go test ./... -v -race -coverprofile cover.out -covermode=atomic + - name: Convert coverage to lcov + uses: jandelgado/gcov2lcov-action@v1.0.0 + with: + infile: cover.out + outfile: coverage.lcov + - name: Coveralls + uses: coverallsapp/github-action@v1.0.1 + with: + github-token: ${{ secrets.github_token }} + path-to-lcov: coverage.lcov From a95b53d9ad84acd43aa1d7a7728261aa094a23c7 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Fri, 29 Nov 2019 16:37:23 +0100 Subject: [PATCH 09/54] Update with coveralls badge. Removed codecov badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82d051b..14ff43b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Fluux XMPP -[![Codeship Status for FluuxIO/xmpp](https://app.codeship.com/projects/dba7f300-d145-0135-6c51-26e28af241d2/status?branch=master)](https://app.codeship.com/projects/262399) [![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![codecov](https://codecov.io/gh/FluuxIO/go-xmpp/branch/master/graph/badge.svg)](https://codecov.io/gh/FluuxIO/go-xmpp) +[![Codeship Status for FluuxIO/xmpp](https://app.codeship.com/projects/dba7f300-d145-0135-6c51-26e28af241d2/status?branch=master)](https://app.codeship.com/projects/262399) [![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![Coverage Status](https://coveralls.io/repos/github/FluuxIO/go-xmpp/badge.svg?branch=master)](https://coveralls.io/github/FluuxIO/go-xmpp?branch=master) Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT. From bfe2b7a30f8965fcb20fbe8af121e9e7300b0323 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Fri, 29 Nov 2019 17:13:18 +0100 Subject: [PATCH 10/54] Removed codeship and codecov. We now use github actions and coveralls. --- README.md | 2 +- codecov.yml | 1 - codeship-services.yml | 5 ----- codeship-steps.yml | 5 ----- codeship.env.encrypted | 1 - 5 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 codecov.yml delete mode 100644 codeship-services.yml delete mode 100644 codeship-steps.yml delete mode 100644 codeship.env.encrypted diff --git a/README.md b/README.md index 14ff43b..08b911c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Fluux XMPP -[![Codeship Status for FluuxIO/xmpp](https://app.codeship.com/projects/dba7f300-d145-0135-6c51-26e28af241d2/status?branch=master)](https://app.codeship.com/projects/262399) [![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![Coverage Status](https://coveralls.io/repos/github/FluuxIO/go-xmpp/badge.svg?branch=master)](https://coveralls.io/github/FluuxIO/go-xmpp?branch=master) +[![GoDoc](https://godoc.org/gosrc.io/xmpp?status.svg)](https://godoc.org/gosrc.io/xmpp) [![GoReportCard](https://goreportcard.com/badge/gosrc.io/xmpp)](https://goreportcard.com/report/fluux.io/xmpp) [![Coverage Status](https://coveralls.io/repos/github/FluuxIO/go-xmpp/badge.svg?branch=master)](https://coveralls.io/github/FluuxIO/go-xmpp?branch=master) Fluux XMPP is a Go XMPP library, focusing on simplicity, simple automation, and IoT. diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index db24720..0000000 --- a/codecov.yml +++ /dev/null @@ -1 +0,0 @@ -comment: off diff --git a/codeship-services.yml b/codeship-services.yml deleted file mode 100644 index bbb476e..0000000 --- a/codeship-services.yml +++ /dev/null @@ -1,5 +0,0 @@ -build: - build: - image: fluux/build - dockerfile: Dockerfile - encrypted_env_file: codeship.env.encrypted diff --git a/codeship-steps.yml b/codeship-steps.yml deleted file mode 100644 index 5528b3d..0000000 --- a/codeship-steps.yml +++ /dev/null @@ -1,5 +0,0 @@ -- type: serial - steps: - - name: test - service: build - command: ./test.sh diff --git a/codeship.env.encrypted b/codeship.env.encrypted deleted file mode 100644 index 5c92b24..0000000 --- a/codeship.env.encrypted +++ /dev/null @@ -1 +0,0 @@ -yVKgVFeKW6SSnC/KgLYpfYtTcqqTke1gOIW5GUiVvRijnhweOJiYKFPmwPjpt1FVrg4WVELQUNbxn3lmfyHVVF7r \ No newline at end of file From 6a3833b27d0b616b48a98ea4ffe71ba5d3b877eb Mon Sep 17 00:00:00 2001 From: rcorniere Date: Mon, 2 Dec 2019 10:19:39 +0100 Subject: [PATCH 11/54] Removed last bits of codecov --- Dockerfile | 4 ---- test.sh | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d159afa..0000000 --- a/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM golang:1.13 -WORKDIR /xmpp -RUN curl -o codecov.sh -s https://codecov.io/bash && chmod +x codecov.sh -COPY . ./ diff --git a/test.sh b/test.sh index 725dcaf..199c05e 100755 --- a/test.sh +++ b/test.sh @@ -11,7 +11,3 @@ for d in $(go list ./... | grep -v vendor); do rm profile.out fi done - -if [ -f "./codecov.sh" ]; then - ./codecov.sh -fi From 5eff2d762322e61b60bd0df2c7c35b8b42653e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Thu, 5 Dec 2019 18:12:00 +0100 Subject: [PATCH 12/54] Added callback to process errors after connection. Added tests and refactored a bit. --- .../xmpp_chat_client/xmpp_chat_client.go | 95 +++++ _examples/xmpp_component/xmpp_component.go | 6 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 5 +- _examples/xmpp_oauth2/xmpp_oauth2.go | 6 +- _examples/xmpp_websocket/xmpp_websocket.go | 6 +- client.go | 23 +- client_test.go | 374 +++++++++++------ component.go | 20 +- component_test.go | 387 +++++++++--------- tcp_server_mock.go | 207 ++++++++++ 10 files changed, 795 insertions(+), 334 deletions(-) create mode 100644 _examples/xmpp_chat_client/xmpp_chat_client.go diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go new file mode 100644 index 0000000..2b2d2e7 --- /dev/null +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -0,0 +1,95 @@ +package main + +/* +xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members +Note that this example sends to a very specific user. User logic is not implemented here. +*/ + +import ( + . "bufio" + "fmt" + "os" + + "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" +) + +const ( + currentUserAddress = "localhost:5222" + currentUserJid = "testuser@localhost" + currentUserPass = "testpass" + correspondantJid = "testuser2@localhost" +) + +func main() { + config := xmpp.Config{ + TransportConfiguration: xmpp.TransportConfiguration{ + Address: currentUserAddress, + }, + Jid: currentUserJid, + Credential: xmpp.Password(currentUserPass), + Insecure: true} + + var client *xmpp.Client + var err error + router := xmpp.NewRouter() + router.HandleFunc("message", handleMessage) + if client, err = xmpp.NewClient(config, router, errorHandler); err != nil { + fmt.Println("Error new client") + } + + // Connecting client and handling messages + // To use a stream manager, just write something like this instead : + //cm := xmpp.NewStreamManager(client, startMessaging) + //log.Fatal(cm.Run()) //=> this will lock the calling goroutine + + if err = client.Connect(); err != nil { + fmt.Printf("XMPP connection failed: %s", err) + return + } + startMessaging(client) + +} + +func startMessaging(client xmpp.Sender) { + reader := NewReader(os.Stdin) + textChan := make(chan string) + var text string + for { + fmt.Print("Enter text: ") + go readInput(reader, textChan) + select { + case <-killChan: + return + case text = <-textChan: + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondantJid}, Body: text} + err := client.Send(reply) + if err != nil { + fmt.Printf("There was a problem sending the message : %v", reply) + return + } + } + } +} + +func readInput(reader *Reader, textChan chan string) { + text, _ := reader.ReadString('\n') + textChan <- text +} + +var killChan = make(chan struct{}) + +// If an error occurs, this is used +func errorHandler(err error) { + fmt.Printf("%v", err) + killChan <- struct{}{} +} + +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) +} diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index 0452888..7f676cb 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -35,7 +35,7 @@ func main() { IQNamespaces("jabber:iq:version"). HandlerFunc(handleVersion) - component, err := xmpp.NewComponent(opts, router) + component, err := xmpp.NewComponent(opts, router, handleError) if err != nil { log.Fatalf("%+v", err) } @@ -47,6 +47,10 @@ func main() { log.Fatal(cm.Run()) } +func handleError(err error) { + fmt.Println(err.Error()) +} + func handleMessage(_ xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if !ok { diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index 91f453c..ce7ebc9 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -53,7 +53,7 @@ func main() { handleIQ(s, p, player) }) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -61,6 +61,9 @@ func main() { cm := xmpp.NewStreamManager(client, nil) log.Fatal(cm.Run()) } +func errorHandler(err error) { + fmt.Println(err.Error()) +} func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { msg, ok := p.(stanza.Message) diff --git a/_examples/xmpp_oauth2/xmpp_oauth2.go b/_examples/xmpp_oauth2/xmpp_oauth2.go index f322447..89b2639 100644 --- a/_examples/xmpp_oauth2/xmpp_oauth2.go +++ b/_examples/xmpp_oauth2/xmpp_oauth2.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -39,6 +39,10 @@ func main() { log.Fatal(cm.Run()) } +func errorHandler(err error) { + fmt.Println(err.Error()) +} + func handleMessage(s xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if !ok { diff --git a/_examples/xmpp_websocket/xmpp_websocket.go b/_examples/xmpp_websocket/xmpp_websocket.go index 428a1d1..c8c0620 100644 --- a/_examples/xmpp_websocket/xmpp_websocket.go +++ b/_examples/xmpp_websocket/xmpp_websocket.go @@ -26,7 +26,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -37,6 +37,10 @@ func main() { log.Fatal(cm.Run()) } +func errorHandler(err error) { + fmt.Println(err.Error()) +} + func handleMessage(s xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if !ok { diff --git a/client.go b/client.go index 14537db..cc152f3 100644 --- a/client.go +++ b/client.go @@ -98,6 +98,8 @@ type Client struct { router *Router // Track and broadcast connection state EventManager + // Handle errors from client execution + ErrorHandler func(error) } /* @@ -107,7 +109,7 @@ Setting up the client / Checking the parameters // NewClient generates a new XMPP client, based on Config passed as parameters. // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. // Default the port to 5222. -func NewClient(config Config, r *Router) (c *Client, err error) { +func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { // Parse JID if config.parsedJid, err = NewJid(config.Jid); err != nil { err = errors.New("missing jid") @@ -140,6 +142,7 @@ func NewClient(config Config, r *Router) (c *Client, err error) { c = new(Client) c.config = config c.router = r + c.ErrorHandler = errorHandler if c.config.ConnectTimeout == 0 { c.config.ConnectTimeout = 15 // 15 second as default @@ -185,13 +188,10 @@ func (c *Client) Resume(state SMState) error { // Start the keepalive go routine keepaliveQuit := make(chan struct{}) - go keepalive(c.transport, keepaliveQuit) + go keepalive(c, keepaliveQuit) // Start the receiver go routine state = c.Session.SMState - // Leaving this channel here for later. Not used atm. We should return this instead of an error because right - // now the returned error is lost in limbo. - errChan := make(chan error) - go c.recv(state, keepaliveQuit, errChan) + go c.recv(state, keepaliveQuit) // We're connected and can now receive and send messages. //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") @@ -270,11 +270,11 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Go routines // Loop: Receive data from server -func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan<- error) { +func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { - errChan <- err + c.ErrorHandler(err) close(keepaliveQuit) c.disconnected(state) return @@ -286,7 +286,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan c.router.route(c, val) close(keepaliveQuit) c.streamError(packet.Error.Local, packet.Text) - errChan <- errors.New("stream error: " + packet.Error.Local) + c.ErrorHandler(errors.New("stream error: " + packet.Error.Local)) return // Process Stream management nonzas case stanza.SMRequest: @@ -296,7 +296,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan }, H: state.Inbound} err = c.Send(answer) if err != nil { - errChan <- err + c.ErrorHandler(err) return } default: @@ -312,8 +312,9 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan // Loop: send whitespace keepalive to server // This is use to keep the connection open, but also to detect connection loss // and trigger proper client connection shutdown. -func keepalive(transport Transport, quit <-chan struct{}) { +func keepalive(c *Client, quit <-chan struct{}) { // TODO: Make keepalive interval configurable + transport := c.transport ticker := time.NewTicker(30 * time.Second) for { select { diff --git a/client_test.go b/client_test.go index 2636f29..15e104f 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,7 @@ package xmpp import ( + "context" "encoding/xml" "errors" "fmt" @@ -14,15 +15,14 @@ import ( const ( // Default port is not standard XMPP port to avoid interfering // with local running XMPP server - testXMPPAddress = "localhost:15222" - - defaultTimeout = 2 * time.Second + testXMPPAddress = "localhost:15222" + testClientDomain = "localhost" ) func TestClient_Connect(t *testing.T) { // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerConnectSuccess) + mock.Start(t, testXMPPAddress, handlerClientConnectSuccess) // Test / Check result config := Config{ @@ -36,7 +36,7 @@ func TestClient_Connect(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -64,7 +64,7 @@ func TestClient_NoInsecure(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -94,7 +94,7 @@ func TestClient_FeaturesTracking(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -109,7 +109,7 @@ func TestClient_FeaturesTracking(t *testing.T) { func TestClient_RFC3921Session(t *testing.T) { // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerConnectWithSession) + mock.Start(t, testXMPPAddress, handlerClientConnectWithSession) // Test / Check result config := Config{ @@ -124,7 +124,7 @@ func TestClient_RFC3921Session(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -135,48 +135,254 @@ func TestClient_RFC3921Session(t *testing.T) { mock.Stop() } +// Testing sending an IQ to the mock server and reading its response. +func TestClient_SendIQ(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, c net.Conn) { + handlerClientConnectSuccess(t, c) + discardPresence(t, c) + respondToIQ(t, c) + done <- struct{}{} + } + client, mock := mockClientConnection(t, h, testClientIqPort) + + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + client.ErrorHandler = errorHandler + res, err := client.SendIQ(ctx, iqReq) + if err != nil { + t.Errorf(err.Error()) + } + + select { + case <-res: // If the server responds with an IQ, we pass the test + case err := <-errChan: // If the server sends an error, or there is a connection error + t.Errorf(err.Error()) + case <-time.After(defaultChannelTimeout): // If we timeout + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + select { + case <-done: + mock.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } +} + +func TestClient_SendIQFail(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, c net.Conn) { + handlerClientConnectSuccess(t, c) + discardPresence(t, c) + respondToIQ(t, c) + done <- struct{}{} + } + client, mock := mockClientConnection(t, h, testClientIqFailPort) + + //================== + // Create an IQ to send + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified + // so we need to overwrite it. + iqReq.Id = "" + + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + client.ErrorHandler = errorHandler + res, _ := client.SendIQ(ctx, iqReq) + + // Test + select { + case <-res: // If the server responds with an IQ + t.Errorf("Server should not respond with an IQ since the request is expected to be invalid !") + case <-errChan: // If the server sends an error, the test passes + case <-time.After(defaultChannelTimeout): // If we timeout + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + select { + case <-done: + mock.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } +} + +func TestClient_SendRaw(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, c net.Conn) { + handlerClientConnectSuccess(t, c) + discardPresence(t, c) + respondToIQ(t, c) + done <- struct{}{} + } + type testCase struct { + req string + shouldErr bool + port int + } + testRequests := make(map[string]testCase) + // Sending a correct IQ of type get. Not supposed to err + testRequests["Correct IQ"] = testCase{ + req: ``, + shouldErr: false, + port: testClientRawPort + 100, + } + // Sending an IQ with a missing ID. Should err + testRequests["IQ with missing ID"] = testCase{ + req: ``, + shouldErr: true, + port: testClientRawPort, + } + + // A handler for the client. + // In the failing test, the server returns a stream error, which triggers this handler, client side. + errChan := make(chan error) + errHandler := func(err error) { + errChan <- err + } + + // Tests for all the IQs + for name, tcase := range testRequests { + t.Run(name, func(st *testing.T) { + //Connecting to a mock server, initialized with given port and handler function + c, m := mockClientConnection(t, h, tcase.port) + c.ErrorHandler = errHandler + // Sending raw xml from test case + err := c.SendRaw(tcase.req) + if err != nil { + t.Errorf("Error sending Raw string") + } + // Just wait a little so the message has time to arrive + select { + // We don't use the default "long" timeout here because waiting it out means passing the test. + case <-time.After(100 * time.Millisecond): + case err = <-errChan: + if err == nil && tcase.shouldErr { + t.Errorf("Failed to get closing stream err") + } else if err != nil && !tcase.shouldErr { + t.Errorf("This test is not supposed to err !") + } + } + c.transport.Close() + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } + }) + } +} + +func TestClient_Disconnect(t *testing.T) { + c, m := mockClientConnection(t, handlerClientConnectSuccess, testClientBasePort) + err := c.transport.Ping() + if err != nil { + t.Errorf("Could not ping but not disconnected yet") + } + c.Disconnect() + err = c.transport.Ping() + if err == nil { + t.Errorf("Did not disconnect properly") + } + m.Stop() +} + +func TestClient_DisconnectStreamManager(t *testing.T) { + // Init mock server + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, handlerAbortTLS) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + } + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + t.Errorf("cannot create XMPP client: %s", err) + } + + sman := NewStreamManager(client, nil) + errChan := make(chan error) + runSMan := func(errChan chan error) { + errChan <- sman.Run() + } + + go runSMan(errChan) + select { + case <-errChan: + case <-time.After(defaultChannelTimeout): + // When insecure is not allowed: + t.Errorf("should fail as insecure connection is not allowed and server does not support TLS") + } + mock.Stop() +} + //============================================================================= // Basic XMPP Server Mock Handlers. -const serverStreamOpen = "" - // Test connection with a basic straightforward workflow -func handlerConnectSuccess(t *testing.T, c net.Conn) { +func handlerClientConnectSuccess(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - checkOpenStream(t, c, decoder) + checkClientOpenStream(t, c, decoder) sendStreamFeatures(t, c, decoder) // Send initial features readAuth(t, decoder) fmt.Fprintln(c, "") - checkOpenStream(t, c, decoder) // Reset stream - sendBindFeature(t, c, decoder) // Send post auth features + checkClientOpenStream(t, c, decoder) // Reset stream + sendBindFeature(t, c, decoder) // Send post auth features bind(t, c, decoder) } // We expect client will abort on TLS func handlerAbortTLS(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - checkOpenStream(t, c, decoder) + checkClientOpenStream(t, c, decoder) sendStreamFeatures(t, c, decoder) // Send initial features } // Test connection with mandatory session (RFC-3921) -func handlerConnectWithSession(t *testing.T, c net.Conn) { +func handlerClientConnectWithSession(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - checkOpenStream(t, c, decoder) + checkClientOpenStream(t, c, decoder) sendStreamFeatures(t, c, decoder) // Send initial features readAuth(t, decoder) fmt.Fprintln(c, "") - checkOpenStream(t, c, decoder) // Reset stream - sendRFC3921Feature(t, c, decoder) // Send post auth features + checkClientOpenStream(t, c, decoder) // Reset stream + sendRFC3921Feature(t, c, decoder) // Send post auth features bind(t, c, decoder) session(t, c, decoder) } -func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { +func checkClientOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { c.SetDeadline(time.Now().Add(defaultTimeout)) defer c.SetDeadline(time.Time{}) @@ -202,105 +408,35 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { } } -func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 1 stream feature: SASL Plain Auth - features := ` - - PLAIN - -` - if _, err := fmt.Fprintln(c, features); err != nil { - t.Errorf("cannot send stream feature: %s", err) +func mockClientConnection(t *testing.T, serverHandler func(*testing.T, net.Conn), port int) (*Client, ServerMock) { + mock := ServerMock{} + testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) + + mock.Start(t, testServerAddress, serverHandler) + + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testServerAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true} + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + t.Errorf("connect create XMPP client: %s", err) } + + if err = client.Connect(); err != nil { + t.Errorf("XMPP connection failed: %s", err) + } + + return client, mock } -// TODO return err in case of error reading the auth params -func readAuth(t *testing.T, decoder *xml.Decoder) string { - se, err := stanza.NextStart(decoder) - if err != nil { - t.Errorf("cannot read auth: %s", err) - return "" - } - - var nv interface{} - nv = &stanza.SASLAuth{} - // Decode element into pointer storage - if err = decoder.DecodeElement(nv, &se); err != nil { - t.Errorf("cannot decode auth: %s", err) - return "" - } - - switch v := nv.(type) { - case *stanza.SASLAuth: - return v.Value - } - return "" -} - -func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 1 stream feature after auth: resource binding - features := ` - -` - if _, err := fmt.Fprintln(c, features); err != nil { - t.Errorf("cannot send stream feature: %s", err) - } -} - -func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 2 features after auth: resource & session binding - features := ` - - -` - if _, err := fmt.Fprintln(c, features); err != nil { - t.Errorf("cannot send stream feature: %s", err) - } -} - -func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := stanza.NextStart(decoder) - if err != nil { - t.Errorf("cannot read bind: %s", err) - return - } - - iq := &stanza.IQ{} - // Decode element into pointer storage - if err = decoder.DecodeElement(&iq, &se); err != nil { - t.Errorf("cannot decode bind iq: %s", err) - return - } - - // TODO Check all elements - switch iq.Payload.(type) { - case *stanza.Bind: - result := ` - - %s - -` - fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID - } -} - -func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := stanza.NextStart(decoder) - if err != nil { - t.Errorf("cannot read session: %s", err) - return - } - - iq := &stanza.IQ{} - // Decode element into pointer storage - if err = decoder.DecodeElement(&iq, &se); err != nil { - t.Errorf("cannot decode session iq: %s", err) - return - } - - switch iq.Payload.(type) { - case *stanza.StreamSession: - result := `` - fmt.Fprintf(c, result, iq.Id) - } +// This really should not be used as is. +// It's just meant to be a placeholder when error handling is not needed at this level +func clientDefaultErrorHandler(err error) { } diff --git a/component.go b/component.go index 471f1db..2f61aef 100644 --- a/component.go +++ b/component.go @@ -48,11 +48,12 @@ type Component struct { transport Transport // read / write - socketProxy io.ReadWriter // TODO + socketProxy io.ReadWriter // TODO + ErrorHandler func(error) } -func NewComponent(opts ComponentOptions, r *Router) (*Component, error) { - c := Component{ComponentOptions: opts, router: r} +func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*Component, error) { + c := Component{ComponentOptions: opts, router: r, ErrorHandler: errorHandler} return &c, nil } @@ -104,11 +105,8 @@ func (c *Component) Resume(sm SMState) error { case stanza.Handshake: // Start the receiver go routine c.updateState(StateSessionEstablished) - // Leaving this channel here for later. Not used atm. We should return this instead of an error because right - // now the returned error is lost in limbo. - errChan := make(chan error) - go c.recv(errChan) // Sends to errChan - return err // Should be empty at this point + go c.recv() + return err // Should be empty at this point default: c.updateState(StatePermanentError) return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true) @@ -128,13 +126,13 @@ func (c *Component) SetHandler(handler EventHandler) { } // Receiver Go routine receiver -func (c *Component) recv(errChan chan<- error) { +func (c *Component) recv() { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.updateState(StateDisconnected) - errChan <- err + c.ErrorHandler(err) return } // Handle stream errors @@ -142,7 +140,7 @@ func (c *Component) recv(errChan chan<- error) { case stanza.StreamError: c.router.route(c, val) c.streamError(p.Error.Local, p.Text) - errChan <- errors.New("stream error: " + p.Error.Local) + c.ErrorHandler(errors.New("stream error: " + p.Error.Local)) return } c.router.route(c, val) diff --git a/component_test.go b/component_test.go index 4e115f0..48963a5 100644 --- a/component_test.go +++ b/component_test.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "errors" "fmt" + "github.com/google/uuid" "gosrc.io/xmpp/stanza" "net" "strings" @@ -15,19 +16,7 @@ import ( // Tests are ran in parallel, so each test creating a server must use a different port so we do not get any // conflict. Using iota for this should do the trick. const ( - testComponentDomain = "localhost" - defaultServerName = "testServer" - defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545" - defaultComponentName = "Test Component" - - // Default port is not standard XMPP port to avoid interfering - // with local running XMPP server - testHandshakePort = iota + 15222 - testDecoderPort - testSendIqPort - testSendRawPort - testDisconnectPort - testSManDisconnectPort + defaultChannelTimeout = 5 * time.Second ) func TestHandshake(t *testing.T) { @@ -48,16 +37,14 @@ func TestHandshake(t *testing.T) { // Tests connection process with a handshake exchange // Tests multiple session IDs. All connections should generate a unique stream ID -func TestGenerateHandshake(t *testing.T) { +func TestGenerateHandshakeId(t *testing.T) { // Using this array with a channel to make a queue of values to test // These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate // some handshake value - var uuidsArray = [5]string{ - "cc9b3249-9582-4780-825f-4311b42f9b0e", - "bba8be3c-d98e-4e26-b9bb-9ed34578a503", - "dae72822-80e8-496b-b763-ab685f53a188", - "a45d6c06-de49-4bb0-935b-1a2201b71028", - "7dc6924f-0eca-4237-9898-18654b8d891e", + var uuidsArray = [5]string{} + for i := 1; i < len(uuidsArray); i++ { + id, _ := uuid.NewRandom() + uuidsArray[i] = id.String() } // Channel to pass stream IDs as a queue @@ -95,7 +82,7 @@ func TestGenerateHandshake(t *testing.T) { Type: "service", } router := NewRouter() - c, err := NewComponent(opts, router) + c, err := NewComponent(opts, router, componentDefaultErrorHandler) if err != nil { t.Errorf("%+v", err) } @@ -126,7 +113,7 @@ func TestStreamManager(t *testing.T) { // The decoder is expected to be built after a valid connection // Based on the xmpp_component example. func TestDecoder(t *testing.T) { - c, _ := mockConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID) + c, _ := mockComponentConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID) if c.transport.GetDecoder() == nil { t.Errorf("Failed to initialize decoder. Decoder is nil.") } @@ -134,39 +121,103 @@ func TestDecoder(t *testing.T) { // Tests sending an IQ to the server, and getting the response func TestSendIq(t *testing.T) { + done := make(chan struct{}) + h := func(t *testing.T, c net.Conn) { + handlerForComponentIQSend(t, c) + done <- struct{}{} + } + //Connecting to a mock server, initialized with given port and handler function - c, m := mockConnection(t, testSendIqPort, handlerForComponentIQSend) + c, m := mockComponentConnection(t, testSendIqPort, h) ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) disco := iqReq.DiscoInfo() iqReq.Payload = disco + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + c.ErrorHandler = errorHandler + var res chan stanza.IQ res, _ = c.SendIQ(ctx, iqReq) select { case <-res: - case <-time.After(100 * time.Millisecond): + case err := <-errChan: + t.Errorf(err.Error()) + case <-time.After(defaultChannelTimeout): t.Errorf("Failed to receive response, to sent IQ, from mock server") } - m.Stop() + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } +} + +// Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind. +func TestSendIqFail(t *testing.T) { + done := make(chan struct{}) + h := func(t *testing.T, c net.Conn) { + handlerForComponentIQSend(t, c) + done <- struct{}{} + } + //Connecting to a mock server, initialized with given port and handler function + c, m := mockComponentConnection(t, testSendIqFailPort, h) + + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + + // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified + // so we need to overwrite it. + iqReq.Id = "" + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + c.ErrorHandler = errorHandler + + var res chan stanza.IQ + res, _ = c.SendIQ(ctx, iqReq) + + select { + case r := <-res: // Do we get an IQ response from the server ? + t.Errorf("We should not be getting an IQ response here : this should fail !") + fmt.Println(r) + case <-errChan: // Do we get a stream error from the server ? + // If we get an error from the server, the test passes. + case <-time.After(defaultChannelTimeout): // Timeout ? + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } } // Tests sending raw xml to the mock server. -// TODO : check the server response client side ? // Right now, the server response is not checked and an err is passed in a channel if the test is supposed to err. // In this test, we use IQs func TestSendRaw(t *testing.T) { - // Error channel for the handler - errChan := make(chan error) + done := make(chan struct{}) // Handler for the mock server h := func(t *testing.T, c net.Conn) { // Completes the connection by exchanging handshakes handlerForComponentHandshakeDefaultID(t, c) - receiveRawIq(t, c, errChan) - return + receiveIq(c, xml.NewDecoder(c)) + done <- struct{}{} } type testCase struct { @@ -185,12 +236,19 @@ func TestSendRaw(t *testing.T) { shouldErr: true, } + // A handler for the component. + // In the failing test, the server returns a stream error, which triggers this handler, component side. + errChan := make(chan error) + errHandler := func(err error) { + errChan <- err + } + // Tests for all the IQs for name, tcase := range testRequests { t.Run(name, func(st *testing.T) { //Connecting to a mock server, initialized with given port and handler function - c, m := mockConnection(t, testSendRawPort, h) - + c, m := mockComponentConnection(t, testSendRawPort, h) + c.ErrorHandler = errHandler // Sending raw xml from test case err := c.SendRaw(tcase.req) if err != nil { @@ -198,21 +256,29 @@ func TestSendRaw(t *testing.T) { } // Just wait a little so the message has time to arrive select { - case <-time.After(100 * time.Millisecond): + // We don't use the default "long" timeout here because waiting it out means passing the test. + case <-time.After(200 * time.Millisecond): case err = <-errChan: if err == nil && tcase.shouldErr { t.Errorf("Failed to get closing stream err") + } else if err != nil && !tcase.shouldErr { + t.Errorf("This test is not supposed to err ! => %s", err.Error()) } } c.transport.Close() - m.Stop() + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } }) } } // Tests the Disconnect method for Components func TestDisconnect(t *testing.T) { - c, m := mockConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID) + c, m := mockComponentConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID) err := c.transport.Ping() if err != nil { t.Errorf("Could not ping but not disconnected yet") @@ -257,14 +323,97 @@ func TestStreamManagerDisconnect(t *testing.T) { //============================================================================= // Basic XMPP Server Mock Handlers. -// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. -// Used in the mock server as a Handler -func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkOpenStreamHandshakeDefaultID(t, c, decoder) - readHandshakeComponent(t, decoder) - fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) - return + +//=============================== +// Init mock server and connection +// Creating a mock server and connecting a Component to it. Initialized with given port and handler function +// The Component and mock are both returned +func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) { + // Init mock server + testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port) + mock := ServerMock{} + mock.Start(t, testComponentAddress, handler) + + //================================== + // Create Component to connect to it + c := makeBasicComponent(defaultComponentName, testComponentAddress, t) + + //======================================== + // Connect the new Component to the server + err := c.Connect() + if err != nil { + t.Errorf("%+v", err) + } + + return c, &mock +} + +func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component { + opts := ComponentOptions{ + TransportConfiguration: TransportConfiguration{ + Address: mockServerAddr, + Domain: "localhost", + }, + Domain: testComponentDomain, + Secret: "mypass", + Name: name, + Category: "gateway", + Type: "service", + } + router := NewRouter() + c, err := NewComponent(opts, router, componentDefaultErrorHandler) + if err != nil { + t.Errorf("%+v", err) + } + c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) + if err != nil { + t.Errorf("%+v", err) + } + return c +} + +// This really should not be used as is. +// It's just meant to be a placeholder when error handling is not needed at this level +func componentDefaultErrorHandler(err error) { + +} + +// Sends IQ response to Component request. +// No parsing of the request here. We just check that it's valid, and send the default response. +func handlerForComponentIQSend(t *testing.T, c net.Conn) { + // Completes the connection by exchanging handshakes + handlerForComponentHandshakeDefaultID(t, c) + respondToIQ(t, c) +} + +// Used for ID and handshake related tests +func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + + for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. + token, err := decoder.Token() + if err != nil { + t.Errorf("cannot read next token: %s", err) + } + + switch elem := token.(type) { + // Wait for first startElement + case xml.StartElement: + 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", streamID, stanza.NSComponent, stanza.NSStream); err != nil { + t.Errorf("cannot write server stream open: %s", err) + } + return + } + } +} + +func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) { + checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID) } // Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. @@ -303,152 +452,12 @@ func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) { } } -func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) { - checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID) -} - -// Used for ID and handshake related tests -func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) - - for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. - token, err := decoder.Token() - if err != nil { - t.Errorf("cannot read next token: %s", err) - } - - switch elem := token.(type) { - // Wait for first startElement - case xml.StartElement: - 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", streamID, stanza.NSComponent, stanza.NSStream); err != nil { - t.Errorf("cannot write server stream open: %s", err) - } - return - } - } -} - -//============================================================================= -// Sends IQ response to Component request. -// No parsing of the request here. We just check that it's valid, and send the default response. -func handlerForComponentIQSend(t *testing.T, c net.Conn) { - // Completes the connection by exchanging handshakes - handlerForComponentHandshakeDefaultID(t, c) - - // Decoder to parse the request +// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. +// Used in the mock server as a Handler +func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - - iqReq, err := receiveIq(t, c, decoder) - if err != nil { - t.Errorf("Error receiving the IQ stanza : %v", err) - } else if !iqReq.IsValid() { - t.Errorf("server received an IQ stanza : %v", iqReq) - } - - // Crafting response - iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) - disco := iqResp.DiscoInfo() - disco.AddFeatures("vcard-temp", - `http://jabber.org/protocol/address`) - - disco.AddIdentity("Multicast", "service", "multicast") - iqResp.Payload = disco - - // Sending response to the Component - mResp, err := xml.Marshal(iqResp) - _, err = fmt.Fprintln(c, string(mResp)) - if err != nil { - t.Errorf("Could not send response stanza : %s", err) - } + checkOpenStreamHandshakeDefaultID(t, c, decoder) + readHandshakeComponent(t, decoder) + fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) return } - -// Reads next request coming from the Component. Expecting it to be an IQ request -func receiveIq(t *testing.T, c net.Conn, decoder *xml.Decoder) (stanza.IQ, error) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) - var iqStz stanza.IQ - err := decoder.Decode(&iqStz) - if err != nil { - t.Errorf("cannot read the received IQ stanza: %s", err) - } - if !iqStz.IsValid() { - t.Errorf("received IQ stanza is invalid : %s", err) - } - return iqStz, nil -} - -func receiveRawIq(t *testing.T, c net.Conn, errChan chan error) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) - decoder := xml.NewDecoder(c) - var iq stanza.IQ - err := decoder.Decode(&iq) - if err != nil || !iq.IsValid() { - s := stanza.StreamError{ - XMLName: xml.Name{Local: "stream:error"}, - Error: xml.Name{Local: "xml-not-well-formed"}, - Text: `XML was not well-formed`, - } - raw, _ := xml.Marshal(s) - fmt.Fprintln(c, string(raw)) - fmt.Fprintln(c, ``) // TODO : check this client side - errChan <- fmt.Errorf("invalid xml") - return - } - errChan <- nil - return -} - -//=============================== -// Init mock server and connection -// Creating a mock server and connecting a Component to it. Initialized with given port and handler function -// The Component and mock are both returned -func mockConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) { - // Init mock server - testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port) - mock := ServerMock{} - mock.Start(t, testComponentAddress, handler) - - //================================== - // Create Component to connect to it - c := makeBasicComponent(defaultComponentName, testComponentAddress, t) - - //======================================== - // Connect the new Component to the server - err := c.Connect() - if err != nil { - t.Errorf("%+v", err) - } - - return c, &mock -} - -func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component { - opts := ComponentOptions{ - TransportConfiguration: TransportConfiguration{ - Address: mockServerAddr, - Domain: "localhost", - }, - Domain: testComponentDomain, - Secret: "mypass", - Name: name, - Category: "gateway", - Type: "service", - } - router := NewRouter() - c, err := NewComponent(opts, router) - if err != nil { - t.Errorf("%+v", err) - } - c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) - if err != nil { - t.Errorf("%+v", err) - } - return c -} diff --git a/tcp_server_mock.go b/tcp_server_mock.go index bdc4397..4afed80 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -1,12 +1,42 @@ package xmpp import ( + "encoding/xml" + "fmt" + "gosrc.io/xmpp/stanza" "net" "testing" + "time" ) //============================================================================= // TCP Server Mock +const ( + defaultTimeout = 2 * time.Second + testComponentDomain = "localhost" + defaultServerName = "testServer" + defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545" + defaultComponentName = "Test Component" + serverStreamOpen = "" + + // Default port is not standard XMPP port to avoid interfering + // with local running XMPP server + + // Component tests + testHandshakePort = iota + 15222 + testDecoderPort + testSendIqPort + testSendIqFailPort + testSendRawPort + testDisconnectPort + testSManDisconnectPort + + // Client tests + testClientBasePort + testClientRawPort + testClientIqPort + testClientIqFailPort +) // ClientHandler is passed by the test client to provide custom behaviour to // the TCP server mock. This allows customizing the server behaviour to allow @@ -81,3 +111,180 @@ func (mock *ServerMock) loop() { go mock.handler(mock.t, conn) } } + +//====================================================================================================================== +// A few functions commonly used for tests. Trying to avoid duplicates in client and component test files. +//====================================================================================================================== + +func respondToIQ(t *testing.T, c net.Conn) { + // Decoder to parse the request + decoder := xml.NewDecoder(c) + + iqReq, err := receiveIq(c, decoder) + if err != nil { + t.Fatalf("failed to receive IQ : %s", err.Error()) + } + + if !iqReq.IsValid() { + mockIQError(c) + return + } + + // Crafting response + iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) + disco := iqResp.DiscoInfo() + disco.AddFeatures("vcard-temp", + `http://jabber.org/protocol/address`) + + disco.AddIdentity("Multicast", "service", "multicast") + iqResp.Payload = disco + + // Sending response to the Component + mResp, err := xml.Marshal(iqResp) + _, err = fmt.Fprintln(c, string(mResp)) + if err != nil { + t.Errorf("Could not send response stanza : %s", err) + } + return +} + +// When a presence stanza is automatically sent (right now it's the case in the client), we may want to discard it +// and test further stanzas. +func discardPresence(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + var presenceStz stanza.Presence + err := decoder.Decode(&presenceStz) + if err != nil { + t.Errorf("Expected presence but this happened : %s", err.Error()) + } +} + +// Reads next request coming from the Component. Expecting it to be an IQ request +func receiveIq(c net.Conn, decoder *xml.Decoder) (*stanza.IQ, error) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + var iqStz stanza.IQ + err := decoder.Decode(&iqStz) + if err != nil { + return nil, err + } + return &iqStz, nil +} + +// Should be used in server handlers when an IQ sent by a client or component is invalid. +// This responds as expected from a "real" server, aside from the error message. +func mockIQError(c net.Conn) { + s := stanza.StreamError{ + XMLName: xml.Name{Local: "stream:error"}, + Error: xml.Name{Local: "xml-not-well-formed"}, + Text: `XML was not well-formed`, + } + raw, _ := xml.Marshal(s) + fmt.Fprintln(c, string(raw)) + fmt.Fprintln(c, ``) +} + +func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) { + // This is a basic server, supporting only 1 stream feature: SASL Plain Auth + features := ` + + PLAIN + +` + if _, err := fmt.Fprintln(c, features); err != nil { + t.Errorf("cannot send stream feature: %s", err) + } +} + +// TODO return err in case of error reading the auth params +func readAuth(t *testing.T, decoder *xml.Decoder) string { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read auth: %s", err) + return "" + } + + var nv interface{} + nv = &stanza.SASLAuth{} + // Decode element into pointer storage + if err = decoder.DecodeElement(nv, &se); err != nil { + t.Errorf("cannot decode auth: %s", err) + return "" + } + + switch v := nv.(type) { + case *stanza.SASLAuth: + return v.Value + } + return "" +} + +func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { + // This is a basic server, supporting only 1 stream feature after auth: resource binding + features := ` + +` + if _, err := fmt.Fprintln(c, features); err != nil { + t.Errorf("cannot send stream feature: %s", err) + } +} + +func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) { + // This is a basic server, supporting only 2 features after auth: resource & session binding + features := ` + + +` + if _, err := fmt.Fprintln(c, features); err != nil { + t.Errorf("cannot send stream feature: %s", err) + } +} + +func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read bind: %s", err) + return + } + + iq := &stanza.IQ{} + // Decode element into pointer storage + if err = decoder.DecodeElement(&iq, &se); err != nil { + t.Errorf("cannot decode bind iq: %s", err) + return + } + + // TODO Check all elements + switch iq.Payload.(type) { + case *stanza.Bind: + result := ` + + %s + +` + fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID + } +} + +func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read session: %s", err) + return + } + + iq := &stanza.IQ{} + // Decode element into pointer storage + if err = decoder.DecodeElement(&iq, &se); err != nil { + t.Errorf("cannot decode session iq: %s", err) + return + } + + switch iq.Payload.(type) { + case *stanza.StreamSession: + result := `` + fmt.Fprintf(c, result, iq.Id) + } +} From f8c992a385ec7a9121e0dc1b544a0c51c0741275 Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Mon, 9 Dec 2019 12:30:37 +0100 Subject: [PATCH 13/54] Fix updating of EventManager.CurrentState The EventManager methods did not use a pointer as receiver, which caused updated of CurrentState to be lost. --- client.go | 6 +++--- client_test.go | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 14537db..f075a94 100644 --- a/client.go +++ b/client.go @@ -60,21 +60,21 @@ type EventManager struct { Handler EventHandler } -func (em EventManager) updateState(state ConnState) { +func (em *EventManager) updateState(state ConnState) { em.CurrentState = state if em.Handler != nil { em.Handler(Event{State: em.CurrentState}) } } -func (em EventManager) disconnected(state SMState) { +func (em *EventManager) disconnected(state SMState) { em.CurrentState = StateDisconnected if em.Handler != nil { em.Handler(Event{State: em.CurrentState, SMState: state}) } } -func (em EventManager) streamError(error, desc string) { +func (em *EventManager) streamError(error, desc string) { em.CurrentState = StateStreamError if em.Handler != nil { em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc}) diff --git a/client_test.go b/client_test.go index 2636f29..e18e8ba 100644 --- a/client_test.go +++ b/client_test.go @@ -19,6 +19,24 @@ const ( defaultTimeout = 2 * time.Second ) +func TestEventManager(t *testing.T) { + mgr := EventManager{} + mgr.updateState(StateConnected) + if mgr.CurrentState != StateConnected { + t.Fatal("CurrentState not updated by updateState()") + } + + mgr.disconnected(SMState{}) + if mgr.CurrentState != StateDisconnected { + t.Fatalf("CurrentState not reset by disconnected()") + } + + mgr.streamError(ErrTLSNotSupported.Error(), "") + if mgr.CurrentState != StateStreamError { + t.Fatalf("CurrentState not set by streamError()") + } +} + func TestClient_Connect(t *testing.T) { // Setup Mock server mock := ServerMock{} From f41177775a2aedf99eeea3ab3c6a8f97762d5e39 Mon Sep 17 00:00:00 2001 From: Wichert Akkerman Date: Wed, 4 Dec 2019 22:17:58 +0100 Subject: [PATCH 14/54] Make keepalive interval configurable This fixes #133 --- client.go | 10 ++++++---- config.go | 14 ++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index 14537db..f49b298 100644 --- a/client.go +++ b/client.go @@ -108,6 +108,9 @@ Setting up the client / Checking the parameters // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. // Default the port to 5222. func NewClient(config Config, r *Router) (c *Client, err error) { + if config.KeepaliveInterval == 0 { + config.KeepaliveInterval = time.Second * 30 + } // Parse JID if config.parsedJid, err = NewJid(config.Jid); err != nil { err = errors.New("missing jid") @@ -185,7 +188,7 @@ func (c *Client) Resume(state SMState) error { // Start the keepalive go routine keepaliveQuit := make(chan struct{}) - go keepalive(c.transport, keepaliveQuit) + go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) // Start the receiver go routine state = c.Session.SMState // Leaving this channel here for later. Not used atm. We should return this instead of an error because right @@ -312,9 +315,8 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan // Loop: send whitespace keepalive to server // This is use to keep the connection open, but also to detect connection loss // and trigger proper client connection shutdown. -func keepalive(transport Transport, quit <-chan struct{}) { - // TODO: Make keepalive interval configurable - ticker := time.NewTicker(30 * time.Second) +func keepalive(transport Transport, interval time.Duration, quit <-chan struct{}) { + ticker := time.NewTicker(interval) for { select { case <-ticker.C: diff --git a/config.go b/config.go index e3ea108..da4d4ab 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package xmpp import ( "os" + "time" ) // Config & TransportConfiguration must not be modified after having been passed to NewClient. Any @@ -9,12 +10,13 @@ import ( type Config struct { TransportConfiguration - Jid string - parsedJid *Jid // For easier manipulation - Credential Credential - StreamLogger *os.File // Used for debugging - Lang string // TODO: should default to 'en' - ConnectTimeout int // Client timeout in seconds. Default to 15 + Jid string + parsedJid *Jid // For easier manipulation + Credential Credential + StreamLogger *os.File // Used for debugging + Lang string // TODO: should default to 'en' + KeepaliveInterval time.Duration // Interval between keepalive packets + ConnectTimeout int // Client timeout in seconds. Default to 15 // Insecure can be set to true to allow to open a session without TLS. If TLS // is supported on the server, we will still try to use it. Insecure bool From e675e65a592d6ccd15714a524d047fa14c09aed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Thu, 5 Dec 2019 18:12:00 +0100 Subject: [PATCH 15/54] Added callback to process errors after connection. Added tests and refactored a bit. --- .../xmpp_chat_client/xmpp_chat_client.go | 95 +++++ _examples/xmpp_component/xmpp_component.go | 6 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 5 +- _examples/xmpp_oauth2/xmpp_oauth2.go | 6 +- _examples/xmpp_websocket/xmpp_websocket.go | 6 +- client.go | 18 +- client_test.go | 374 +++++++++++------ component.go | 20 +- component_test.go | 387 +++++++++--------- tcp_server_mock.go | 207 ++++++++++ 10 files changed, 792 insertions(+), 332 deletions(-) create mode 100644 _examples/xmpp_chat_client/xmpp_chat_client.go diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go new file mode 100644 index 0000000..2b2d2e7 --- /dev/null +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -0,0 +1,95 @@ +package main + +/* +xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members +Note that this example sends to a very specific user. User logic is not implemented here. +*/ + +import ( + . "bufio" + "fmt" + "os" + + "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" +) + +const ( + currentUserAddress = "localhost:5222" + currentUserJid = "testuser@localhost" + currentUserPass = "testpass" + correspondantJid = "testuser2@localhost" +) + +func main() { + config := xmpp.Config{ + TransportConfiguration: xmpp.TransportConfiguration{ + Address: currentUserAddress, + }, + Jid: currentUserJid, + Credential: xmpp.Password(currentUserPass), + Insecure: true} + + var client *xmpp.Client + var err error + router := xmpp.NewRouter() + router.HandleFunc("message", handleMessage) + if client, err = xmpp.NewClient(config, router, errorHandler); err != nil { + fmt.Println("Error new client") + } + + // Connecting client and handling messages + // To use a stream manager, just write something like this instead : + //cm := xmpp.NewStreamManager(client, startMessaging) + //log.Fatal(cm.Run()) //=> this will lock the calling goroutine + + if err = client.Connect(); err != nil { + fmt.Printf("XMPP connection failed: %s", err) + return + } + startMessaging(client) + +} + +func startMessaging(client xmpp.Sender) { + reader := NewReader(os.Stdin) + textChan := make(chan string) + var text string + for { + fmt.Print("Enter text: ") + go readInput(reader, textChan) + select { + case <-killChan: + return + case text = <-textChan: + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondantJid}, Body: text} + err := client.Send(reply) + if err != nil { + fmt.Printf("There was a problem sending the message : %v", reply) + return + } + } + } +} + +func readInput(reader *Reader, textChan chan string) { + text, _ := reader.ReadString('\n') + textChan <- text +} + +var killChan = make(chan struct{}) + +// If an error occurs, this is used +func errorHandler(err error) { + fmt.Printf("%v", err) + killChan <- struct{}{} +} + +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) +} diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index 0452888..7f676cb 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -35,7 +35,7 @@ func main() { IQNamespaces("jabber:iq:version"). HandlerFunc(handleVersion) - component, err := xmpp.NewComponent(opts, router) + component, err := xmpp.NewComponent(opts, router, handleError) if err != nil { log.Fatalf("%+v", err) } @@ -47,6 +47,10 @@ func main() { log.Fatal(cm.Run()) } +func handleError(err error) { + fmt.Println(err.Error()) +} + func handleMessage(_ xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if !ok { diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index 91f453c..ce7ebc9 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -53,7 +53,7 @@ func main() { handleIQ(s, p, player) }) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -61,6 +61,9 @@ func main() { cm := xmpp.NewStreamManager(client, nil) log.Fatal(cm.Run()) } +func errorHandler(err error) { + fmt.Println(err.Error()) +} func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { msg, ok := p.(stanza.Message) diff --git a/_examples/xmpp_oauth2/xmpp_oauth2.go b/_examples/xmpp_oauth2/xmpp_oauth2.go index f322447..89b2639 100644 --- a/_examples/xmpp_oauth2/xmpp_oauth2.go +++ b/_examples/xmpp_oauth2/xmpp_oauth2.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -39,6 +39,10 @@ func main() { log.Fatal(cm.Run()) } +func errorHandler(err error) { + fmt.Println(err.Error()) +} + func handleMessage(s xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if !ok { diff --git a/_examples/xmpp_websocket/xmpp_websocket.go b/_examples/xmpp_websocket/xmpp_websocket.go index 428a1d1..c8c0620 100644 --- a/_examples/xmpp_websocket/xmpp_websocket.go +++ b/_examples/xmpp_websocket/xmpp_websocket.go @@ -26,7 +26,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -37,6 +37,10 @@ func main() { log.Fatal(cm.Run()) } +func errorHandler(err error) { + fmt.Println(err.Error()) +} + func handleMessage(s xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if !ok { diff --git a/client.go b/client.go index ecb2aad..a5ad1bf 100644 --- a/client.go +++ b/client.go @@ -98,6 +98,8 @@ type Client struct { router *Router // Track and broadcast connection state EventManager + // Handle errors from client execution + ErrorHandler func(error) } /* @@ -107,7 +109,7 @@ Setting up the client / Checking the parameters // NewClient generates a new XMPP client, based on Config passed as parameters. // If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. // Default the port to 5222. -func NewClient(config Config, r *Router) (c *Client, err error) { +func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { if config.KeepaliveInterval == 0 { config.KeepaliveInterval = time.Second * 30 } @@ -143,6 +145,7 @@ func NewClient(config Config, r *Router) (c *Client, err error) { c = new(Client) c.config = config c.router = r + c.ErrorHandler = errorHandler if c.config.ConnectTimeout == 0 { c.config.ConnectTimeout = 15 // 15 second as default @@ -191,10 +194,7 @@ func (c *Client) Resume(state SMState) error { go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) // Start the receiver go routine state = c.Session.SMState - // Leaving this channel here for later. Not used atm. We should return this instead of an error because right - // now the returned error is lost in limbo. - errChan := make(chan error) - go c.recv(state, keepaliveQuit, errChan) + go c.recv(state, keepaliveQuit) // We're connected and can now receive and send messages. //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") @@ -273,11 +273,11 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Go routines // Loop: Receive data from server -func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan<- error) { +func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { - errChan <- err + c.ErrorHandler(err) close(keepaliveQuit) c.disconnected(state) return @@ -289,7 +289,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan c.router.route(c, val) close(keepaliveQuit) c.streamError(packet.Error.Local, packet.Text) - errChan <- errors.New("stream error: " + packet.Error.Local) + c.ErrorHandler(errors.New("stream error: " + packet.Error.Local)) return // Process Stream management nonzas case stanza.SMRequest: @@ -299,7 +299,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}, errChan chan }, H: state.Inbound} err = c.Send(answer) if err != nil { - errChan <- err + c.ErrorHandler(err) return } default: diff --git a/client_test.go b/client_test.go index e18e8ba..0caace0 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,7 @@ package xmpp import ( + "context" "encoding/xml" "errors" "fmt" @@ -14,9 +15,8 @@ import ( const ( // Default port is not standard XMPP port to avoid interfering // with local running XMPP server - testXMPPAddress = "localhost:15222" - - defaultTimeout = 2 * time.Second + testXMPPAddress = "localhost:15222" + testClientDomain = "localhost" ) func TestEventManager(t *testing.T) { @@ -40,7 +40,7 @@ func TestEventManager(t *testing.T) { func TestClient_Connect(t *testing.T) { // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerConnectSuccess) + mock.Start(t, testXMPPAddress, handlerClientConnectSuccess) // Test / Check result config := Config{ @@ -54,7 +54,7 @@ func TestClient_Connect(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -82,7 +82,7 @@ func TestClient_NoInsecure(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -112,7 +112,7 @@ func TestClient_FeaturesTracking(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -127,7 +127,7 @@ func TestClient_FeaturesTracking(t *testing.T) { func TestClient_RFC3921Session(t *testing.T) { // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerConnectWithSession) + mock.Start(t, testXMPPAddress, handlerClientConnectWithSession) // Test / Check result config := Config{ @@ -142,7 +142,7 @@ func TestClient_RFC3921Session(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router); err != nil { + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -153,48 +153,254 @@ func TestClient_RFC3921Session(t *testing.T) { mock.Stop() } +// Testing sending an IQ to the mock server and reading its response. +func TestClient_SendIQ(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, c net.Conn) { + handlerClientConnectSuccess(t, c) + discardPresence(t, c) + respondToIQ(t, c) + done <- struct{}{} + } + client, mock := mockClientConnection(t, h, testClientIqPort) + + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + client.ErrorHandler = errorHandler + res, err := client.SendIQ(ctx, iqReq) + if err != nil { + t.Errorf(err.Error()) + } + + select { + case <-res: // If the server responds with an IQ, we pass the test + case err := <-errChan: // If the server sends an error, or there is a connection error + t.Errorf(err.Error()) + case <-time.After(defaultChannelTimeout): // If we timeout + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + select { + case <-done: + mock.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } +} + +func TestClient_SendIQFail(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, c net.Conn) { + handlerClientConnectSuccess(t, c) + discardPresence(t, c) + respondToIQ(t, c) + done <- struct{}{} + } + client, mock := mockClientConnection(t, h, testClientIqFailPort) + + //================== + // Create an IQ to send + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified + // so we need to overwrite it. + iqReq.Id = "" + + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + client.ErrorHandler = errorHandler + res, _ := client.SendIQ(ctx, iqReq) + + // Test + select { + case <-res: // If the server responds with an IQ + t.Errorf("Server should not respond with an IQ since the request is expected to be invalid !") + case <-errChan: // If the server sends an error, the test passes + case <-time.After(defaultChannelTimeout): // If we timeout + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + select { + case <-done: + mock.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } +} + +func TestClient_SendRaw(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, c net.Conn) { + handlerClientConnectSuccess(t, c) + discardPresence(t, c) + respondToIQ(t, c) + done <- struct{}{} + } + type testCase struct { + req string + shouldErr bool + port int + } + testRequests := make(map[string]testCase) + // Sending a correct IQ of type get. Not supposed to err + testRequests["Correct IQ"] = testCase{ + req: ``, + shouldErr: false, + port: testClientRawPort + 100, + } + // Sending an IQ with a missing ID. Should err + testRequests["IQ with missing ID"] = testCase{ + req: ``, + shouldErr: true, + port: testClientRawPort, + } + + // A handler for the client. + // In the failing test, the server returns a stream error, which triggers this handler, client side. + errChan := make(chan error) + errHandler := func(err error) { + errChan <- err + } + + // Tests for all the IQs + for name, tcase := range testRequests { + t.Run(name, func(st *testing.T) { + //Connecting to a mock server, initialized with given port and handler function + c, m := mockClientConnection(t, h, tcase.port) + c.ErrorHandler = errHandler + // Sending raw xml from test case + err := c.SendRaw(tcase.req) + if err != nil { + t.Errorf("Error sending Raw string") + } + // Just wait a little so the message has time to arrive + select { + // We don't use the default "long" timeout here because waiting it out means passing the test. + case <-time.After(100 * time.Millisecond): + case err = <-errChan: + if err == nil && tcase.shouldErr { + t.Errorf("Failed to get closing stream err") + } else if err != nil && !tcase.shouldErr { + t.Errorf("This test is not supposed to err !") + } + } + c.transport.Close() + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } + }) + } +} + +func TestClient_Disconnect(t *testing.T) { + c, m := mockClientConnection(t, handlerClientConnectSuccess, testClientBasePort) + err := c.transport.Ping() + if err != nil { + t.Errorf("Could not ping but not disconnected yet") + } + c.Disconnect() + err = c.transport.Ping() + if err == nil { + t.Errorf("Did not disconnect properly") + } + m.Stop() +} + +func TestClient_DisconnectStreamManager(t *testing.T) { + // Init mock server + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, handlerAbortTLS) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + } + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + t.Errorf("cannot create XMPP client: %s", err) + } + + sman := NewStreamManager(client, nil) + errChan := make(chan error) + runSMan := func(errChan chan error) { + errChan <- sman.Run() + } + + go runSMan(errChan) + select { + case <-errChan: + case <-time.After(defaultChannelTimeout): + // When insecure is not allowed: + t.Errorf("should fail as insecure connection is not allowed and server does not support TLS") + } + mock.Stop() +} + //============================================================================= // Basic XMPP Server Mock Handlers. -const serverStreamOpen = "" - // Test connection with a basic straightforward workflow -func handlerConnectSuccess(t *testing.T, c net.Conn) { +func handlerClientConnectSuccess(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - checkOpenStream(t, c, decoder) + checkClientOpenStream(t, c, decoder) sendStreamFeatures(t, c, decoder) // Send initial features readAuth(t, decoder) fmt.Fprintln(c, "") - checkOpenStream(t, c, decoder) // Reset stream - sendBindFeature(t, c, decoder) // Send post auth features + checkClientOpenStream(t, c, decoder) // Reset stream + sendBindFeature(t, c, decoder) // Send post auth features bind(t, c, decoder) } // We expect client will abort on TLS func handlerAbortTLS(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - checkOpenStream(t, c, decoder) + checkClientOpenStream(t, c, decoder) sendStreamFeatures(t, c, decoder) // Send initial features } // Test connection with mandatory session (RFC-3921) -func handlerConnectWithSession(t *testing.T, c net.Conn) { +func handlerClientConnectWithSession(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - checkOpenStream(t, c, decoder) + checkClientOpenStream(t, c, decoder) sendStreamFeatures(t, c, decoder) // Send initial features readAuth(t, decoder) fmt.Fprintln(c, "") - checkOpenStream(t, c, decoder) // Reset stream - sendRFC3921Feature(t, c, decoder) // Send post auth features + checkClientOpenStream(t, c, decoder) // Reset stream + sendRFC3921Feature(t, c, decoder) // Send post auth features bind(t, c, decoder) session(t, c, decoder) } -func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { +func checkClientOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { c.SetDeadline(time.Now().Add(defaultTimeout)) defer c.SetDeadline(time.Time{}) @@ -220,105 +426,35 @@ func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { } } -func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 1 stream feature: SASL Plain Auth - features := ` - - PLAIN - -` - if _, err := fmt.Fprintln(c, features); err != nil { - t.Errorf("cannot send stream feature: %s", err) +func mockClientConnection(t *testing.T, serverHandler func(*testing.T, net.Conn), port int) (*Client, ServerMock) { + mock := ServerMock{} + testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) + + mock.Start(t, testServerAddress, serverHandler) + + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testServerAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true} + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + t.Errorf("connect create XMPP client: %s", err) } + + if err = client.Connect(); err != nil { + t.Errorf("XMPP connection failed: %s", err) + } + + return client, mock } -// TODO return err in case of error reading the auth params -func readAuth(t *testing.T, decoder *xml.Decoder) string { - se, err := stanza.NextStart(decoder) - if err != nil { - t.Errorf("cannot read auth: %s", err) - return "" - } - - var nv interface{} - nv = &stanza.SASLAuth{} - // Decode element into pointer storage - if err = decoder.DecodeElement(nv, &se); err != nil { - t.Errorf("cannot decode auth: %s", err) - return "" - } - - switch v := nv.(type) { - case *stanza.SASLAuth: - return v.Value - } - return "" -} - -func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 1 stream feature after auth: resource binding - features := ` - -` - if _, err := fmt.Fprintln(c, features); err != nil { - t.Errorf("cannot send stream feature: %s", err) - } -} - -func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 2 features after auth: resource & session binding - features := ` - - -` - if _, err := fmt.Fprintln(c, features); err != nil { - t.Errorf("cannot send stream feature: %s", err) - } -} - -func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := stanza.NextStart(decoder) - if err != nil { - t.Errorf("cannot read bind: %s", err) - return - } - - iq := &stanza.IQ{} - // Decode element into pointer storage - if err = decoder.DecodeElement(&iq, &se); err != nil { - t.Errorf("cannot decode bind iq: %s", err) - return - } - - // TODO Check all elements - switch iq.Payload.(type) { - case *stanza.Bind: - result := ` - - %s - -` - fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID - } -} - -func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := stanza.NextStart(decoder) - if err != nil { - t.Errorf("cannot read session: %s", err) - return - } - - iq := &stanza.IQ{} - // Decode element into pointer storage - if err = decoder.DecodeElement(&iq, &se); err != nil { - t.Errorf("cannot decode session iq: %s", err) - return - } - - switch iq.Payload.(type) { - case *stanza.StreamSession: - result := `` - fmt.Fprintf(c, result, iq.Id) - } +// This really should not be used as is. +// It's just meant to be a placeholder when error handling is not needed at this level +func clientDefaultErrorHandler(err error) { } diff --git a/component.go b/component.go index 471f1db..2f61aef 100644 --- a/component.go +++ b/component.go @@ -48,11 +48,12 @@ type Component struct { transport Transport // read / write - socketProxy io.ReadWriter // TODO + socketProxy io.ReadWriter // TODO + ErrorHandler func(error) } -func NewComponent(opts ComponentOptions, r *Router) (*Component, error) { - c := Component{ComponentOptions: opts, router: r} +func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*Component, error) { + c := Component{ComponentOptions: opts, router: r, ErrorHandler: errorHandler} return &c, nil } @@ -104,11 +105,8 @@ func (c *Component) Resume(sm SMState) error { case stanza.Handshake: // Start the receiver go routine c.updateState(StateSessionEstablished) - // Leaving this channel here for later. Not used atm. We should return this instead of an error because right - // now the returned error is lost in limbo. - errChan := make(chan error) - go c.recv(errChan) // Sends to errChan - return err // Should be empty at this point + go c.recv() + return err // Should be empty at this point default: c.updateState(StatePermanentError) return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true) @@ -128,13 +126,13 @@ func (c *Component) SetHandler(handler EventHandler) { } // Receiver Go routine receiver -func (c *Component) recv(errChan chan<- error) { +func (c *Component) recv() { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.updateState(StateDisconnected) - errChan <- err + c.ErrorHandler(err) return } // Handle stream errors @@ -142,7 +140,7 @@ func (c *Component) recv(errChan chan<- error) { case stanza.StreamError: c.router.route(c, val) c.streamError(p.Error.Local, p.Text) - errChan <- errors.New("stream error: " + p.Error.Local) + c.ErrorHandler(errors.New("stream error: " + p.Error.Local)) return } c.router.route(c, val) diff --git a/component_test.go b/component_test.go index 4e115f0..48963a5 100644 --- a/component_test.go +++ b/component_test.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "errors" "fmt" + "github.com/google/uuid" "gosrc.io/xmpp/stanza" "net" "strings" @@ -15,19 +16,7 @@ import ( // Tests are ran in parallel, so each test creating a server must use a different port so we do not get any // conflict. Using iota for this should do the trick. const ( - testComponentDomain = "localhost" - defaultServerName = "testServer" - defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545" - defaultComponentName = "Test Component" - - // Default port is not standard XMPP port to avoid interfering - // with local running XMPP server - testHandshakePort = iota + 15222 - testDecoderPort - testSendIqPort - testSendRawPort - testDisconnectPort - testSManDisconnectPort + defaultChannelTimeout = 5 * time.Second ) func TestHandshake(t *testing.T) { @@ -48,16 +37,14 @@ func TestHandshake(t *testing.T) { // Tests connection process with a handshake exchange // Tests multiple session IDs. All connections should generate a unique stream ID -func TestGenerateHandshake(t *testing.T) { +func TestGenerateHandshakeId(t *testing.T) { // Using this array with a channel to make a queue of values to test // These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate // some handshake value - var uuidsArray = [5]string{ - "cc9b3249-9582-4780-825f-4311b42f9b0e", - "bba8be3c-d98e-4e26-b9bb-9ed34578a503", - "dae72822-80e8-496b-b763-ab685f53a188", - "a45d6c06-de49-4bb0-935b-1a2201b71028", - "7dc6924f-0eca-4237-9898-18654b8d891e", + var uuidsArray = [5]string{} + for i := 1; i < len(uuidsArray); i++ { + id, _ := uuid.NewRandom() + uuidsArray[i] = id.String() } // Channel to pass stream IDs as a queue @@ -95,7 +82,7 @@ func TestGenerateHandshake(t *testing.T) { Type: "service", } router := NewRouter() - c, err := NewComponent(opts, router) + c, err := NewComponent(opts, router, componentDefaultErrorHandler) if err != nil { t.Errorf("%+v", err) } @@ -126,7 +113,7 @@ func TestStreamManager(t *testing.T) { // The decoder is expected to be built after a valid connection // Based on the xmpp_component example. func TestDecoder(t *testing.T) { - c, _ := mockConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID) + c, _ := mockComponentConnection(t, testDecoderPort, handlerForComponentHandshakeDefaultID) if c.transport.GetDecoder() == nil { t.Errorf("Failed to initialize decoder. Decoder is nil.") } @@ -134,39 +121,103 @@ func TestDecoder(t *testing.T) { // Tests sending an IQ to the server, and getting the response func TestSendIq(t *testing.T) { + done := make(chan struct{}) + h := func(t *testing.T, c net.Conn) { + handlerForComponentIQSend(t, c) + done <- struct{}{} + } + //Connecting to a mock server, initialized with given port and handler function - c, m := mockConnection(t, testSendIqPort, handlerForComponentIQSend) + c, m := mockComponentConnection(t, testSendIqPort, h) ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) disco := iqReq.DiscoInfo() iqReq.Payload = disco + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + c.ErrorHandler = errorHandler + var res chan stanza.IQ res, _ = c.SendIQ(ctx, iqReq) select { case <-res: - case <-time.After(100 * time.Millisecond): + case err := <-errChan: + t.Errorf(err.Error()) + case <-time.After(defaultChannelTimeout): t.Errorf("Failed to receive response, to sent IQ, from mock server") } - m.Stop() + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } +} + +// Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind. +func TestSendIqFail(t *testing.T) { + done := make(chan struct{}) + h := func(t *testing.T, c net.Conn) { + handlerForComponentIQSend(t, c) + done <- struct{}{} + } + //Connecting to a mock server, initialized with given port and handler function + c, m := mockComponentConnection(t, testSendIqFailPort, h) + + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + + // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified + // so we need to overwrite it. + iqReq.Id = "" + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + c.ErrorHandler = errorHandler + + var res chan stanza.IQ + res, _ = c.SendIQ(ctx, iqReq) + + select { + case r := <-res: // Do we get an IQ response from the server ? + t.Errorf("We should not be getting an IQ response here : this should fail !") + fmt.Println(r) + case <-errChan: // Do we get a stream error from the server ? + // If we get an error from the server, the test passes. + case <-time.After(defaultChannelTimeout): // Timeout ? + t.Errorf("Failed to receive response, to sent IQ, from mock server") + } + + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } } // Tests sending raw xml to the mock server. -// TODO : check the server response client side ? // Right now, the server response is not checked and an err is passed in a channel if the test is supposed to err. // In this test, we use IQs func TestSendRaw(t *testing.T) { - // Error channel for the handler - errChan := make(chan error) + done := make(chan struct{}) // Handler for the mock server h := func(t *testing.T, c net.Conn) { // Completes the connection by exchanging handshakes handlerForComponentHandshakeDefaultID(t, c) - receiveRawIq(t, c, errChan) - return + receiveIq(c, xml.NewDecoder(c)) + done <- struct{}{} } type testCase struct { @@ -185,12 +236,19 @@ func TestSendRaw(t *testing.T) { shouldErr: true, } + // A handler for the component. + // In the failing test, the server returns a stream error, which triggers this handler, component side. + errChan := make(chan error) + errHandler := func(err error) { + errChan <- err + } + // Tests for all the IQs for name, tcase := range testRequests { t.Run(name, func(st *testing.T) { //Connecting to a mock server, initialized with given port and handler function - c, m := mockConnection(t, testSendRawPort, h) - + c, m := mockComponentConnection(t, testSendRawPort, h) + c.ErrorHandler = errHandler // Sending raw xml from test case err := c.SendRaw(tcase.req) if err != nil { @@ -198,21 +256,29 @@ func TestSendRaw(t *testing.T) { } // Just wait a little so the message has time to arrive select { - case <-time.After(100 * time.Millisecond): + // We don't use the default "long" timeout here because waiting it out means passing the test. + case <-time.After(200 * time.Millisecond): case err = <-errChan: if err == nil && tcase.shouldErr { t.Errorf("Failed to get closing stream err") + } else if err != nil && !tcase.shouldErr { + t.Errorf("This test is not supposed to err ! => %s", err.Error()) } } c.transport.Close() - m.Stop() + select { + case <-done: + m.Stop() + case <-time.After(defaultChannelTimeout): + t.Errorf("The mock server failed to finish its job !") + } }) } } // Tests the Disconnect method for Components func TestDisconnect(t *testing.T) { - c, m := mockConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID) + c, m := mockComponentConnection(t, testDisconnectPort, handlerForComponentHandshakeDefaultID) err := c.transport.Ping() if err != nil { t.Errorf("Could not ping but not disconnected yet") @@ -257,14 +323,97 @@ func TestStreamManagerDisconnect(t *testing.T) { //============================================================================= // Basic XMPP Server Mock Handlers. -// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. -// Used in the mock server as a Handler -func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkOpenStreamHandshakeDefaultID(t, c, decoder) - readHandshakeComponent(t, decoder) - fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) - return + +//=============================== +// Init mock server and connection +// Creating a mock server and connecting a Component to it. Initialized with given port and handler function +// The Component and mock are both returned +func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) { + // Init mock server + testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port) + mock := ServerMock{} + mock.Start(t, testComponentAddress, handler) + + //================================== + // Create Component to connect to it + c := makeBasicComponent(defaultComponentName, testComponentAddress, t) + + //======================================== + // Connect the new Component to the server + err := c.Connect() + if err != nil { + t.Errorf("%+v", err) + } + + return c, &mock +} + +func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component { + opts := ComponentOptions{ + TransportConfiguration: TransportConfiguration{ + Address: mockServerAddr, + Domain: "localhost", + }, + Domain: testComponentDomain, + Secret: "mypass", + Name: name, + Category: "gateway", + Type: "service", + } + router := NewRouter() + c, err := NewComponent(opts, router, componentDefaultErrorHandler) + if err != nil { + t.Errorf("%+v", err) + } + c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) + if err != nil { + t.Errorf("%+v", err) + } + return c +} + +// This really should not be used as is. +// It's just meant to be a placeholder when error handling is not needed at this level +func componentDefaultErrorHandler(err error) { + +} + +// Sends IQ response to Component request. +// No parsing of the request here. We just check that it's valid, and send the default response. +func handlerForComponentIQSend(t *testing.T, c net.Conn) { + // Completes the connection by exchanging handshakes + handlerForComponentHandshakeDefaultID(t, c) + respondToIQ(t, c) +} + +// Used for ID and handshake related tests +func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + + for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. + token, err := decoder.Token() + if err != nil { + t.Errorf("cannot read next token: %s", err) + } + + switch elem := token.(type) { + // Wait for first startElement + case xml.StartElement: + 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", streamID, stanza.NSComponent, stanza.NSStream); err != nil { + t.Errorf("cannot write server stream open: %s", err) + } + return + } + } +} + +func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) { + checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID) } // Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. @@ -303,152 +452,12 @@ func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) { } } -func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) { - checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID) -} - -// Used for ID and handshake related tests -func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) - - for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. - token, err := decoder.Token() - if err != nil { - t.Errorf("cannot read next token: %s", err) - } - - switch elem := token.(type) { - // Wait for first startElement - case xml.StartElement: - 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", streamID, stanza.NSComponent, stanza.NSStream); err != nil { - t.Errorf("cannot write server stream open: %s", err) - } - return - } - } -} - -//============================================================================= -// Sends IQ response to Component request. -// No parsing of the request here. We just check that it's valid, and send the default response. -func handlerForComponentIQSend(t *testing.T, c net.Conn) { - // Completes the connection by exchanging handshakes - handlerForComponentHandshakeDefaultID(t, c) - - // Decoder to parse the request +// Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. +// Used in the mock server as a Handler +func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) - - iqReq, err := receiveIq(t, c, decoder) - if err != nil { - t.Errorf("Error receiving the IQ stanza : %v", err) - } else if !iqReq.IsValid() { - t.Errorf("server received an IQ stanza : %v", iqReq) - } - - // Crafting response - iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) - disco := iqResp.DiscoInfo() - disco.AddFeatures("vcard-temp", - `http://jabber.org/protocol/address`) - - disco.AddIdentity("Multicast", "service", "multicast") - iqResp.Payload = disco - - // Sending response to the Component - mResp, err := xml.Marshal(iqResp) - _, err = fmt.Fprintln(c, string(mResp)) - if err != nil { - t.Errorf("Could not send response stanza : %s", err) - } + checkOpenStreamHandshakeDefaultID(t, c, decoder) + readHandshakeComponent(t, decoder) + fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) return } - -// Reads next request coming from the Component. Expecting it to be an IQ request -func receiveIq(t *testing.T, c net.Conn, decoder *xml.Decoder) (stanza.IQ, error) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) - var iqStz stanza.IQ - err := decoder.Decode(&iqStz) - if err != nil { - t.Errorf("cannot read the received IQ stanza: %s", err) - } - if !iqStz.IsValid() { - t.Errorf("received IQ stanza is invalid : %s", err) - } - return iqStz, nil -} - -func receiveRawIq(t *testing.T, c net.Conn, errChan chan error) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) - decoder := xml.NewDecoder(c) - var iq stanza.IQ - err := decoder.Decode(&iq) - if err != nil || !iq.IsValid() { - s := stanza.StreamError{ - XMLName: xml.Name{Local: "stream:error"}, - Error: xml.Name{Local: "xml-not-well-formed"}, - Text: `XML was not well-formed`, - } - raw, _ := xml.Marshal(s) - fmt.Fprintln(c, string(raw)) - fmt.Fprintln(c, ``) // TODO : check this client side - errChan <- fmt.Errorf("invalid xml") - return - } - errChan <- nil - return -} - -//=============================== -// Init mock server and connection -// Creating a mock server and connecting a Component to it. Initialized with given port and handler function -// The Component and mock are both returned -func mockConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) { - // Init mock server - testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port) - mock := ServerMock{} - mock.Start(t, testComponentAddress, handler) - - //================================== - // Create Component to connect to it - c := makeBasicComponent(defaultComponentName, testComponentAddress, t) - - //======================================== - // Connect the new Component to the server - err := c.Connect() - if err != nil { - t.Errorf("%+v", err) - } - - return c, &mock -} - -func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component { - opts := ComponentOptions{ - TransportConfiguration: TransportConfiguration{ - Address: mockServerAddr, - Domain: "localhost", - }, - Domain: testComponentDomain, - Secret: "mypass", - Name: name, - Category: "gateway", - Type: "service", - } - router := NewRouter() - c, err := NewComponent(opts, router) - if err != nil { - t.Errorf("%+v", err) - } - c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) - if err != nil { - t.Errorf("%+v", err) - } - return c -} diff --git a/tcp_server_mock.go b/tcp_server_mock.go index bdc4397..4afed80 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -1,12 +1,42 @@ package xmpp import ( + "encoding/xml" + "fmt" + "gosrc.io/xmpp/stanza" "net" "testing" + "time" ) //============================================================================= // TCP Server Mock +const ( + defaultTimeout = 2 * time.Second + testComponentDomain = "localhost" + defaultServerName = "testServer" + defaultStreamID = "91bd0bba-012f-4d92-bb17-5fc41e6fe545" + defaultComponentName = "Test Component" + serverStreamOpen = "" + + // Default port is not standard XMPP port to avoid interfering + // with local running XMPP server + + // Component tests + testHandshakePort = iota + 15222 + testDecoderPort + testSendIqPort + testSendIqFailPort + testSendRawPort + testDisconnectPort + testSManDisconnectPort + + // Client tests + testClientBasePort + testClientRawPort + testClientIqPort + testClientIqFailPort +) // ClientHandler is passed by the test client to provide custom behaviour to // the TCP server mock. This allows customizing the server behaviour to allow @@ -81,3 +111,180 @@ func (mock *ServerMock) loop() { go mock.handler(mock.t, conn) } } + +//====================================================================================================================== +// A few functions commonly used for tests. Trying to avoid duplicates in client and component test files. +//====================================================================================================================== + +func respondToIQ(t *testing.T, c net.Conn) { + // Decoder to parse the request + decoder := xml.NewDecoder(c) + + iqReq, err := receiveIq(c, decoder) + if err != nil { + t.Fatalf("failed to receive IQ : %s", err.Error()) + } + + if !iqReq.IsValid() { + mockIQError(c) + return + } + + // Crafting response + iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) + disco := iqResp.DiscoInfo() + disco.AddFeatures("vcard-temp", + `http://jabber.org/protocol/address`) + + disco.AddIdentity("Multicast", "service", "multicast") + iqResp.Payload = disco + + // Sending response to the Component + mResp, err := xml.Marshal(iqResp) + _, err = fmt.Fprintln(c, string(mResp)) + if err != nil { + t.Errorf("Could not send response stanza : %s", err) + } + return +} + +// When a presence stanza is automatically sent (right now it's the case in the client), we may want to discard it +// and test further stanzas. +func discardPresence(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + var presenceStz stanza.Presence + err := decoder.Decode(&presenceStz) + if err != nil { + t.Errorf("Expected presence but this happened : %s", err.Error()) + } +} + +// Reads next request coming from the Component. Expecting it to be an IQ request +func receiveIq(c net.Conn, decoder *xml.Decoder) (*stanza.IQ, error) { + c.SetDeadline(time.Now().Add(defaultTimeout)) + defer c.SetDeadline(time.Time{}) + var iqStz stanza.IQ + err := decoder.Decode(&iqStz) + if err != nil { + return nil, err + } + return &iqStz, nil +} + +// Should be used in server handlers when an IQ sent by a client or component is invalid. +// This responds as expected from a "real" server, aside from the error message. +func mockIQError(c net.Conn) { + s := stanza.StreamError{ + XMLName: xml.Name{Local: "stream:error"}, + Error: xml.Name{Local: "xml-not-well-formed"}, + Text: `XML was not well-formed`, + } + raw, _ := xml.Marshal(s) + fmt.Fprintln(c, string(raw)) + fmt.Fprintln(c, ``) +} + +func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) { + // This is a basic server, supporting only 1 stream feature: SASL Plain Auth + features := ` + + PLAIN + +` + if _, err := fmt.Fprintln(c, features); err != nil { + t.Errorf("cannot send stream feature: %s", err) + } +} + +// TODO return err in case of error reading the auth params +func readAuth(t *testing.T, decoder *xml.Decoder) string { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read auth: %s", err) + return "" + } + + var nv interface{} + nv = &stanza.SASLAuth{} + // Decode element into pointer storage + if err = decoder.DecodeElement(nv, &se); err != nil { + t.Errorf("cannot decode auth: %s", err) + return "" + } + + switch v := nv.(type) { + case *stanza.SASLAuth: + return v.Value + } + return "" +} + +func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { + // This is a basic server, supporting only 1 stream feature after auth: resource binding + features := ` + +` + if _, err := fmt.Fprintln(c, features); err != nil { + t.Errorf("cannot send stream feature: %s", err) + } +} + +func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) { + // This is a basic server, supporting only 2 features after auth: resource & session binding + features := ` + + +` + if _, err := fmt.Fprintln(c, features); err != nil { + t.Errorf("cannot send stream feature: %s", err) + } +} + +func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read bind: %s", err) + return + } + + iq := &stanza.IQ{} + // Decode element into pointer storage + if err = decoder.DecodeElement(&iq, &se); err != nil { + t.Errorf("cannot decode bind iq: %s", err) + return + } + + // TODO Check all elements + switch iq.Payload.(type) { + case *stanza.Bind: + result := ` + + %s + +` + fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID + } +} + +func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { + se, err := stanza.NextStart(decoder) + if err != nil { + t.Errorf("cannot read session: %s", err) + return + } + + iq := &stanza.IQ{} + // Decode element into pointer storage + if err = decoder.DecodeElement(&iq, &se); err != nil { + t.Errorf("cannot decode session iq: %s", err) + return + } + + switch iq.Payload.(type) { + case *stanza.StreamSession: + result := `` + fmt.Fprintf(c, result, iq.Id) + } +} From 6d8e9d325a7862f5c3b9d51206374621aefa9417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Mon, 9 Dec 2019 13:31:01 +0100 Subject: [PATCH 16/54] Try removing decoder from IQ tests and changing writing method --- client.go | 3 +-- component.go | 12 +++++++++--- tcp_server_mock.go | 18 +++++++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index a5ad1bf..4d7857e 100644 --- a/client.go +++ b/client.go @@ -4,7 +4,6 @@ import ( "context" "encoding/xml" "errors" - "fmt" "io" "net" "time" @@ -200,7 +199,7 @@ func (c *Client) Resume(state SMState) error { //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") // TODO: Do we always want to send initial presence automatically ? // Do we need an option to avoid that or do we rely on client to send the presence itself ? - _, err = fmt.Fprintf(c.transport, "") + err = c.sendWithWriter(c.transport, []byte("")) return err } diff --git a/component.go b/component.go index 2f61aef..8b96240 100644 --- a/component.go +++ b/component.go @@ -85,7 +85,7 @@ func (c *Component) Resume(sm SMState) error { c.updateState(StateConnected) // Authentication - if _, err := fmt.Fprintf(c.transport, "%s", c.handshake(streamId)); err != nil { + if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("%s", c.handshake(streamId)))); err != nil { c.updateState(StateStreamError) return NewConnError(errors.New("cannot send handshake "+err.Error()), false) @@ -159,12 +159,18 @@ func (c *Component) Send(packet stanza.Packet) error { return errors.New("cannot marshal packet " + err.Error()) } - if _, err := fmt.Fprintf(transport, string(data)); err != nil { + if err := c.sendWithWriter(transport, data); err != nil { return errors.New("cannot send packet " + err.Error()) } return nil } +func (c *Component) sendWithWriter(writer io.Writer, packet []byte) error { + var err error + _, err = writer.Write(packet) + return err +} + // SendIQ sends an IQ set or get stanza to the server. If a result is received // the provided handler function will automatically be called. // @@ -195,7 +201,7 @@ func (c *Component) SendRaw(packet string) error { } var err error - _, err = fmt.Fprintf(transport, packet) + err = c.sendWithWriter(transport, []byte(packet)) return err } diff --git a/tcp_server_mock.go b/tcp_server_mock.go index 4afed80..efdda23 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -117,21 +117,25 @@ func (mock *ServerMock) loop() { //====================================================================================================================== func respondToIQ(t *testing.T, c net.Conn) { - // Decoder to parse the request - decoder := xml.NewDecoder(c) - - iqReq, err := receiveIq(c, decoder) + recvBuf := make([]byte, 1024) + var iqR stanza.IQ + _, err := c.Read(recvBuf[:]) // recv data if err != nil { - t.Fatalf("failed to receive IQ : %s", err.Error()) + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + t.Errorf("read timeout: %s", err) + } else { + t.Errorf("read error: %s", err) + } } + xml.Unmarshal(recvBuf, &iqR) - if !iqReq.IsValid() { + if !iqR.IsValid() { mockIQError(c) return } // Crafting response - iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) + iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqR.To, To: iqR.From, Id: iqR.Id, Lang: "en"}) disco := iqResp.DiscoInfo() disco.AddFeatures("vcard-temp", `http://jabber.org/protocol/address`) From fd48f52f3db309764f32bb63a2b0c3848006ff86 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 10 Dec 2019 14:30:15 +0100 Subject: [PATCH 17/54] Using precisely sized buffers for tcp tests --- client.go | 3 ++- client_test.go | 6 +++--- tcp_server_mock.go | 16 ++++++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/client.go b/client.go index 4d7857e..254a793 100644 --- a/client.go +++ b/client.go @@ -20,6 +20,7 @@ type ConnState = uint8 // This is a the list of events happening on the connection that the // client can be notified about. const ( + InitialPresence = "" StateDisconnected ConnState = iota StateConnected StateSessionEstablished @@ -199,7 +200,7 @@ func (c *Client) Resume(state SMState) error { //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") // TODO: Do we always want to send initial presence automatically ? // Do we need an option to avoid that or do we rely on client to send the presence itself ? - err = c.sendWithWriter(c.transport, []byte("")) + err = c.sendWithWriter(c.transport, []byte(InitialPresence)) return err } diff --git a/client_test.go b/client_test.go index 0caace0..f2b775a 100644 --- a/client_test.go +++ b/client_test.go @@ -184,15 +184,15 @@ func TestClient_SendIQ(t *testing.T) { select { case <-res: // If the server responds with an IQ, we pass the test case err := <-errChan: // If the server sends an error, or there is a connection error - t.Errorf(err.Error()) + t.Fatal(err.Error()) case <-time.After(defaultChannelTimeout): // If we timeout - t.Errorf("Failed to receive response, to sent IQ, from mock server") + t.Fatal("Failed to receive response, to sent IQ, from mock server") } select { case <-done: mock.Stop() case <-time.After(defaultChannelTimeout): - t.Errorf("The mock server failed to finish its job !") + t.Fatal("The mock server failed to finish its job !") } } diff --git a/tcp_server_mock.go b/tcp_server_mock.go index efdda23..1084cbd 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -120,6 +120,7 @@ func respondToIQ(t *testing.T, c net.Conn) { recvBuf := make([]byte, 1024) var iqR stanza.IQ _, err := c.Read(recvBuf[:]) // recv data + if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { t.Errorf("read timeout: %s", err) @@ -155,11 +156,22 @@ func respondToIQ(t *testing.T, c net.Conn) { // When a presence stanza is automatically sent (right now it's the case in the client), we may want to discard it // and test further stanzas. func discardPresence(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) c.SetDeadline(time.Now().Add(defaultTimeout)) defer c.SetDeadline(time.Time{}) var presenceStz stanza.Presence - err := decoder.Decode(&presenceStz) + + recvBuf := make([]byte, len(InitialPresence)) + _, err := c.Read(recvBuf[:]) // recv data + + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + t.Errorf("read timeout: %s", err) + } else { + t.Errorf("read error: %s", err) + } + } + xml.Unmarshal(recvBuf, &presenceStz) + if err != nil { t.Errorf("Expected presence but this happened : %s", err.Error()) } From 3c9b0db5b80ea26f031ee38c8ab56783faf2ad72 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 10 Dec 2019 17:15:16 +0100 Subject: [PATCH 18/54] Fixed decoder usage. Decoders have internal buffering, and creating many on a single TCP connection can cause issues in parsing exchanged XML documents. --- client_test.go | 83 ++++++++++++++++++--------------------- component_test.go | 78 ++++++++++++++++++------------------ doc.go | 2 +- session.go | 2 +- tcp_server_mock.go | 98 +++++++++++++++++++++++----------------------- 5 files changed, 130 insertions(+), 133 deletions(-) diff --git a/client_test.go b/client_test.go index f2b775a..8d109d0 100644 --- a/client_test.go +++ b/client_test.go @@ -5,7 +5,6 @@ import ( "encoding/xml" "errors" "fmt" - "net" "testing" "time" @@ -157,10 +156,10 @@ func TestClient_RFC3921Session(t *testing.T) { func TestClient_SendIQ(t *testing.T) { done := make(chan struct{}) // Handler for Mock server - h := func(t *testing.T, c net.Conn) { - handlerClientConnectSuccess(t, c) - discardPresence(t, c) - respondToIQ(t, c) + h := func(t *testing.T, sc *ServerConn) { + handlerClientConnectSuccess(t, sc) + discardPresence(t, sc) + respondToIQ(t, sc) done <- struct{}{} } client, mock := mockClientConnection(t, h, testClientIqPort) @@ -199,10 +198,10 @@ func TestClient_SendIQ(t *testing.T) { func TestClient_SendIQFail(t *testing.T) { done := make(chan struct{}) // Handler for Mock server - h := func(t *testing.T, c net.Conn) { - handlerClientConnectSuccess(t, c) - discardPresence(t, c) - respondToIQ(t, c) + h := func(t *testing.T, sc *ServerConn) { + handlerClientConnectSuccess(t, sc) + discardPresence(t, sc) + respondToIQ(t, sc) done <- struct{}{} } client, mock := mockClientConnection(t, h, testClientIqFailPort) @@ -244,10 +243,10 @@ func TestClient_SendIQFail(t *testing.T) { func TestClient_SendRaw(t *testing.T) { done := make(chan struct{}) // Handler for Mock server - h := func(t *testing.T, c net.Conn) { - handlerClientConnectSuccess(t, c) - discardPresence(t, c) - respondToIQ(t, c) + h := func(t *testing.T, sc *ServerConn) { + handlerClientConnectSuccess(t, sc) + discardPresence(t, sc) + respondToIQ(t, sc) done <- struct{}{} } type testCase struct { @@ -365,48 +364,44 @@ func TestClient_DisconnectStreamManager(t *testing.T) { // Basic XMPP Server Mock Handlers. // Test connection with a basic straightforward workflow -func handlerClientConnectSuccess(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkClientOpenStream(t, c, decoder) +func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + fmt.Fprintln(sc.connection, "") - sendStreamFeatures(t, c, decoder) // Send initial features - readAuth(t, decoder) - fmt.Fprintln(c, "") - - checkClientOpenStream(t, c, decoder) // Reset stream - sendBindFeature(t, c, decoder) // Send post auth features - bind(t, c, decoder) + checkClientOpenStream(t, sc) // Reset stream + sendBindFeature(t, sc) // Send post auth features + bind(t, sc) } // We expect client will abort on TLS -func handlerAbortTLS(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkClientOpenStream(t, c, decoder) - sendStreamFeatures(t, c, decoder) // Send initial features +func handlerAbortTLS(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + sendStreamFeatures(t, sc) // Send initial features } // Test connection with mandatory session (RFC-3921) -func handlerClientConnectWithSession(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkClientOpenStream(t, c, decoder) +func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) - sendStreamFeatures(t, c, decoder) // Send initial features - readAuth(t, decoder) - fmt.Fprintln(c, "") + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + fmt.Fprintln(sc.connection, "") - checkClientOpenStream(t, c, decoder) // Reset stream - sendRFC3921Feature(t, c, decoder) // Send post auth features - bind(t, c, decoder) - session(t, c, decoder) + checkClientOpenStream(t, sc) // Reset stream + sendRFC3921Feature(t, sc) // Send post auth features + bind(t, sc) + session(t, sc) } -func checkClientOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) +func checkClientOpenStream(t *testing.T, sc *ServerConn) { + sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + defer sc.connection.SetDeadline(time.Time{}) for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. var token xml.Token - token, err := decoder.Token() + token, err := sc.decoder.Token() if err != nil { t.Errorf("cannot read next token: %s", err) } @@ -418,7 +413,7 @@ func checkClientOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space) return } - if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil { + if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", "streamid1", stanza.NSClient, stanza.NSStream); err != nil { t.Errorf("cannot write server stream open: %s", err) } return @@ -426,8 +421,8 @@ func checkClientOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { } } -func mockClientConnection(t *testing.T, serverHandler func(*testing.T, net.Conn), port int) (*Client, ServerMock) { - mock := ServerMock{} +func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int) (*Client, *ServerMock) { + mock := &ServerMock{} testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) mock.Start(t, testServerAddress, serverHandler) diff --git a/component_test.go b/component_test.go index 48963a5..f4d1a07 100644 --- a/component_test.go +++ b/component_test.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/google/uuid" "gosrc.io/xmpp/stanza" - "net" "strings" "testing" "time" @@ -36,7 +35,7 @@ func TestHandshake(t *testing.T) { } // Tests connection process with a handshake exchange -// Tests multiple session IDs. All connections should generate a unique stream ID +// Tests multiple session IDs. All serverConnections should generate a unique stream ID func TestGenerateHandshakeId(t *testing.T) { // Using this array with a channel to make a queue of values to test // These are stream IDs that will be used to test the connection process, mixing them with the "secret" to generate @@ -56,11 +55,11 @@ func TestGenerateHandshakeId(t *testing.T) { // Performs a Component connection with a handshake. It expects to have an ID sent its way through the "uchan" // channel of this file. Otherwise it will hang for ever. - h := func(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkOpenStreamHandshakeID(t, c, decoder, <-uchan) - readHandshakeComponent(t, decoder) - fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) + h := func(t *testing.T, sc *ServerConn) { + + checkOpenStreamHandshakeID(t, sc, <-uchan) + readHandshakeComponent(t, sc.decoder) + fmt.Fprintln(sc.connection, "") // That's all the server needs to return (see xep-0114) return } @@ -122,8 +121,8 @@ func TestDecoder(t *testing.T) { // Tests sending an IQ to the server, and getting the response func TestSendIq(t *testing.T) { done := make(chan struct{}) - h := func(t *testing.T, c net.Conn) { - handlerForComponentIQSend(t, c) + h := func(t *testing.T, sc *ServerConn) { + handlerForComponentIQSend(t, sc) done <- struct{}{} } @@ -164,8 +163,8 @@ func TestSendIq(t *testing.T) { // Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind. func TestSendIqFail(t *testing.T) { done := make(chan struct{}) - h := func(t *testing.T, c net.Conn) { - handlerForComponentIQSend(t, c) + h := func(t *testing.T, sc *ServerConn) { + handlerForComponentIQSend(t, sc) done <- struct{}{} } //Connecting to a mock server, initialized with given port and handler function @@ -213,27 +212,30 @@ func TestSendIqFail(t *testing.T) { func TestSendRaw(t *testing.T) { done := make(chan struct{}) // Handler for the mock server - h := func(t *testing.T, c net.Conn) { + h := func(t *testing.T, sc *ServerConn) { // Completes the connection by exchanging handshakes - handlerForComponentHandshakeDefaultID(t, c) - receiveIq(c, xml.NewDecoder(c)) + handlerForComponentHandshakeDefaultID(t, sc) + respondToIQ(t, sc) done <- struct{}{} } type testCase struct { req string shouldErr bool + port int } testRequests := make(map[string]testCase) // Sending a correct IQ of type get. Not supposed to err testRequests["Correct IQ"] = testCase{ req: ``, shouldErr: false, + port: testSendRawPort + 100, } // Sending an IQ with a missing ID. Should err testRequests["IQ with missing ID"] = testCase{ req: ``, shouldErr: true, + port: testSendRawPort + 200, } // A handler for the component. @@ -247,7 +249,7 @@ func TestSendRaw(t *testing.T) { for name, tcase := range testRequests { t.Run(name, func(st *testing.T) { //Connecting to a mock server, initialized with given port and handler function - c, m := mockComponentConnection(t, testSendRawPort, h) + c, m := mockComponentConnection(t, tcase.port, h) c.ErrorHandler = errHandler // Sending raw xml from test case err := c.SendRaw(tcase.req) @@ -328,10 +330,10 @@ func TestStreamManagerDisconnect(t *testing.T) { // Init mock server and connection // Creating a mock server and connecting a Component to it. Initialized with given port and handler function // The Component and mock are both returned -func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, c net.Conn)) (*Component, *ServerMock) { +func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, sc *ServerConn)) (*Component, *ServerMock) { // Init mock server testComponentAddress := fmt.Sprintf("%s:%d", testComponentDomain, port) - mock := ServerMock{} + mock := &ServerMock{} mock.Start(t, testComponentAddress, handler) //================================== @@ -345,7 +347,9 @@ func mockComponentConnection(t *testing.T, port int, handler func(t *testing.T, t.Errorf("%+v", err) } - return c, &mock + // Now that the Component is connected, let's set the xml.Decoder for the server + + return c, mock } func makeBasicComponent(name string, mockServerAddr string, t *testing.T) *Component { @@ -380,19 +384,19 @@ func componentDefaultErrorHandler(err error) { // Sends IQ response to Component request. // No parsing of the request here. We just check that it's valid, and send the default response. -func handlerForComponentIQSend(t *testing.T, c net.Conn) { +func handlerForComponentIQSend(t *testing.T, sc *ServerConn) { // Completes the connection by exchanging handshakes - handlerForComponentHandshakeDefaultID(t, c) - respondToIQ(t, c) + handlerForComponentHandshakeDefaultID(t, sc) + respondToIQ(t, sc) } // Used for ID and handshake related tests -func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, streamID string) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) +func checkOpenStreamHandshakeID(t *testing.T, sc *ServerConn, streamID string) { + sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + defer sc.connection.SetDeadline(time.Time{}) for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. - token, err := decoder.Token() + token, err := sc.decoder.Token() if err != nil { t.Errorf("cannot read next token: %s", err) } @@ -404,7 +408,7 @@ func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space) return } - if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", streamID, stanza.NSComponent, stanza.NSStream); err != nil { + if _, err := fmt.Fprintf(sc.connection, serverStreamOpen, "localhost", streamID, stanza.NSComponent, stanza.NSStream); err != nil { t.Errorf("cannot write server stream open: %s", err) } return @@ -412,16 +416,15 @@ func checkOpenStreamHandshakeID(t *testing.T, c net.Conn, decoder *xml.Decoder, } } -func checkOpenStreamHandshakeDefaultID(t *testing.T, c net.Conn, decoder *xml.Decoder) { - checkOpenStreamHandshakeID(t, c, decoder, defaultStreamID) +func checkOpenStreamHandshakeDefaultID(t *testing.T, sc *ServerConn) { + checkOpenStreamHandshakeID(t, sc, defaultStreamID) } // Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. // This handler is supposed to fail by sending a "message" stanza instead of a stanza to finalize the handshake. -func handlerComponentFailedHandshakeDefaultID(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkOpenStreamHandshakeDefaultID(t, c, decoder) - readHandshakeComponent(t, decoder) +func handlerComponentFailedHandshakeDefaultID(t *testing.T, sc *ServerConn) { + checkOpenStreamHandshakeDefaultID(t, sc) + readHandshakeComponent(t, sc.decoder) // Send a message, instead of a "" tag, to fail the handshake process dans disconnect the client. me := stanza.Message{ @@ -429,7 +432,7 @@ func handlerComponentFailedHandshakeDefaultID(t *testing.T, c net.Conn) { Body: "Fail my handshake.", } s, _ := xml.Marshal(me) - fmt.Fprintln(c, string(s)) + fmt.Fprintln(sc.connection, string(s)) return } @@ -454,10 +457,9 @@ func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) { // Performs a Component connection with a handshake. It uses a default ID defined in this file as a constant. // Used in the mock server as a Handler -func handlerForComponentHandshakeDefaultID(t *testing.T, c net.Conn) { - decoder := xml.NewDecoder(c) - checkOpenStreamHandshakeDefaultID(t, c, decoder) - readHandshakeComponent(t, decoder) - fmt.Fprintln(c, "") // That's all the server needs to return (see xep-0114) +func handlerForComponentHandshakeDefaultID(t *testing.T, sc *ServerConn) { + checkOpenStreamHandshakeDefaultID(t, sc) + readHandshakeComponent(t, sc.decoder) + fmt.Fprintln(sc.connection, "") // That's all the server needs to return (see xep-0114) return } diff --git a/doc.go b/doc.go index 40f4f6a..f29bbf6 100644 --- a/doc.go +++ b/doc.go @@ -29,7 +29,7 @@ Components XMPP components can typically be used to extends the features of an XMPP server, in a portable way, using component protocol over persistent TCP -connections. +serverConnections. Component protocol is defined in XEP-114 (https://xmpp.org/extensions/xep-0114.html). diff --git a/session.go b/session.go index 22d76b2..6b9c75a 100644 --- a/session.go +++ b/session.go @@ -119,7 +119,7 @@ func (s *Session) startTlsIfSupported(o Config) { return } - // If we do not allow cleartext connections, make it explicit that server do not support starttls + // If we do not allow cleartext serverConnections, make it explicit that server do not support starttls if !o.Insecure { s.err = errors.New("XMPP server does not advertise support for starttls") } diff --git a/tcp_server_mock.go b/tcp_server_mock.go index 1084cbd..c8f5d97 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -41,16 +41,21 @@ const ( // ClientHandler is passed by the test client to provide custom behaviour to // the TCP server mock. This allows customizing the server behaviour to allow // testing clients under various scenarii. -type ClientHandler func(t *testing.T, conn net.Conn) +type ClientHandler func(t *testing.T, serverConn *ServerConn) // ServerMock is a simple TCP server that can be use to mock basic server // behaviour to test clients. type ServerMock struct { - t *testing.T - handler ClientHandler - listener net.Listener - connections []net.Conn - done chan struct{} + t *testing.T + handler ClientHandler + listener net.Listener + serverConnections []*ServerConn + done chan struct{} +} + +type ServerConn struct { + connection net.Conn + decoder *xml.Decoder } // Start launches the mock TCP server, listening to an actual address / port. @@ -68,9 +73,9 @@ func (mock *ServerMock) Stop() { if mock.listener != nil { mock.listener.Close() } - // Close all existing connections - for _, c := range mock.connections { - c.Close() + // Close all existing serverConnections + for _, c := range mock.serverConnections { + c.connection.Close() } } @@ -90,13 +95,14 @@ func (mock *ServerMock) init(addr string) error { return nil } -// loop accepts connections and creates a go routine per connection. +// loop accepts serverConnections and creates a go routine per connection. // The go routine is running the client handler, that is used to provide the // real TCP server behaviour. func (mock *ServerMock) loop() { listener := mock.listener for { conn, err := listener.Accept() + serverConn := &ServerConn{conn, xml.NewDecoder(conn)} if err != nil { select { case <-mock.done: @@ -106,9 +112,10 @@ func (mock *ServerMock) loop() { } return } - mock.connections = append(mock.connections, conn) + mock.serverConnections = append(mock.serverConnections, serverConn) + // TODO Create and pass a context to cancel the handler if they are still around = avoid possible leak on complex handlers - go mock.handler(mock.t, conn) + go mock.handler(mock.t, serverConn) } } @@ -116,27 +123,20 @@ func (mock *ServerMock) loop() { // A few functions commonly used for tests. Trying to avoid duplicates in client and component test files. //====================================================================================================================== -func respondToIQ(t *testing.T, c net.Conn) { - recvBuf := make([]byte, 1024) - var iqR stanza.IQ - _, err := c.Read(recvBuf[:]) // recv data - +func respondToIQ(t *testing.T, sc *ServerConn) { + // Decoder to parse the request + iqReq, err := receiveIq(sc) if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - t.Errorf("read timeout: %s", err) - } else { - t.Errorf("read error: %s", err) - } + t.Fatalf("failed to receive IQ : %s", err.Error()) } - xml.Unmarshal(recvBuf, &iqR) - if !iqR.IsValid() { - mockIQError(c) + if !iqReq.IsValid() { + mockIQError(sc.connection) return } // Crafting response - iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqR.To, To: iqR.From, Id: iqR.Id, Lang: "en"}) + iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) disco := iqResp.DiscoInfo() disco.AddFeatures("vcard-temp", `http://jabber.org/protocol/address`) @@ -146,7 +146,7 @@ func respondToIQ(t *testing.T, c net.Conn) { // Sending response to the Component mResp, err := xml.Marshal(iqResp) - _, err = fmt.Fprintln(c, string(mResp)) + _, err = fmt.Fprintln(sc.connection, string(mResp)) if err != nil { t.Errorf("Could not send response stanza : %s", err) } @@ -155,13 +155,13 @@ func respondToIQ(t *testing.T, c net.Conn) { // When a presence stanza is automatically sent (right now it's the case in the client), we may want to discard it // and test further stanzas. -func discardPresence(t *testing.T, c net.Conn) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) +func discardPresence(t *testing.T, sc *ServerConn) { + sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + defer sc.connection.SetDeadline(time.Time{}) var presenceStz stanza.Presence recvBuf := make([]byte, len(InitialPresence)) - _, err := c.Read(recvBuf[:]) // recv data + _, err := sc.connection.Read(recvBuf[:]) // recv data if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { @@ -178,11 +178,11 @@ func discardPresence(t *testing.T, c net.Conn) { } // Reads next request coming from the Component. Expecting it to be an IQ request -func receiveIq(c net.Conn, decoder *xml.Decoder) (*stanza.IQ, error) { - c.SetDeadline(time.Now().Add(defaultTimeout)) - defer c.SetDeadline(time.Time{}) +func receiveIq(sc *ServerConn) (*stanza.IQ, error) { + sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + defer sc.connection.SetDeadline(time.Time{}) var iqStz stanza.IQ - err := decoder.Decode(&iqStz) + err := sc.decoder.Decode(&iqStz) if err != nil { return nil, err } @@ -202,14 +202,14 @@ func mockIQError(c net.Conn) { fmt.Fprintln(c, ``) } -func sendStreamFeatures(t *testing.T, c net.Conn, _ *xml.Decoder) { +func sendStreamFeatures(t *testing.T, sc *ServerConn) { // This is a basic server, supporting only 1 stream feature: SASL Plain Auth features := ` PLAIN ` - if _, err := fmt.Fprintln(c, features); err != nil { + if _, err := fmt.Fprintln(sc.connection, features); err != nil { t.Errorf("cannot send stream feature: %s", err) } } @@ -237,29 +237,29 @@ func readAuth(t *testing.T, decoder *xml.Decoder) string { return "" } -func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { +func sendBindFeature(t *testing.T, sc *ServerConn) { // This is a basic server, supporting only 1 stream feature after auth: resource binding features := ` ` - if _, err := fmt.Fprintln(c, features); err != nil { + if _, err := fmt.Fprintln(sc.connection, features); err != nil { t.Errorf("cannot send stream feature: %s", err) } } -func sendRFC3921Feature(t *testing.T, c net.Conn, _ *xml.Decoder) { +func sendRFC3921Feature(t *testing.T, sc *ServerConn) { // This is a basic server, supporting only 2 features after auth: resource & session binding features := ` ` - if _, err := fmt.Fprintln(c, features); err != nil { + if _, err := fmt.Fprintln(sc.connection, features); err != nil { t.Errorf("cannot send stream feature: %s", err) } } -func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := stanza.NextStart(decoder) +func bind(t *testing.T, sc *ServerConn) { + se, err := stanza.NextStart(sc.decoder) if err != nil { t.Errorf("cannot read bind: %s", err) return @@ -267,7 +267,7 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { iq := &stanza.IQ{} // Decode element into pointer storage - if err = decoder.DecodeElement(&iq, &se); err != nil { + if err = sc.decoder.DecodeElement(&iq, &se); err != nil { t.Errorf("cannot decode bind iq: %s", err) return } @@ -280,12 +280,12 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { %s ` - fmt.Fprintf(c, result, iq.Id, "test@localhost/test") // TODO use real JID + fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real JID } } -func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { - se, err := stanza.NextStart(decoder) +func session(t *testing.T, sc *ServerConn) { + se, err := stanza.NextStart(sc.decoder) if err != nil { t.Errorf("cannot read session: %s", err) return @@ -293,7 +293,7 @@ func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { iq := &stanza.IQ{} // Decode element into pointer storage - if err = decoder.DecodeElement(&iq, &se); err != nil { + if err = sc.decoder.DecodeElement(&iq, &se); err != nil { t.Errorf("cannot decode session iq: %s", err) return } @@ -301,6 +301,6 @@ func session(t *testing.T, c net.Conn, decoder *xml.Decoder) { switch iq.Payload.(type) { case *stanza.StreamSession: result := `` - fmt.Fprintf(c, result, iq.Id) + fmt.Fprintf(sc.connection, result, iq.Id) } } From 1ba2add651723955733a20e67197408f3b8e7026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Mon, 16 Dec 2019 01:42:27 +0100 Subject: [PATCH 19/54] Example client with TUI --- _examples/xmpp_chat_client/config.yml | 12 + _examples/xmpp_chat_client/go.mod | 10 + _examples/xmpp_chat_client/interface.go | 167 +++++++++++++ .../xmpp_chat_client/xmpp_chat_client.go | 230 +++++++++++++++--- cmd/go.mod | 2 +- go.mod | 3 + 6 files changed, 384 insertions(+), 40 deletions(-) create mode 100644 _examples/xmpp_chat_client/config.yml create mode 100644 _examples/xmpp_chat_client/go.mod create mode 100644 _examples/xmpp_chat_client/interface.go diff --git a/_examples/xmpp_chat_client/config.yml b/_examples/xmpp_chat_client/config.yml new file mode 100644 index 0000000..2ebfe1b --- /dev/null +++ b/_examples/xmpp_chat_client/config.yml @@ -0,0 +1,12 @@ +# Default config for the client +Server : + - full_address: "localhost:5222" + - port: 5222 +Client : + - name: "testuser2" + - jid: "testuser2@localhost" + - pass: "pass123" #Password in a config file yay + +Contacts : "testuser1@localhost;testuser3@localhost" + + diff --git a/_examples/xmpp_chat_client/go.mod b/_examples/xmpp_chat_client/go.mod new file mode 100644 index 0000000..8d510f6 --- /dev/null +++ b/_examples/xmpp_chat_client/go.mod @@ -0,0 +1,10 @@ +module go-xmpp/_examples/xmpp_chat_client + +go 1.13 + +require ( + github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.6.1 + gosrc.io/xmpp v0.3.1-0.20191212145100-27130d72926b +) diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go new file mode 100644 index 0000000..a64f182 --- /dev/null +++ b/_examples/xmpp_chat_client/interface.go @@ -0,0 +1,167 @@ +package main + +import ( + "fmt" + "github.com/awesome-gocui/gocui" + "log" +) + +const ( + chatLogWindow = "clw" + inputWindow = "iw" + menuWindow = "menw" +) + +func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { + if _, err := g.SetCurrentView(name); err != nil { + return nil, err + } + return g.SetViewOnTop(name) +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + if v, err := g.SetView(chatLogWindow, maxX/5, 0, maxX-1, 5*maxY/6-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Chat log" + v.Wrap = true + v.Autoscroll = true + } + + if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Contacts" + v.Wrap = true + v.Autoscroll = true + } + + if v, err := g.SetView(inputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Write a message :" + v.Editable = true + v.Wrap = true + + if _, err = setCurrentViewOnTop(g, inputWindow); err != nil { + return err + } + } + + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +// Sends an input line from the user to the backend while also printing it in the chatlog window. +func writeInput(g *gocui.Gui, v *gocui.View) error { + log, _ := g.View(chatLogWindow) + for _, line := range v.ViewBufferLines() { + textChan <- line + fmt.Fprintln(log, "Me : ", line) + } + v.Clear() + v.EditDeleteToStartOfLine() + return nil +} + +func setKeyBindings(g *gocui.Gui) { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(inputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(inputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + + } + if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + log.Panicln(err) + + } + if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + log.Panicln(err) + + } + if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil { + log.Panicln(err) + } + +} + +// When we select a new correspondent, we change it in the client, and we display a message window confirming the change. +func getLine(g *gocui.Gui, v *gocui.View) error { + var l string + var err error + + _, cy := v.Cursor() + if l, err = v.Line(cy); err != nil { + l = "" + } + // Updating the current correspondent, back-end side. + CorrespChan <- l + + // Showing a message to the user, and switching back to input after the new contact is selected. + message := "Now sending messages to : " + l + " in a private conversation" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + g.SetCurrentView(inputWindow) + return nil +} + +// Changing view between input and "menu" (= basically contacts only right now) when pressing the specific key. +func nextView(g *gocui.Gui, v *gocui.View) error { + if v == nil || v.Name() == inputWindow { + _, err := g.SetCurrentView(menuWindow) + return err + } + _, err := g.SetCurrentView(inputWindow) + return err +} + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + // Avoid going below the list of contacts + cv := g.CurrentView() + h := cv.LinesHeight() + if cy+1 >= h-1 { + return nil + } + // Lower cursor + if err := v.SetCursor(cx, cy+1); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + } + return nil +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + } + return nil +} diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 2b2d2e7..9e3c2c6 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -6,90 +6,242 @@ Note that this example sends to a very specific user. User logic is not implemen */ import ( - . "bufio" + "flag" "fmt" - "os" - + "github.com/awesome-gocui/gocui" + "github.com/spf13/pflag" + "github.com/spf13/viper" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" + "log" + "strings" ) const ( - currentUserAddress = "localhost:5222" - currentUserJid = "testuser@localhost" - currentUserPass = "testpass" - correspondantJid = "testuser2@localhost" + infoFormat = "====== " + // Default configuration + defaultConfigFilePath = "./" + + configFileName = "config" + configType = "yaml" + // Keys in config + serverAddressKey = "full_address" + clientJid = "jid" + clientPass = "pass" + configContactSep = ";" ) +var ( + CorrespChan = make(chan string, 1) + textChan = make(chan string, 5) + killChan = make(chan struct{}, 1) +) + +type config struct { + Server map[string]string `mapstructure:"server"` + Client map[string]string `mapstructure:"client"` + Contacts string `string:"contact"` +} + func main() { - config := xmpp.Config{ + // ============================================================ + // Parse the flag with the config directory path as argument + flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+ + " file you want to use. Config file should be named \"config\" and be of YAML format..") + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.Parse() + + // ========================== + // Read configuration + c := readConfig() + + // ========================== + // Create TUI + g, err := gocui.NewGui(gocui.OutputNormal, true) + if err != nil { + log.Panicln(err) + } + defer g.Close() + g.Highlight = true + g.Cursor = true + g.SelFgColor = gocui.ColorGreen + g.SetManagerFunc(layout) + setKeyBindings(g) + + // ========================== + // Run TUI + errChan := make(chan error) + go func() { + errChan <- g.MainLoop() + }() + + // ========================== + // Start XMPP client + go startClient(g, c) + + select { + case err := <-errChan: + if err == gocui.ErrQuit { + log.Println("Closing client.") + } else { + log.Panicln(err) + } + } +} + +func startClient(g *gocui.Gui, config *config) { + + // ========================== + // Client setup + clientCfg := xmpp.Config{ TransportConfiguration: xmpp.TransportConfiguration{ - Address: currentUserAddress, + Address: config.Server[serverAddressKey], }, - Jid: currentUserJid, - Credential: xmpp.Password(currentUserPass), + Jid: config.Client[clientJid], + Credential: xmpp.Password(config.Client[clientPass]), Insecure: true} var client *xmpp.Client var err error router := xmpp.NewRouter() - router.HandleFunc("message", handleMessage) - if client, err = xmpp.NewClient(config, router, errorHandler); err != nil { - fmt.Println("Error new client") + + handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) { + msg, ok := p.(stanza.Message) + v, err := g.View(chatLogWindow) + if !ok { + fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p) + return + } + if err != nil { + return + } + g.Update(func(g *gocui.Gui) error { + if msg.Error.Code != 0 { + _, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space) + return err + } + _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) + return err + }) } - // Connecting client and handling messages - // To use a stream manager, just write something like this instead : - //cm := xmpp.NewStreamManager(client, startMessaging) - //log.Fatal(cm.Run()) //=> this will lock the calling goroutine + router.HandleFunc("message", handlerWithGui) + if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil { + panic(fmt.Sprintf("Could not create a new client ! %s", err)) + } + + // ========================== + // Client connection if err = client.Connect(); err != nil { - fmt.Printf("XMPP connection failed: %s", err) + msg := fmt.Sprintf("%sXMPP connection failed: %s", infoFormat, err) + g.Update(func(g *gocui.Gui) error { + v, err := g.View(chatLogWindow) + fmt.Fprintf(v, msg) + return err + }) return } - startMessaging(client) + // ========================== + // Start working + //askForRoster(client, g) + updateRosterFromConfig(g, config) + startMessaging(client, config) } -func startMessaging(client xmpp.Sender) { - reader := NewReader(os.Stdin) - textChan := make(chan string) +func startMessaging(client xmpp.Sender, config *config) { var text string + // Update this with a channel. Default value is the first contact in the list from the config. + correspondent := strings.Split(config.Contacts, configContactSep)[0] for { - fmt.Print("Enter text: ") - go readInput(reader, textChan) select { case <-killChan: return case text = <-textChan: - reply := stanza.Message{Attrs: stanza.Attrs{To: correspondantJid}, Body: text} + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent}, Body: text} err := client.Send(reply) if err != nil { fmt.Printf("There was a problem sending the message : %v", reply) return } + case crrsp := <-CorrespChan: + correspondent = crrsp } + } } -func readInput(reader *Reader, textChan chan string) { - text, _ := reader.ReadString('\n') - textChan <- text +func readConfig() *config { + viper.SetConfigName(configFileName) // name of config file (without extension) + viper.BindPFlags(pflag.CommandLine) + viper.AddConfigPath(viper.GetString("c")) // path to look for the config file in + err := viper.ReadInConfig() // Find and read the config file + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + log.Fatalf("%s %s", err, "Please make sure you give a path to the directory of the config and not to the config itself.") + } else { + log.Panicln(err) + } + } + viper.SetConfigType(configType) + var config config + err = viper.Unmarshal(&config) + if err != nil { + panic(fmt.Errorf("Unable to decode Config: %s \n", err)) + } + + return &config } -var killChan = make(chan struct{}) - -// If an error occurs, this is used +// If an error occurs, this is used to kill the client func errorHandler(err error) { fmt.Printf("%v", err) killChan <- struct{}{} } -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) +// Read the client roster from the config. This does not check with the server that the roster is correct. +// If user tries to send a message to someone not registered with the server, the server will return an error. +func updateRosterFromConfig(g *gocui.Gui, config *config) { + g.Update(func(g *gocui.Gui) error { + menu, _ := g.View(menuWindow) + for _, contact := range strings.Split(config.Contacts, configContactSep) { + fmt.Fprintln(menu, contact) + } + return nil + }) +} + +// Updates the menu panel of the view with the current user's roster. +// Need to add support for Roster IQ stanzas to make this work. +func askForRoster(client *xmpp.Client, g *gocui.Gui) { + //ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + //iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: currentUserJid, To: "localhost", Lang: "en"}) + //disco := iqReq.DiscoInfo() + //iqReq.Payload = disco + // + //// Handle a possible error + //errChan := make(chan error) + //errorHandler := func(err error) { + // errChan <- err + //} + //client.ErrorHandler = errorHandler + //res, err := client.SendIQ(ctx, iqReq) + //if err != nil { + // t.Errorf(err.Error()) + //} + // + //select { + //case <-res: + //} + + //roster := []string{"testuser1", "testuser2", "testuser3@localhost"} + // + //g.Update(func(g *gocui.Gui) error { + // menu, _ := g.View(menuWindow) + // for _, contact := range roster { + // fmt.Fprintln(menu, contact) + // } + // return nil + //}) } diff --git a/cmd/go.mod b/cmd/go.mod index 85df002..1c4684f 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -6,7 +6,7 @@ require ( github.com/bdlm/log v0.1.19 github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 github.com/spf13/cobra v0.0.5 - github.com/spf13/viper v1.4.0 + github.com/spf13/viper v1.6.1 gosrc.io/xmpp v0.1.1 ) diff --git a/go.mod b/go.mod index f31fe40..d3b3273 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module gosrc.io/xmpp go 1.13 require ( + github.com/awesome-gocui/gocui v0.6.0 // indirect github.com/google/go-cmp v0.3.1 github.com/google/uuid v1.1.1 + github.com/spf13/viper v1.6.1 // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 nhooyr.io/websocket v1.6.5 + ) From f0179ad90e66b131bcf5a19c44ea9fb3748307b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Wed, 18 Dec 2019 01:55:00 +0100 Subject: [PATCH 20/54] Logging. Added menus. Can now send raw stanzas. --- _examples/xmpp_chat_client/config.yml | 3 + _examples/xmpp_chat_client/interface.go | 229 +++++++++++++++--- .../xmpp_chat_client/xmpp_chat_client.go | 135 +++++++---- 3 files changed, 287 insertions(+), 80 deletions(-) diff --git a/_examples/xmpp_chat_client/config.yml b/_examples/xmpp_chat_client/config.yml index 2ebfe1b..ed6e902 100644 --- a/_examples/xmpp_chat_client/config.yml +++ b/_examples/xmpp_chat_client/config.yml @@ -9,4 +9,7 @@ Client : Contacts : "testuser1@localhost;testuser3@localhost" +LogStanzas: + - logger_on: "true" + - logfile_path: "./logs" diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index a64f182..84c2ed6 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -1,15 +1,48 @@ package main import ( + "errors" "fmt" "github.com/awesome-gocui/gocui" "log" + "strings" ) const ( - chatLogWindow = "clw" - inputWindow = "iw" - menuWindow = "menw" + // Windows + chatLogWindow = "clw" // Where (received and sent) messages are logged + chatInputWindow = "iw" // Where messages are written + rawInputWindow = "rw" // Where raw stanzas are written + contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable + menuWindow = "mw" // Where the menu is shown + + // Menu options + disconnect = "Disconnect" + askServerForRoster = "Ask server for roster" + rawMode = "Switch to Send Raw Mode" + messageMode = "Switch to Send Message Mode" + contactList = "Contacts list" + backFromContacts = "<- Go back" +) + +// To store names of views on top +type viewsState struct { + input string // Which input view is on top + side string // Which side view is on top + contacts []string // Contacts list + currentContact string // Contact we are currently messaging +} + +var ( + // Which window is on top currently on top of the other. + // This is the init setup + viewState = viewsState{ + input: chatInputWindow, + side: menuWindow, + } + menuOptions = []string{contactList, rawMode, askServerForRoster, disconnect} + // Errors + servConnFail = errors.New("failed to connect to server. Check your configuration ? Exiting") ) func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { @@ -31,7 +64,7 @@ func layout(g *gocui.Gui) error { v.Autoscroll = true } - if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-1, 0); err != nil { + if v, err := g.SetView(contactsListWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil { if !gocui.IsUnknownView(err) { return err } @@ -40,7 +73,29 @@ func layout(g *gocui.Gui) error { v.Autoscroll = true } - if v, err := g.SetView(inputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { + if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Menu" + v.Wrap = true + v.Autoscroll = true + fmt.Fprint(v, strings.Join(menuOptions, "\n")) + if _, err = setCurrentViewOnTop(g, menuWindow); err != nil { + return err + } + } + + if v, err := g.SetView(rawInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :" + v.Editable = true + v.Wrap = true + } + + if v, err := g.SetView(chatInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { if !gocui.IsUnknownView(err) { return err } @@ -48,7 +103,7 @@ func layout(g *gocui.Gui) error { v.Editable = true v.Wrap = true - if _, err = setCurrentViewOnTop(g, inputWindow); err != nil { + if _, err = setCurrentViewOnTop(g, chatInputWindow); err != nil { return err } } @@ -60,50 +115,83 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -// Sends an input line from the user to the backend while also printing it in the chatlog window. +// Sends an input text from the user to the backend while also printing it in the chatlog window. +// KeyEnter is viewed as "\n" by gocui, so messages should only be one line, whereas raw sending has a different key +// binding and therefor should work with this too (for multiple lines stanzas) func writeInput(g *gocui.Gui, v *gocui.View) error { - log, _ := g.View(chatLogWindow) - for _, line := range v.ViewBufferLines() { - textChan <- line - fmt.Fprintln(log, "Me : ", line) - } + chatLogWindow, _ := g.View(chatLogWindow) + + input := strings.Join(v.ViewBufferLines(), "\n") + + fmt.Fprintln(chatLogWindow, "Me : ", input) + textChan <- input + v.Clear() v.EditDeleteToStartOfLine() return nil } func setKeyBindings(g *gocui.Gui) { + // ========================== + // All views if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { log.Panicln(err) } - if err := g.SetKeybinding(inputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil { + // ========================== + // Chat input + if err := g.SetKeybinding(chatInputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil { log.Panicln(err) } - if err := g.SetKeybinding(inputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { - log.Panicln(err) - - } - if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + if err := g.SetKeybinding(chatInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { log.Panicln(err) } + // ========================== + // Raw input + if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlE, gocui.ModNone, writeInput); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + + // ========================== + // Menu if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { log.Panicln(err) - } if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { log.Panicln(err) - + } + if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) } if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil { log.Panicln(err) } + // ========================== + // Contacts list + if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(contactsListWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(contactsListWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + } -// When we select a new correspondent, we change it in the client, and we display a message window confirming the change. +// General +// Used to handle menu selections and navigations func getLine(g *gocui.Gui, v *gocui.View) error { var l string var err error @@ -112,34 +200,107 @@ func getLine(g *gocui.Gui, v *gocui.View) error { if l, err = v.Line(cy); err != nil { l = "" } - // Updating the current correspondent, back-end side. - CorrespChan <- l + if viewState.side == menuWindow { + if l == contactList { + cv, _ := g.View(contactsListWindow) + viewState.side = contactsListWindow + g.SetViewOnTop(contactsListWindow) + g.SetCurrentView(contactsListWindow) + if len(cv.ViewBufferLines()) == 0 { + printContactsToWindow(g, viewState.contacts) + } + } else if l == disconnect || l == askServerForRoster { + chlw, _ := g.View(chatLogWindow) + fmt.Fprintln(chlw, infoFormat+" Not yet implemented !") + } else if l == rawMode { + mw, _ := g.View(menuWindow) + viewState.input = rawInputWindow + g.SetViewOnTop(rawInputWindow) + g.SetCurrentView(rawInputWindow) + menuOptions[1] = messageMode + v.Clear() + v.EditDeleteToStartOfLine() + fmt.Fprintln(mw, strings.Join(menuOptions, "\n")) + message := "Now sending in raw stanza mode" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + } else if l == messageMode { + mw, _ := g.View(menuWindow) + viewState.input = chatInputWindow + g.SetViewOnTop(chatInputWindow) + g.SetCurrentView(chatInputWindow) + menuOptions[1] = rawMode + v.Clear() + v.EditDeleteToStartOfLine() + fmt.Fprintln(mw, strings.Join(menuOptions, "\n")) + message := "Now sending in messages mode" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + } + } else if viewState.side == contactsListWindow { + if l == backFromContacts { + viewState.side = menuWindow + g.SetViewOnTop(menuWindow) + g.SetCurrentView(menuWindow) + } else if l == "" { + return nil + } else { + // Updating the current correspondent, back-end side. + CorrespChan <- l + viewState.currentContact = l + // Showing the selected contact in contacts list + cl, _ := g.View(contactsListWindow) + cts := cl.ViewBufferLines() + cl.Clear() + printContactsToWindow(g, cts) + // Showing a message to the user, and switching back to input after the new contact is selected. + message := "Now sending messages to : " + l + " in a private conversation" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + g.SetCurrentView(chatInputWindow) + } + } - // Showing a message to the user, and switching back to input after the new contact is selected. - message := "Now sending messages to : " + l + " in a private conversation" - clv, _ := g.View(chatLogWindow) - fmt.Fprintln(clv, infoFormat+message) - g.SetCurrentView(inputWindow) return nil } -// Changing view between input and "menu" (= basically contacts only right now) when pressing the specific key. +func printContactsToWindow(g *gocui.Gui, contactsList []string) { + cl, _ := g.View(contactsListWindow) + for _, c := range contactsList { + c = strings.ReplaceAll(c, " *", "") + if c == viewState.currentContact { + fmt.Fprintf(cl, c+" *\n") + } else { + fmt.Fprintf(cl, c+"\n") + } + } +} + +// Changing view between input and "menu/contacts" when pressing the specific key. func nextView(g *gocui.Gui, v *gocui.View) error { - if v == nil || v.Name() == inputWindow { - _, err := g.SetCurrentView(menuWindow) + if v == nil || v.Name() == chatInputWindow || v.Name() == rawInputWindow { + _, err := g.SetCurrentView(viewState.side) + return err + } else if v.Name() == menuWindow || v.Name() == contactsListWindow { + _, err := g.SetCurrentView(viewState.input) return err } - _, err := g.SetCurrentView(inputWindow) + + // Should not be reached right now + _, err := g.SetCurrentView(chatInputWindow) return err } func cursorDown(g *gocui.Gui, v *gocui.View) error { if v != nil { cx, cy := v.Cursor() - // Avoid going below the list of contacts + // Avoid going below the list of contacts. Although lines are stored in the view as a slice + // in the used lib. Therefor, if the number of lines is too big, the cursor will go past the last line since + // increasing slice capacity is done by doubling it. Last lines will be "nil" and reachable by the cursor + // in a dynamic context (such as contacts list) cv := g.CurrentView() h := cv.LinesHeight() - if cy+1 >= h-1 { + if cy+1 >= h { return nil } // Lower cursor diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 9e3c2c6..3904e2f 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -2,10 +2,10 @@ package main /* xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members -Note that this example sends to a very specific user. User logic is not implemented here. */ import ( + "encoding/xml" "flag" "fmt" "github.com/awesome-gocui/gocui" @@ -14,6 +14,9 @@ import ( "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" "log" + "os" + "path" + "strconv" "strings" ) @@ -24,6 +27,8 @@ const ( configFileName = "config" configType = "yaml" + logStanzasOn = "logger_on" + logFilePath = "logfile_path" // Keys in config serverAddressKey = "full_address" clientJid = "jid" @@ -34,16 +39,22 @@ const ( var ( CorrespChan = make(chan string, 1) textChan = make(chan string, 5) + rawTextChan = make(chan string, 5) killChan = make(chan struct{}, 1) + errChan = make(chan error) + + logger *log.Logger ) type config struct { - Server map[string]string `mapstructure:"server"` - Client map[string]string `mapstructure:"client"` - Contacts string `string:"contact"` + Server map[string]string `mapstructure:"server"` + Client map[string]string `mapstructure:"client"` + Contacts string `string:"contact"` + LogStanzas map[string]string `mapstructure:"logstanzas"` } func main() { + // ============================================================ // Parse the flag with the config directory path as argument flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+ @@ -55,6 +66,22 @@ func main() { // Read configuration c := readConfig() + //================================ + // Setup logger + on, err := strconv.ParseBool(c.LogStanzas[logStanzasOn]) + if err != nil { + log.Panicln(err) + } + if on { + f, err := os.OpenFile(path.Join(c.LogStanzas[logFilePath], "logs.txt"), os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) + if err != nil { + log.Panicln(err) + } + logger = log.New(f, "", log.Lshortfile|log.Ldate|log.Ltime) + logger.SetOutput(f) + defer f.Close() + } + // ========================== // Create TUI g, err := gocui.NewGui(gocui.OutputNormal, true) @@ -70,7 +97,6 @@ func main() { // ========================== // Run TUI - errChan := make(chan error) go func() { errChan <- g.MainLoop() }() @@ -107,6 +133,10 @@ func startClient(g *gocui.Gui, config *config) { handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) + if logger != nil { + logger.Println(msg) + } + v, err := g.View(chatLogWindow) if !ok { fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p) @@ -120,8 +150,11 @@ func startClient(g *gocui.Gui, config *config) { _, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space) return err } - _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) - return err + if len(strings.TrimSpace(msg.Body)) != 0 { + _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) + return err + } + return nil }) } @@ -140,6 +173,8 @@ func startClient(g *gocui.Gui, config *config) { fmt.Fprintf(v, msg) return err }) + fmt.Println("Failed to connect to server. Exiting...") + errChan <- servConnFail return } @@ -147,24 +182,42 @@ func startClient(g *gocui.Gui, config *config) { // Start working //askForRoster(client, g) updateRosterFromConfig(g, config) + // Sending the default contact in a channel. Default value is the first contact in the list from the config. + viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0] + // Informing user of the default contact + clw, _ := g.View(chatLogWindow) + fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n") + CorrespChan <- viewState.currentContact startMessaging(client, config) } func startMessaging(client xmpp.Sender, config *config) { var text string - // Update this with a channel. Default value is the first contact in the list from the config. - correspondent := strings.Split(config.Contacts, configContactSep)[0] + var correspondent string for { select { case <-killChan: return case text = <-textChan: - reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent}, Body: text} + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, From: config.Client[clientJid], Type: stanza.MessageTypeChat}, Body: text} + if logger != nil { + raw, _ := xml.Marshal(reply) + logger.Println(string(raw)) + } err := client.Send(reply) if err != nil { fmt.Printf("There was a problem sending the message : %v", reply) return } + case text = <-rawTextChan: + if logger != nil { + logger.Println(text) + } + err := client.SendRaw(text) + if err != nil { + fmt.Printf("There was a problem sending the message : %v", text) + return + } case crrsp := <-CorrespChan: correspondent = crrsp } @@ -172,6 +225,7 @@ func startMessaging(client xmpp.Sender, config *config) { } } +// Only reads and parses the configuration func readConfig() *config { viper.SetConfigName(configFileName) // name of config file (without extension) viper.BindPFlags(pflag.CommandLine) @@ -184,6 +238,7 @@ func readConfig() *config { log.Panicln(err) } } + viper.SetConfigType(configType) var config config err = viper.Unmarshal(&config) @@ -191,6 +246,20 @@ func readConfig() *config { panic(fmt.Errorf("Unable to decode Config: %s \n", err)) } + // Check if we have contacts to message + if len(strings.TrimSpace(config.Contacts)) == 0 { + log.Panicln("You appear to have no contacts to message !") + } + // Check logging + config.LogStanzas[logFilePath] = path.Clean(config.LogStanzas[logFilePath]) + on, err := strconv.ParseBool(config.LogStanzas[logStanzasOn]) + if err != nil { + log.Panicln(err) + } + if d, e := isDirectory(config.LogStanzas[logFilePath]); (e != nil || !d) && on { + log.Panicln("The log file path could not be found or is not a directory.") + } + return &config } @@ -203,45 +272,19 @@ func errorHandler(err error) { // Read the client roster from the config. This does not check with the server that the roster is correct. // If user tries to send a message to someone not registered with the server, the server will return an error. func updateRosterFromConfig(g *gocui.Gui, config *config) { - g.Update(func(g *gocui.Gui) error { - menu, _ := g.View(menuWindow) - for _, contact := range strings.Split(config.Contacts, configContactSep) { - fmt.Fprintln(menu, contact) - } - return nil - }) + viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts) } // Updates the menu panel of the view with the current user's roster. // Need to add support for Roster IQ stanzas to make this work. func askForRoster(client *xmpp.Client, g *gocui.Gui) { - //ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) - //iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: currentUserJid, To: "localhost", Lang: "en"}) - //disco := iqReq.DiscoInfo() - //iqReq.Payload = disco - // - //// Handle a possible error - //errChan := make(chan error) - //errorHandler := func(err error) { - // errChan <- err - //} - //client.ErrorHandler = errorHandler - //res, err := client.SendIQ(ctx, iqReq) - //if err != nil { - // t.Errorf(err.Error()) - //} - // - //select { - //case <-res: - //} - - //roster := []string{"testuser1", "testuser2", "testuser3@localhost"} - // - //g.Update(func(g *gocui.Gui) error { - // menu, _ := g.View(menuWindow) - // for _, contact := range roster { - // fmt.Fprintln(menu, contact) - // } - // return nil - //}) + // Not implemented yet ! +} + +func isDirectory(path string) (bool, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return false, err + } + return fileInfo.IsDir(), err } From c006990c20533148f8afe433673c862178a77c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Wed, 18 Dec 2019 07:59:23 +0100 Subject: [PATCH 21/54] Logging. Added menus. Can now send raw stanzas. --- _examples/xmpp_chat_client/config.yml | 3 + _examples/xmpp_chat_client/interface.go | 229 +++++++++++++++--- .../xmpp_chat_client/xmpp_chat_client.go | 135 +++++++---- 3 files changed, 287 insertions(+), 80 deletions(-) diff --git a/_examples/xmpp_chat_client/config.yml b/_examples/xmpp_chat_client/config.yml index 2ebfe1b..ed6e902 100644 --- a/_examples/xmpp_chat_client/config.yml +++ b/_examples/xmpp_chat_client/config.yml @@ -9,4 +9,7 @@ Client : Contacts : "testuser1@localhost;testuser3@localhost" +LogStanzas: + - logger_on: "true" + - logfile_path: "./logs" diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index a64f182..84c2ed6 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -1,15 +1,48 @@ package main import ( + "errors" "fmt" "github.com/awesome-gocui/gocui" "log" + "strings" ) const ( - chatLogWindow = "clw" - inputWindow = "iw" - menuWindow = "menw" + // Windows + chatLogWindow = "clw" // Where (received and sent) messages are logged + chatInputWindow = "iw" // Where messages are written + rawInputWindow = "rw" // Where raw stanzas are written + contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable + menuWindow = "mw" // Where the menu is shown + + // Menu options + disconnect = "Disconnect" + askServerForRoster = "Ask server for roster" + rawMode = "Switch to Send Raw Mode" + messageMode = "Switch to Send Message Mode" + contactList = "Contacts list" + backFromContacts = "<- Go back" +) + +// To store names of views on top +type viewsState struct { + input string // Which input view is on top + side string // Which side view is on top + contacts []string // Contacts list + currentContact string // Contact we are currently messaging +} + +var ( + // Which window is on top currently on top of the other. + // This is the init setup + viewState = viewsState{ + input: chatInputWindow, + side: menuWindow, + } + menuOptions = []string{contactList, rawMode, askServerForRoster, disconnect} + // Errors + servConnFail = errors.New("failed to connect to server. Check your configuration ? Exiting") ) func setCurrentViewOnTop(g *gocui.Gui, name string) (*gocui.View, error) { @@ -31,7 +64,7 @@ func layout(g *gocui.Gui) error { v.Autoscroll = true } - if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-1, 0); err != nil { + if v, err := g.SetView(contactsListWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil { if !gocui.IsUnknownView(err) { return err } @@ -40,7 +73,29 @@ func layout(g *gocui.Gui) error { v.Autoscroll = true } - if v, err := g.SetView(inputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { + if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Menu" + v.Wrap = true + v.Autoscroll = true + fmt.Fprint(v, strings.Join(menuOptions, "\n")) + if _, err = setCurrentViewOnTop(g, menuWindow); err != nil { + return err + } + } + + if v, err := g.SetView(rawInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + v.Title = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :" + v.Editable = true + v.Wrap = true + } + + if v, err := g.SetView(chatInputWindow, 0, 5*maxY/6-1, maxX/1-1, maxY-1, 0); err != nil { if !gocui.IsUnknownView(err) { return err } @@ -48,7 +103,7 @@ func layout(g *gocui.Gui) error { v.Editable = true v.Wrap = true - if _, err = setCurrentViewOnTop(g, inputWindow); err != nil { + if _, err = setCurrentViewOnTop(g, chatInputWindow); err != nil { return err } } @@ -60,50 +115,83 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -// Sends an input line from the user to the backend while also printing it in the chatlog window. +// Sends an input text from the user to the backend while also printing it in the chatlog window. +// KeyEnter is viewed as "\n" by gocui, so messages should only be one line, whereas raw sending has a different key +// binding and therefor should work with this too (for multiple lines stanzas) func writeInput(g *gocui.Gui, v *gocui.View) error { - log, _ := g.View(chatLogWindow) - for _, line := range v.ViewBufferLines() { - textChan <- line - fmt.Fprintln(log, "Me : ", line) - } + chatLogWindow, _ := g.View(chatLogWindow) + + input := strings.Join(v.ViewBufferLines(), "\n") + + fmt.Fprintln(chatLogWindow, "Me : ", input) + textChan <- input + v.Clear() v.EditDeleteToStartOfLine() return nil } func setKeyBindings(g *gocui.Gui) { + // ========================== + // All views if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { log.Panicln(err) } - if err := g.SetKeybinding(inputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil { + // ========================== + // Chat input + if err := g.SetKeybinding(chatInputWindow, gocui.KeyEnter, gocui.ModNone, writeInput); err != nil { log.Panicln(err) } - if err := g.SetKeybinding(inputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { - log.Panicln(err) - - } - if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + if err := g.SetKeybinding(chatInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { log.Panicln(err) } + // ========================== + // Raw input + if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlE, gocui.ModNone, writeInput); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding(rawInputWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + + // ========================== + // Menu if err := g.SetKeybinding(menuWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { log.Panicln(err) - } if err := g.SetKeybinding(menuWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { log.Panicln(err) - + } + if err := g.SetKeybinding(menuWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) } if err := g.SetKeybinding(menuWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil { log.Panicln(err) } + // ========================== + // Contacts list + if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(contactsListWindow, gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(contactsListWindow, gocui.KeyEnter, gocui.ModNone, getLine); err != nil { + log.Panicln(err) + } + if err := g.SetKeybinding(contactsListWindow, gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { + log.Panicln(err) + } + } -// When we select a new correspondent, we change it in the client, and we display a message window confirming the change. +// General +// Used to handle menu selections and navigations func getLine(g *gocui.Gui, v *gocui.View) error { var l string var err error @@ -112,34 +200,107 @@ func getLine(g *gocui.Gui, v *gocui.View) error { if l, err = v.Line(cy); err != nil { l = "" } - // Updating the current correspondent, back-end side. - CorrespChan <- l + if viewState.side == menuWindow { + if l == contactList { + cv, _ := g.View(contactsListWindow) + viewState.side = contactsListWindow + g.SetViewOnTop(contactsListWindow) + g.SetCurrentView(contactsListWindow) + if len(cv.ViewBufferLines()) == 0 { + printContactsToWindow(g, viewState.contacts) + } + } else if l == disconnect || l == askServerForRoster { + chlw, _ := g.View(chatLogWindow) + fmt.Fprintln(chlw, infoFormat+" Not yet implemented !") + } else if l == rawMode { + mw, _ := g.View(menuWindow) + viewState.input = rawInputWindow + g.SetViewOnTop(rawInputWindow) + g.SetCurrentView(rawInputWindow) + menuOptions[1] = messageMode + v.Clear() + v.EditDeleteToStartOfLine() + fmt.Fprintln(mw, strings.Join(menuOptions, "\n")) + message := "Now sending in raw stanza mode" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + } else if l == messageMode { + mw, _ := g.View(menuWindow) + viewState.input = chatInputWindow + g.SetViewOnTop(chatInputWindow) + g.SetCurrentView(chatInputWindow) + menuOptions[1] = rawMode + v.Clear() + v.EditDeleteToStartOfLine() + fmt.Fprintln(mw, strings.Join(menuOptions, "\n")) + message := "Now sending in messages mode" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + } + } else if viewState.side == contactsListWindow { + if l == backFromContacts { + viewState.side = menuWindow + g.SetViewOnTop(menuWindow) + g.SetCurrentView(menuWindow) + } else if l == "" { + return nil + } else { + // Updating the current correspondent, back-end side. + CorrespChan <- l + viewState.currentContact = l + // Showing the selected contact in contacts list + cl, _ := g.View(contactsListWindow) + cts := cl.ViewBufferLines() + cl.Clear() + printContactsToWindow(g, cts) + // Showing a message to the user, and switching back to input after the new contact is selected. + message := "Now sending messages to : " + l + " in a private conversation" + clv, _ := g.View(chatLogWindow) + fmt.Fprintln(clv, infoFormat+message) + g.SetCurrentView(chatInputWindow) + } + } - // Showing a message to the user, and switching back to input after the new contact is selected. - message := "Now sending messages to : " + l + " in a private conversation" - clv, _ := g.View(chatLogWindow) - fmt.Fprintln(clv, infoFormat+message) - g.SetCurrentView(inputWindow) return nil } -// Changing view between input and "menu" (= basically contacts only right now) when pressing the specific key. +func printContactsToWindow(g *gocui.Gui, contactsList []string) { + cl, _ := g.View(contactsListWindow) + for _, c := range contactsList { + c = strings.ReplaceAll(c, " *", "") + if c == viewState.currentContact { + fmt.Fprintf(cl, c+" *\n") + } else { + fmt.Fprintf(cl, c+"\n") + } + } +} + +// Changing view between input and "menu/contacts" when pressing the specific key. func nextView(g *gocui.Gui, v *gocui.View) error { - if v == nil || v.Name() == inputWindow { - _, err := g.SetCurrentView(menuWindow) + if v == nil || v.Name() == chatInputWindow || v.Name() == rawInputWindow { + _, err := g.SetCurrentView(viewState.side) + return err + } else if v.Name() == menuWindow || v.Name() == contactsListWindow { + _, err := g.SetCurrentView(viewState.input) return err } - _, err := g.SetCurrentView(inputWindow) + + // Should not be reached right now + _, err := g.SetCurrentView(chatInputWindow) return err } func cursorDown(g *gocui.Gui, v *gocui.View) error { if v != nil { cx, cy := v.Cursor() - // Avoid going below the list of contacts + // Avoid going below the list of contacts. Although lines are stored in the view as a slice + // in the used lib. Therefor, if the number of lines is too big, the cursor will go past the last line since + // increasing slice capacity is done by doubling it. Last lines will be "nil" and reachable by the cursor + // in a dynamic context (such as contacts list) cv := g.CurrentView() h := cv.LinesHeight() - if cy+1 >= h-1 { + if cy+1 >= h { return nil } // Lower cursor diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 9e3c2c6..3904e2f 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -2,10 +2,10 @@ package main /* xmpp_chat_client is a demo client that connect on an XMPP server to chat with other members -Note that this example sends to a very specific user. User logic is not implemented here. */ import ( + "encoding/xml" "flag" "fmt" "github.com/awesome-gocui/gocui" @@ -14,6 +14,9 @@ import ( "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" "log" + "os" + "path" + "strconv" "strings" ) @@ -24,6 +27,8 @@ const ( configFileName = "config" configType = "yaml" + logStanzasOn = "logger_on" + logFilePath = "logfile_path" // Keys in config serverAddressKey = "full_address" clientJid = "jid" @@ -34,16 +39,22 @@ const ( var ( CorrespChan = make(chan string, 1) textChan = make(chan string, 5) + rawTextChan = make(chan string, 5) killChan = make(chan struct{}, 1) + errChan = make(chan error) + + logger *log.Logger ) type config struct { - Server map[string]string `mapstructure:"server"` - Client map[string]string `mapstructure:"client"` - Contacts string `string:"contact"` + Server map[string]string `mapstructure:"server"` + Client map[string]string `mapstructure:"client"` + Contacts string `string:"contact"` + LogStanzas map[string]string `mapstructure:"logstanzas"` } func main() { + // ============================================================ // Parse the flag with the config directory path as argument flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+ @@ -55,6 +66,22 @@ func main() { // Read configuration c := readConfig() + //================================ + // Setup logger + on, err := strconv.ParseBool(c.LogStanzas[logStanzasOn]) + if err != nil { + log.Panicln(err) + } + if on { + f, err := os.OpenFile(path.Join(c.LogStanzas[logFilePath], "logs.txt"), os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) + if err != nil { + log.Panicln(err) + } + logger = log.New(f, "", log.Lshortfile|log.Ldate|log.Ltime) + logger.SetOutput(f) + defer f.Close() + } + // ========================== // Create TUI g, err := gocui.NewGui(gocui.OutputNormal, true) @@ -70,7 +97,6 @@ func main() { // ========================== // Run TUI - errChan := make(chan error) go func() { errChan <- g.MainLoop() }() @@ -107,6 +133,10 @@ func startClient(g *gocui.Gui, config *config) { handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) + if logger != nil { + logger.Println(msg) + } + v, err := g.View(chatLogWindow) if !ok { fmt.Fprintf(v, "%sIgnoring packet: %T\n", infoFormat, p) @@ -120,8 +150,11 @@ func startClient(g *gocui.Gui, config *config) { _, err := fmt.Fprintf(v, "Error from server : %s : %s \n", msg.Error.Reason, msg.XMLName.Space) return err } - _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) - return err + if len(strings.TrimSpace(msg.Body)) != 0 { + _, err := fmt.Fprintf(v, "%s : %s \n", msg.From, msg.Body) + return err + } + return nil }) } @@ -140,6 +173,8 @@ func startClient(g *gocui.Gui, config *config) { fmt.Fprintf(v, msg) return err }) + fmt.Println("Failed to connect to server. Exiting...") + errChan <- servConnFail return } @@ -147,24 +182,42 @@ func startClient(g *gocui.Gui, config *config) { // Start working //askForRoster(client, g) updateRosterFromConfig(g, config) + // Sending the default contact in a channel. Default value is the first contact in the list from the config. + viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0] + // Informing user of the default contact + clw, _ := g.View(chatLogWindow) + fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n") + CorrespChan <- viewState.currentContact startMessaging(client, config) } func startMessaging(client xmpp.Sender, config *config) { var text string - // Update this with a channel. Default value is the first contact in the list from the config. - correspondent := strings.Split(config.Contacts, configContactSep)[0] + var correspondent string for { select { case <-killChan: return case text = <-textChan: - reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent}, Body: text} + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, From: config.Client[clientJid], Type: stanza.MessageTypeChat}, Body: text} + if logger != nil { + raw, _ := xml.Marshal(reply) + logger.Println(string(raw)) + } err := client.Send(reply) if err != nil { fmt.Printf("There was a problem sending the message : %v", reply) return } + case text = <-rawTextChan: + if logger != nil { + logger.Println(text) + } + err := client.SendRaw(text) + if err != nil { + fmt.Printf("There was a problem sending the message : %v", text) + return + } case crrsp := <-CorrespChan: correspondent = crrsp } @@ -172,6 +225,7 @@ func startMessaging(client xmpp.Sender, config *config) { } } +// Only reads and parses the configuration func readConfig() *config { viper.SetConfigName(configFileName) // name of config file (without extension) viper.BindPFlags(pflag.CommandLine) @@ -184,6 +238,7 @@ func readConfig() *config { log.Panicln(err) } } + viper.SetConfigType(configType) var config config err = viper.Unmarshal(&config) @@ -191,6 +246,20 @@ func readConfig() *config { panic(fmt.Errorf("Unable to decode Config: %s \n", err)) } + // Check if we have contacts to message + if len(strings.TrimSpace(config.Contacts)) == 0 { + log.Panicln("You appear to have no contacts to message !") + } + // Check logging + config.LogStanzas[logFilePath] = path.Clean(config.LogStanzas[logFilePath]) + on, err := strconv.ParseBool(config.LogStanzas[logStanzasOn]) + if err != nil { + log.Panicln(err) + } + if d, e := isDirectory(config.LogStanzas[logFilePath]); (e != nil || !d) && on { + log.Panicln("The log file path could not be found or is not a directory.") + } + return &config } @@ -203,45 +272,19 @@ func errorHandler(err error) { // Read the client roster from the config. This does not check with the server that the roster is correct. // If user tries to send a message to someone not registered with the server, the server will return an error. func updateRosterFromConfig(g *gocui.Gui, config *config) { - g.Update(func(g *gocui.Gui) error { - menu, _ := g.View(menuWindow) - for _, contact := range strings.Split(config.Contacts, configContactSep) { - fmt.Fprintln(menu, contact) - } - return nil - }) + viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts) } // Updates the menu panel of the view with the current user's roster. // Need to add support for Roster IQ stanzas to make this work. func askForRoster(client *xmpp.Client, g *gocui.Gui) { - //ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) - //iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: currentUserJid, To: "localhost", Lang: "en"}) - //disco := iqReq.DiscoInfo() - //iqReq.Payload = disco - // - //// Handle a possible error - //errChan := make(chan error) - //errorHandler := func(err error) { - // errChan <- err - //} - //client.ErrorHandler = errorHandler - //res, err := client.SendIQ(ctx, iqReq) - //if err != nil { - // t.Errorf(err.Error()) - //} - // - //select { - //case <-res: - //} - - //roster := []string{"testuser1", "testuser2", "testuser3@localhost"} - // - //g.Update(func(g *gocui.Gui) error { - // menu, _ := g.View(menuWindow) - // for _, contact := range roster { - // fmt.Fprintln(menu, contact) - // } - // return nil - //}) + // Not implemented yet ! +} + +func isDirectory(path string) (bool, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return false, err + } + return fileInfo.IsDir(), err } From 390336b89472dc4efe8faa27c0b0f85f03a81160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Mon, 23 Dec 2019 09:04:18 +0100 Subject: [PATCH 22/54] Added Roster IQs Added an overly primitive "disconnect" for the client to use in the chat client example --- _examples/xmpp_chat_client/interface.go | 30 ++++- .../xmpp_chat_client/xmpp_chat_client.go | 19 ++- client.go | 7 +- component.go | 1 - stanza/iq_disco.go | 4 +- stanza/iq_disco_test.go | 2 +- stanza/iq_roster.go | 115 ++++++++++++++++++ stanza/iq_roster_test.go | 109 +++++++++++++++++ stanza/stream.go | 2 + 9 files changed, 278 insertions(+), 11 deletions(-) create mode 100644 stanza/iq_roster.go create mode 100644 stanza/iq_roster_test.go diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index 84c2ed6..6437da5 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -15,6 +15,7 @@ const ( rawInputWindow = "rw" // Where raw stanzas are written contactsListWindow = "cl" // Where the contacts list is shown, and contacts are selectable menuWindow = "mw" // Where the menu is shown + disconnectMsg = "msg" // Menu options disconnect = "Disconnect" @@ -188,6 +189,12 @@ func setKeyBindings(g *gocui.Gui) { log.Panicln(err) } + // ========================== + // Disconnect message + if err := g.SetKeybinding(disconnectMsg, gocui.KeyEnter, gocui.ModNone, delMsg); err != nil { + log.Panicln(err) + } + } // General @@ -209,7 +216,20 @@ func getLine(g *gocui.Gui, v *gocui.View) error { if len(cv.ViewBufferLines()) == 0 { printContactsToWindow(g, viewState.contacts) } - } else if l == disconnect || l == askServerForRoster { + } else if l == disconnect { + maxX, maxY := g.Size() + msg := "You disconnected from the server. Press enter to quit." + if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil { + if !gocui.IsUnknownView(err) { + return err + } + fmt.Fprintln(v, msg) + if _, err := g.SetCurrentView(disconnectMsg); err != nil { + return err + } + } + killChan <- disconnectErr + } else if l == askServerForRoster { chlw, _ := g.View(chatLogWindow) fmt.Fprintln(chlw, infoFormat+" Not yet implemented !") } else if l == rawMode { @@ -326,3 +346,11 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error { } return nil } + +func delMsg(g *gocui.Gui, v *gocui.View) error { + if err := g.DeleteView(disconnectMsg); err != nil { + return err + } + errChan <- gocui.ErrQuit // Quit the program + return nil +} diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 3904e2f..d28c124 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -6,6 +6,7 @@ xmpp_chat_client is a demo client that connect on an XMPP server to chat with ot import ( "encoding/xml" + "errors" "flag" "fmt" "github.com/awesome-gocui/gocui" @@ -40,10 +41,11 @@ var ( CorrespChan = make(chan string, 1) textChan = make(chan string, 5) rawTextChan = make(chan string, 5) - killChan = make(chan struct{}, 1) + killChan = make(chan error, 1) errChan = make(chan error) - logger *log.Logger + logger *log.Logger + disconnectErr = errors.New("disconnecting client") ) type config struct { @@ -160,7 +162,7 @@ func startClient(g *gocui.Gui, config *config) { router.HandleFunc("message", handlerWithGui) if client, err = xmpp.NewClient(clientCfg, router, errorHandler); err != nil { - panic(fmt.Sprintf("Could not create a new client ! %s", err)) + log.Panicln(fmt.Sprintf("Could not create a new client ! %s", err)) } @@ -196,7 +198,13 @@ func startMessaging(client xmpp.Sender, config *config) { var correspondent string for { select { - case <-killChan: + case err := <-killChan: + if err == disconnectErr { + sc := client.(xmpp.StreamClient) + sc.Disconnect() + } else { + logger.Println(err) + } return case text = <-textChan: reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, From: config.Client[clientJid], Type: stanza.MessageTypeChat}, Body: text} @@ -265,8 +273,7 @@ func readConfig() *config { // If an error occurs, this is used to kill the client func errorHandler(err error) { - fmt.Printf("%v", err) - killChan <- struct{}{} + killChan <- err } // Read the client roster from the config. This does not check with the server that the roster is correct. diff --git a/client.go b/client.go index 254a793..1c5ea22 100644 --- a/client.go +++ b/client.go @@ -206,7 +206,12 @@ func (c *Client) Resume(state SMState) error { } func (c *Client) Disconnect() { - // TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect + // TODO : Wait for server response for clean disconnect + presence := stanza.NewPresence(stanza.Attrs{From: c.config.Jid}) + presence.Type = stanza.PresenceTypeUnavailable + c.Send(presence) + c.SendRaw(stanza.StreamClose) + if c.transport != nil { _ = c.transport.Close() } diff --git a/component.go b/component.go index 8b96240..828ba07 100644 --- a/component.go +++ b/component.go @@ -111,7 +111,6 @@ func (c *Component) Resume(sm SMState) error { c.updateState(StatePermanentError) return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true) } - return err } func (c *Component) Disconnect() { diff --git a/stanza/iq_disco.go b/stanza/iq_disco.go index cc94756..7b9acac 100644 --- a/stanza/iq_disco.go +++ b/stanza/iq_disco.go @@ -8,6 +8,7 @@ import ( // Disco Info const ( + // NSDiscoInfo defines the namespace for disco IQ stanzas NSDiscoInfo = "http://jabber.org/protocol/disco#info" ) @@ -21,6 +22,7 @@ type DiscoInfo struct { Features []Feature `xml:"feature"` } +// Namespace lets DiscoInfo implement the IQPayload interface func (d *DiscoInfo) Namespace() string { return d.XMLName.Space } @@ -112,7 +114,7 @@ func (d *DiscoItems) Namespace() string { // DiscoItems builds a default DiscoItems payload func (iq *IQ) DiscoItems() *DiscoItems { d := DiscoItems{ - XMLName: xml.Name{Space: "http://jabber.org/protocol/disco#items", Local: "query"}, + XMLName: xml.Name{Space: NSDiscoItems, Local: "query"}, } iq.Payload = &d return &d diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go index d659cde..012952e 100644 --- a/stanza/iq_disco_test.go +++ b/stanza/iq_disco_test.go @@ -50,7 +50,7 @@ func TestDiscoInfo_Builder(t *testing.T) { // Implements XEP-0030 example 17 // https://xmpp.org/extensions/xep-0030.html#example-17 func TestDiscoItems_Builder(t *testing.T) { - iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "catalog.shakespeare.lit", + iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit", To: "romeo@montague.net/orchard", Id: "items-2"}) iq.DiscoItems(). AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare"). diff --git a/stanza/iq_roster.go b/stanza/iq_roster.go new file mode 100644 index 0000000..1923013 --- /dev/null +++ b/stanza/iq_roster.go @@ -0,0 +1,115 @@ +package stanza + +import ( + "encoding/xml" +) + +// ============================================================================ +// Roster + +const ( + // NSRoster is the Roster IQ namespace + NSRoster = "jabber:iq:roster" + // SubscriptionNone indicates the user does not have a subscription to + // the contact's presence, and the contact does not have a subscription + // to the user's presence; this is the default value, so if the subscription + // attribute is not included then the state is to be understood as "none" + SubscriptionNone = "none" + + // SubscriptionTo indicates the user has a subscription to the contact's + // presence, but the contact does not have a subscription to the user's presence. + SubscriptionTo = "to" + + // SubscriptionFrom indicates the contact has a subscription to the user's + // presence, but the user does not have a subscription to the contact's presence + SubscriptionFrom = "from" + + // SubscriptionBoth indicates the user and the contact have subscriptions to each + // other's presence (also called a "mutual subscription") + SubscriptionBoth = "both" +) + +// ---------- +// Namespaces + +// Roster struct represents Roster IQs +type Roster struct { + XMLName xml.Name `xml:"jabber:iq:roster query"` +} + +// Namespace defines the namespace for the RosterIQ +func (r *Roster) Namespace() string { + return r.XMLName.Space +} + +// --------------- +// Builder helpers + +// RosterIQ builds a default Roster payload +func (iq *IQ) RosterIQ() *Roster { + r := Roster{ + XMLName: xml.Name{ + Space: NSRoster, + Local: "query", + }, + } + iq.Payload = &r + return &r +} + +// ----------- +// SubElements + +// RosterItems represents the list of items in a roster IQ +type RosterItems struct { + XMLName xml.Name `xml:"jabber:iq:roster query"` + Items []RosterItem `xml:"item"` +} + +// Namespace lets RosterItems implement the IQPayload interface +func (r *RosterItems) Namespace() string { + return r.XMLName.Space +} + +// RosterItem represents an item in the roster iq +type RosterItem struct { + XMLName xml.Name `xml:"jabber:iq:roster item"` + Jid string `xml:"jid,attr"` + Ask string `xml:"ask,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Subscription string `xml:"subscription,attr,omitempty"` + Groups []string `xml:"group"` +} + +// --------------- +// Builder helpers + +// RosterItems builds a default RosterItems payload +func (iq *IQ) RosterItems() *RosterItems { + ri := RosterItems{ + XMLName: xml.Name{Space: "jabber:iq:roster", Local: "query"}, + } + iq.Payload = &ri + return &ri +} + +// AddItem builds an item and ads it to the roster IQ +func (r *RosterItems) AddItem(jid, subscription, ask, name string, groups []string) *RosterItems { + item := RosterItem{ + Jid: jid, + Name: name, + Groups: groups, + Subscription: subscription, + Ask: ask, + } + r.Items = append(r.Items, item) + return r +} + +// ============================================================================ +// Registry init + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, Roster{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSRoster, Local: "query"}, RosterItems{}) +} diff --git a/stanza/iq_roster_test.go b/stanza/iq_roster_test.go new file mode 100644 index 0000000..7228084 --- /dev/null +++ b/stanza/iq_roster_test.go @@ -0,0 +1,109 @@ +package stanza + +import ( + "encoding/xml" + "reflect" + "testing" +) + +func TestRosterBuilder(t *testing.T) { + iq := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"}) + var noGroup []string + + iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com", + SubscriptionBoth, + "", + "xl8ceaw", + []string{"0flucpm8i2jtrjhxw01uf1nd2", + "bm2bajg9ex4e1swiuju9i9nu5", + "rvjpanomi4ejpx42fpmffoac0"}). + AddItem("9aynsym60zbu78jbdvpho7s68@crypho.com", + SubscriptionBoth, + "", + "9aynsym60", + []string{"mzaoy73i6ra5k502182zi1t97"}). + AddItem("admin@crypho.com", + SubscriptionBoth, + "", + "admin", + noGroup) + + parsedIQ, err := checkMarshalling(t, iq) + if err != nil { + return + } + + // Check result + pp, ok := parsedIQ.Payload.(*RosterItems) + if !ok { + t.Errorf("Parsed stanza does not contain correct IQ payload") + } + + // Check items + items := []RosterItem{ + { + XMLName: xml.Name{}, + Name: "xl8ceaw", + Ask: "", + Jid: "xl8ceawrfu8zdneomw1h6h28d@crypho.com", + Subscription: SubscriptionBoth, + Groups: []string{"0flucpm8i2jtrjhxw01uf1nd2", + "bm2bajg9ex4e1swiuju9i9nu5", + "rvjpanomi4ejpx42fpmffoac0"}, + }, + { + XMLName: xml.Name{}, + Name: "9aynsym60", + Ask: "", + Jid: "9aynsym60zbu78jbdvpho7s68@crypho.com", + Subscription: SubscriptionBoth, + Groups: []string{"mzaoy73i6ra5k502182zi1t97"}, + }, + { + XMLName: xml.Name{}, + Name: "admin", + Ask: "", + Jid: "admin@crypho.com", + Subscription: SubscriptionBoth, + Groups: noGroup, + }, + } + if len(pp.Items) != len(items) { + t.Errorf("Items length mismatch: %#v", pp.Items) + } else { + for i, item := range pp.Items { + if item.Jid != items[i].Jid { + t.Errorf("JID Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + } + if !reflect.DeepEqual(item.Groups, items[i].Groups) { + t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + } + if item.Name != items[i].Name { + t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + } + if item.Ask != items[i].Ask { + t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + } + if item.Subscription != items[i].Subscription { + t.Errorf("Name Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + } + } + } +} + +func checkMarshalling(t *testing.T, iq IQ) (*IQ, error) { + // Marshall + data, err := xml.Marshal(iq) + if err != nil { + t.Errorf("cannot marshal iq: %s\n%#v", err, iq) + return nil, err + } + + // Unmarshall + var parsedIQ IQ + err = xml.Unmarshal(data, &parsedIQ) + if err != nil { + t.Errorf("Unmarshal returned error: %s\n%s", err, data) + } + return &parsedIQ, err +} diff --git a/stanza/stream.go b/stanza/stream.go index 203cc83..6ab4bad 100644 --- a/stanza/stream.go +++ b/stanza/stream.go @@ -12,3 +12,5 @@ type Stream struct { Id string `xml:"id,attr"` Version string `xml:"version,attr"` } + +const StreamClose = "" From f3252346c454eeb736a3f7c60cfb50bb710b1286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Mon, 23 Dec 2019 10:05:27 +0100 Subject: [PATCH 23/54] Added roster update to chat client example --- _examples/xmpp_chat_client/go.mod | 2 +- _examples/xmpp_chat_client/interface.go | 15 ++++-- .../xmpp_chat_client/xmpp_chat_client.go | 51 ++++++++++++++++--- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/_examples/xmpp_chat_client/go.mod b/_examples/xmpp_chat_client/go.mod index 8d510f6..267b416 100644 --- a/_examples/xmpp_chat_client/go.mod +++ b/_examples/xmpp_chat_client/go.mod @@ -6,5 +6,5 @@ require ( github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.1 - gosrc.io/xmpp v0.3.1-0.20191212145100-27130d72926b + gosrc.io/xmpp v0.3.1-0.20191223080939-f8f820170e08 ) diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index 15a29eb..0919709 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -9,7 +9,7 @@ import ( ) const ( -// Windows + // Windows chatLogWindow = "clw" // Where (received and sent) messages are logged chatInputWindow = "iw" // Where messages are written rawInputWindow = "rw" // Where raw stanzas are written @@ -71,7 +71,11 @@ func layout(g *gocui.Gui) error { } v.Title = "Contacts" v.Wrap = true - v.Autoscroll = true + // If we set this to true, the contacts list will "fit" in the window but if the number + // of contacts exceeds the maximum height, some contacts will be hidden... + // If set to false, we can scroll up and down the contact list... infinitely. Meaning lower lines + // will be unlimited and empty... Didn't find a way to quickfix yet. + v.Autoscroll = false } if v, err := g.SetView(menuWindow, 0, 0, maxX/5-1, 5*maxY/6-2, 0); err != nil { @@ -215,7 +219,7 @@ func getLine(g *gocui.Gui, v *gocui.View) error { if len(cv.ViewBufferLines()) == 0 { printContactsToWindow(g, viewState.contacts) } - } else if l == disconnect { + } else if l == disconnect { maxX, maxY := g.Size() msg := "You disconnected from the server. Press enter to quit." if v, err := g.SetView(disconnectMsg, maxX/2-30, maxY/2, maxX/2-29+len(msg), maxY/2+2, 0); err != nil { @@ -226,11 +230,12 @@ func getLine(g *gocui.Gui, v *gocui.View) error { if _, err := g.SetCurrentView(disconnectMsg); err != nil { return err } - } + } killChan <- disconnectErr } else if l == askServerForRoster { chlw, _ := g.View(chatLogWindow) - fmt.Fprintln(chlw, infoFormat+" Not yet implemented !") + fmt.Fprintln(chlw, infoFormat+"Asking server for contacts list...") + rosterChan <- struct{}{} } else if l == rawMode { mw, _ := g.View(menuWindow) viewState.input = rawInputWindow diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index d28c124..1e156d3 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -5,6 +5,7 @@ xmpp_chat_client is a demo client that connect on an XMPP server to chat with ot */ import ( + "context" "encoding/xml" "errors" "flag" @@ -19,6 +20,7 @@ import ( "path" "strconv" "strings" + "time" ) const ( @@ -43,6 +45,7 @@ var ( rawTextChan = make(chan string, 5) killChan = make(chan error, 1) errChan = make(chan error) + rosterChan = make(chan struct{}) logger *log.Logger disconnectErr = errors.New("disconnecting client") @@ -182,7 +185,6 @@ func startClient(g *gocui.Gui, config *config) { // ========================== // Start working - //askForRoster(client, g) updateRosterFromConfig(g, config) // Sending the default contact in a channel. Default value is the first contact in the list from the config. viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0] @@ -190,10 +192,10 @@ func startClient(g *gocui.Gui, config *config) { clw, _ := g.View(chatLogWindow) fmt.Fprintf(clw, infoFormat+"Now sending messages to "+viewState.currentContact+" in a private conversation\n") CorrespChan <- viewState.currentContact - startMessaging(client, config) + startMessaging(client, config, g) } -func startMessaging(client xmpp.Sender, config *config) { +func startMessaging(client xmpp.Sender, config *config, g *gocui.Gui) { var text string var correspondent string for { @@ -228,6 +230,8 @@ func startMessaging(client xmpp.Sender, config *config) { } case crrsp := <-CorrespChan: correspondent = crrsp + case <-rosterChan: + askForRoster(client, g, config) } } @@ -282,10 +286,43 @@ func updateRosterFromConfig(g *gocui.Gui, config *config) { viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts) } -// Updates the menu panel of the view with the current user's roster. -// Need to add support for Roster IQ stanzas to make this work. -func askForRoster(client *xmpp.Client, g *gocui.Gui) { - // Not implemented yet ! +// Updates the menu panel of the view with the current user's roster, by asking the server. +func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) { + // Craft a roster request + req := stanza.NewIQ(stanza.Attrs{From: config.Client[clientJid], Type: stanza.IQTypeGet}) + req.RosterItems() + if logger != nil { + m, _ := xml.Marshal(req) + logger.Println(string(m)) + } + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + + // Send the roster request to the server + c, err := client.SendIQ(ctx, req) + if err != nil { + logger.Panicln(err) + } + + // Sending a IQ has a channel spawned to process the response once we receive it. + // In order not to block the client, we spawn a goroutine to update the TUI once the server has responded. + go func() { + serverResp := <-c + if logger != nil { + m, _ := xml.Marshal(serverResp) + logger.Println(string(m)) + } + // Update contacts with the response from the server + chlw, _ := g.View(chatLogWindow) + if rosterItems, ok := serverResp.Payload.(*stanza.RosterItems); ok { + viewState.contacts = []string{} + for _, item := range rosterItems.Items { + viewState.contacts = append(viewState.contacts, item.Jid) + } + fmt.Fprintln(chlw, infoFormat+"Contacts list updated !") + return + } + fmt.Fprintln(chlw, infoFormat+"Failed to update contact list !") + }() } func isDirectory(path string) (bool, error) { From ccc573c3b28af7493da505ef2a40146c661e5cf3 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Mon, 23 Dec 2019 09:25:39 +0000 Subject: [PATCH 24/54] Update xmpp_chat_client.go Quickfix : go back to menu when contacts are updated from server. --- _examples/xmpp_chat_client/xmpp_chat_client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 1e156d3..b13e398 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -318,6 +318,7 @@ func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) { for _, item := range rosterItems.Items { viewState.contacts = append(viewState.contacts, item.Jid) } + viewState.contacts = append(viewState.contacts, backFromContacts) fmt.Fprintln(chlw, infoFormat+"Contacts list updated !") return } From daf37cf5a88e69dddf4011aa064dbc27a9e4fc36 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Tue, 24 Dec 2019 10:44:01 +0000 Subject: [PATCH 25/54] Update Disconnect method on client Remove wrong stanza sends --- client.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client.go b/client.go index 1c5ea22..f75985a 100644 --- a/client.go +++ b/client.go @@ -207,11 +207,6 @@ func (c *Client) Resume(state SMState) error { func (c *Client) Disconnect() { // TODO : Wait for server response for clean disconnect - presence := stanza.NewPresence(stanza.Attrs{From: c.config.Jid}) - presence.Type = stanza.PresenceTypeUnavailable - c.Send(presence) - c.SendRaw(stanza.StreamClose) - if c.transport != nil { _ = c.transport.Close() } From e62b7fa0c7f52e548ae67343fa1741dd30e7bee2 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Tue, 24 Dec 2019 10:47:25 +0000 Subject: [PATCH 26/54] Update client.go --- client.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client.go b/client.go index f75985a..30c9d7e 100644 --- a/client.go +++ b/client.go @@ -207,6 +207,10 @@ func (c *Client) Resume(state SMState) error { func (c *Client) Disconnect() { // TODO : Wait for server response for clean disconnect + presence := stanza.NewPresence(stanza.Attrs{From: c.config.Jid}) + presence.Type = stanza.PresenceTypeUnavailable + c.Send(presence) + c.SendRaw(stanza.StreamClose) if c.transport != nil { _ = c.transport.Close() } From 94aceac802e51f58710300bd94d33566bb3b236f Mon Sep 17 00:00:00 2001 From: remicorniere Date: Thu, 26 Dec 2019 13:47:02 +0000 Subject: [PATCH 27/54] Changed "Disconnect" to wait for the closing stream tag. (#141) Updated example with a README.md and fixed some logs. --- _examples/xmpp_chat_client/README.md | 51 ++++++++++++++ _examples/xmpp_chat_client/config.yml | 4 +- _examples/xmpp_chat_client/interface.go | 17 +++-- .../xmpp_chat_client/xmpp_chat_client.go | 10 ++- client.go | 40 ++++++++--- client_test.go | 40 +++++++++-- component.go | 12 +++- stanza/parser.go | 66 +++++++++++++++---- stanza/stream_features.go | 18 +++++ stream_manager.go | 2 +- transport.go | 3 + websocket_transport.go | 8 ++- xmpp_transport.go | 33 +++++++--- 13 files changed, 252 insertions(+), 52 deletions(-) create mode 100644 _examples/xmpp_chat_client/README.md diff --git a/_examples/xmpp_chat_client/README.md b/_examples/xmpp_chat_client/README.md new file mode 100644 index 0000000..d4db360 --- /dev/null +++ b/_examples/xmpp_chat_client/README.md @@ -0,0 +1,51 @@ +# Chat TUI example +This is a simple chat example, with a TUI. +It shows the library usage and a few of its capabilities. +## How to run +### Build +You can build the client using : +``` + go build -o example_client +``` +and then run with (on unix for example): +``` + ./example_client +``` +or you can simply build + run in one command while at the example directory root, like this: +``` + go run xmpp_chat_client.go interface.go +``` + +### Configuration +The example needs a configuration file to run. A sample file is provided. +By default, the example will look for a file named "config" in the current directory. +To provide a different configuration file, pass the following argument to the example : +``` + go run xmpp_chat_client.go interface.go -c /path/to/config +``` +where /path/to/config is the path to the directory containing the configuration file. The configuration file must be named +"config" and be using the yaml format. + +Required fields are : +```yaml +Server : + - full_address: "localhost:5222" +Client : # This is you + - jid: "testuser2@localhost" + - pass: "pass123" #Password in a config file yay + +# Contacts list, ";" separated +Contacts : "testuser1@localhost;testuser3@localhost" +# Should we log stanzas ? +LogStanzas: + - logger_on: "true" + - logfile_path: "./logs" # Path to directory, not file. +``` + +## How to use +Shortcuts : + - ctrl+space : switch between input window and menu window. + - While in input window : + - enter : sends a message if in message mode (see menu options) + - ctrl+e : sends a raw stanza when in raw mode (see menu options) + - ctrl+c : quit \ No newline at end of file diff --git a/_examples/xmpp_chat_client/config.yml b/_examples/xmpp_chat_client/config.yml index ed6e902..6c6498b 100644 --- a/_examples/xmpp_chat_client/config.yml +++ b/_examples/xmpp_chat_client/config.yml @@ -1,9 +1,7 @@ -# Default config for the client +# Sample config for the client Server : - full_address: "localhost:5222" - - port: 5222 Client : - - name: "testuser2" - jid: "testuser2@localhost" - pass: "pass123" #Password in a config file yay diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index 0919709..0c05edd 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -17,6 +17,13 @@ const ( menuWindow = "mw" // Where the menu is shown disconnectMsg = "msg" + // Windows titles + chatLogWindowTitle = "Chat log" + menuWindowTitle = "Menu" + chatInputWindowTitle = "Write a message :" + rawInputWindowTitle = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :" + contactsListWindowTitle = "Contacts" + // Menu options disconnect = "Disconnect" askServerForRoster = "Ask server for roster" @@ -60,7 +67,7 @@ func layout(g *gocui.Gui) error { if !gocui.IsUnknownView(err) { return err } - v.Title = "Chat log" + v.Title = chatLogWindowTitle v.Wrap = true v.Autoscroll = true } @@ -69,7 +76,7 @@ func layout(g *gocui.Gui) error { if !gocui.IsUnknownView(err) { return err } - v.Title = "Contacts" + v.Title = contactsListWindowTitle v.Wrap = true // If we set this to true, the contacts list will "fit" in the window but if the number // of contacts exceeds the maximum height, some contacts will be hidden... @@ -82,7 +89,7 @@ func layout(g *gocui.Gui) error { if !gocui.IsUnknownView(err) { return err } - v.Title = "Menu" + v.Title = menuWindowTitle v.Wrap = true v.Autoscroll = true fmt.Fprint(v, strings.Join(menuOptions, "\n")) @@ -95,7 +102,7 @@ func layout(g *gocui.Gui) error { if !gocui.IsUnknownView(err) { return err } - v.Title = "Write or paste a raw stanza. Press \"Ctrl+E\" to send :" + v.Title = rawInputWindowTitle v.Editable = true v.Wrap = true } @@ -104,7 +111,7 @@ func layout(g *gocui.Gui) error { if !gocui.IsUnknownView(err) { return err } - v.Title = "Write a message :" + v.Title = chatInputWindowTitle v.Editable = true v.Wrap = true diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index b13e398..0d4b94b 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -63,7 +63,7 @@ func main() { // ============================================================ // Parse the flag with the config directory path as argument flag.String("c", defaultConfigFilePath, "Provide a path to the directory that contains the configuration"+ - " file you want to use. Config file should be named \"config\" and be of YAML format..") + " file you want to use. Config file should be named \"config\" and be in YAML format..") pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() @@ -139,7 +139,8 @@ func startClient(g *gocui.Gui, config *config) { handlerWithGui := func(_ xmpp.Sender, p stanza.Packet) { msg, ok := p.(stanza.Message) if logger != nil { - logger.Println(msg) + m, _ := xml.Marshal(msg) + logger.Println(string(m)) } v, err := g.View(chatLogWindow) @@ -209,7 +210,7 @@ func startMessaging(client xmpp.Sender, config *config, g *gocui.Gui) { } return case text = <-textChan: - reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, From: config.Client[clientJid], Type: stanza.MessageTypeChat}, Body: text} + reply := stanza.Message{Attrs: stanza.Attrs{To: correspondent, Type: stanza.MessageTypeChat}, Body: text} if logger != nil { raw, _ := xml.Marshal(reply) logger.Println(string(raw)) @@ -284,6 +285,8 @@ func errorHandler(err error) { // If user tries to send a message to someone not registered with the server, the server will return an error. func updateRosterFromConfig(g *gocui.Gui, config *config) { viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts) + // Put a "go back" button at the end of the list + viewState.contacts = append(viewState.contacts, backFromContacts) } // Updates the menu panel of the view with the current user's roster, by asking the server. @@ -318,6 +321,7 @@ func askForRoster(client xmpp.Sender, g *gocui.Gui, config *config) { for _, item := range rosterItems.Items { viewState.contacts = append(viewState.contacts, item.Jid) } + // Put a "go back" button at the end of the list viewState.contacts = append(viewState.contacts, backFromContacts) fmt.Fprintln(chlw, infoFormat+"Contacts list updated !") return diff --git a/client.go b/client.go index 30c9d7e..be15540 100644 --- a/client.go +++ b/client.go @@ -154,7 +154,8 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e if config.TransportConfiguration.Domain == "" { config.TransportConfiguration.Domain = config.parsedJid.Domain } - c.transport = NewClientTransport(config.TransportConfiguration) + c.config.TransportConfiguration.ConnectTimeout = c.config.ConnectTimeout + c.transport = NewClientTransport(c.config.TransportConfiguration) if config.StreamLogger != nil { c.transport.LogTraffic(config.StreamLogger) @@ -183,7 +184,24 @@ func (c *Client) Resume(state SMState) error { // Client is ok, we now open XMPP session if c.Session, err = NewSession(c.transport, c.config, state); err != nil { - c.transport.Close() + // Try to get the stream close tag from the server. + go func() { + for { + val, err := stanza.NextPacket(c.transport.GetDecoder()) + if err != nil { + c.ErrorHandler(err) + c.disconnected(state) + return + } + switch val.(type) { + case stanza.StreamClosePacket: + // TCP messages should arrive in order, so we can expect to get nothing more after this occurs + c.transport.ReceivedStreamClose() + return + } + } + }() + c.Disconnect() return err } c.Session.StreamId = streamId @@ -205,15 +223,12 @@ func (c *Client) Resume(state SMState) error { return err } -func (c *Client) Disconnect() { - // TODO : Wait for server response for clean disconnect - presence := stanza.NewPresence(stanza.Attrs{From: c.config.Jid}) - presence.Type = stanza.PresenceTypeUnavailable - c.Send(presence) - c.SendRaw(stanza.StreamClose) +func (c *Client) Disconnect() error { if c.transport != nil { - _ = c.transport.Close() + return c.transport.Close() } + // No transport so no connection. + return nil } func (c *Client) SetHandler(handler EventHandler) { @@ -294,7 +309,8 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { close(keepaliveQuit) c.streamError(packet.Error.Local, packet.Text) c.ErrorHandler(errors.New("stream error: " + packet.Error.Local)) - return + // We don't return here, because we want to wait for the stream close tag from the server, or timeout. + c.Disconnect() // Process Stream management nonzas case stanza.SMRequest: answer := stanza.SMAnswer{XMLName: xml.Name{ @@ -306,6 +322,10 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { c.ErrorHandler(err) return } + case stanza.StreamClosePacket: + // TCP messages should arrive in order, so we can expect to get nothing more after this occurs + c.transport.ReceivedStreamClose() + return default: state.Inbound++ } diff --git a/client_test.go b/client_test.go index 8d109d0..f455fdf 100644 --- a/client_test.go +++ b/client_test.go @@ -67,7 +67,10 @@ func TestClient_Connect(t *testing.T) { func TestClient_NoInsecure(t *testing.T) { // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerAbortTLS) + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + handlerAbortTLS(t, sc) + closeConn(t, sc) + }) // Test / Check result config := Config{ @@ -97,7 +100,10 @@ func TestClient_NoInsecure(t *testing.T) { func TestClient_FeaturesTracking(t *testing.T) { // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerAbortTLS) + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + handlerAbortTLS(t, sc) + closeConn(t, sc) + }) // Test / Check result config := Config{ @@ -247,6 +253,7 @@ func TestClient_SendRaw(t *testing.T) { handlerClientConnectSuccess(t, sc) discardPresence(t, sc) respondToIQ(t, sc) + closeConn(t, sc) done <- struct{}{} } type testCase struct { @@ -290,6 +297,7 @@ func TestClient_SendRaw(t *testing.T) { select { // We don't use the default "long" timeout here because waiting it out means passing the test. case <-time.After(100 * time.Millisecond): + c.Disconnect() case err = <-errChan: if err == nil && tcase.shouldErr { t.Errorf("Failed to get closing stream err") @@ -297,7 +305,6 @@ func TestClient_SendRaw(t *testing.T) { t.Errorf("This test is not supposed to err !") } } - c.transport.Close() select { case <-done: m.Stop() @@ -309,7 +316,10 @@ func TestClient_SendRaw(t *testing.T) { } func TestClient_Disconnect(t *testing.T) { - c, m := mockClientConnection(t, handlerClientConnectSuccess, testClientBasePort) + c, m := mockClientConnection(t, func(t *testing.T, sc *ServerConn) { + handlerClientConnectSuccess(t, sc) + closeConn(t, sc) + }, testClientBasePort) err := c.transport.Ping() if err != nil { t.Errorf("Could not ping but not disconnected yet") @@ -326,7 +336,10 @@ func TestClient_DisconnectStreamManager(t *testing.T) { // Init mock server // Setup Mock server mock := ServerMock{} - mock.Start(t, testXMPPAddress, handlerAbortTLS) + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + handlerAbortTLS(t, sc) + closeConn(t, sc) + }) // Test / Check result config := Config{ @@ -375,6 +388,23 @@ func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) { bind(t, sc) } +// closeConn closes the connection on request from the client +func closeConn(t *testing.T, sc *ServerConn) { + for { + cls, err := stanza.NextPacket(sc.decoder) + if err != nil { + t.Errorf("cannot read from socket: %s", err) + return + } + switch cls.(type) { + case stanza.StreamClosePacket: + fmt.Fprintf(sc.connection, stanza.StreamClose) + return + } + } + +} + // We expect client will abort on TLS func handlerAbortTLS(t *testing.T, sc *ServerConn) { checkClientOpenStream(t, sc) diff --git a/component.go b/component.go index 828ba07..bd85aa2 100644 --- a/component.go +++ b/component.go @@ -113,11 +113,13 @@ func (c *Component) Resume(sm SMState) error { } } -func (c *Component) Disconnect() { +func (c *Component) Disconnect() error { // TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect if c.transport != nil { - _ = c.transport.Close() + return c.transport.Close() } + // No transport so no connection. + return nil } func (c *Component) SetHandler(handler EventHandler) { @@ -126,7 +128,6 @@ func (c *Component) SetHandler(handler EventHandler) { // Receiver Go routine receiver func (c *Component) recv() { - for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { @@ -140,6 +141,11 @@ func (c *Component) recv() { c.router.route(c, val) c.streamError(p.Error.Local, p.Text) c.ErrorHandler(errors.New("stream error: " + p.Error.Local)) + // We don't return here, because we want to wait for the stream close tag from the server, or timeout. + c.Disconnect() + case stanza.StreamClosePacket: + // TCP messages should arrive in order, so we can expect to get nothing more after this occurs + c.transport.ReceivedStreamClose() return } c.router.route(c, val) diff --git a/stanza/parser.go b/stanza/parser.go index 75f78e7..b5f11cf 100644 --- a/stanza/parser.go +++ b/stanza/parser.go @@ -50,11 +50,20 @@ func InitStream(p *xml.Decoder) (sessionID string, err 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) + t, err := NextXmppToken(p) if err != nil { return nil, err } + if ee, ok := t.(xml.EndElement); ok { + return decodeStream(p, ee) + } + + // If not an end element, then must be a start + se, ok := t.(xml.StartElement) + if !ok { + return nil, errors.New("unknown token ") + } // Decode one of the top level XMPP namespace switch se.Name.Space { case NSStream: @@ -73,7 +82,29 @@ func NextPacket(p *xml.Decoder) (Packet, error) { } } -// Scan XML token stream to find next StartElement. +// NextXmppToken scans XML token stream to find next StartElement or stream EndElement. +// We need the EndElement scan, because we must register stream close tags +func NextXmppToken(p *xml.Decoder) (xml.Token, 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 + case xml.EndElement: + if t.Name.Space == NSStream && t.Name.Local == "stream" { + return t, nil + } + } + } +} + +// NextStart scans XML token stream to find next StartElement. func NextStart(p *xml.Decoder) (xml.StartElement, error) { for { t, err := p.Token() @@ -97,16 +128,29 @@ TODO: From all the decoder, we can return a pointer to the actual concrete type, */ // decodeStream will fully decode a stream packet -func decodeStream(p *xml.Decoder, se xml.StartElement) (Packet, error) { - switch se.Name.Local { - case "error": - return streamError.decode(p, se) - case "features": - return streamFeatures.decode(p, se) - default: - return nil, errors.New("unexpected XMPP packet " + - se.Name.Space + " <" + se.Name.Local + "/>") +func decodeStream(p *xml.Decoder, t xml.Token) (Packet, error) { + if se, ok := t.(xml.StartElement); ok { + switch se.Name.Local { + case "error": + return streamError.decode(p, se) + case "features": + return streamFeatures.decode(p, se) + default: + return nil, errors.New("unexpected XMPP packet " + + se.Name.Space + " <" + se.Name.Local + "/>") + } } + + if ee, ok := t.(xml.EndElement); ok { + if ee.Name.Local == "stream" { + return streamClose.decode(ee), nil + } + return nil, errors.New("unexpected XMPP packet " + + ee.Name.Space + " <" + ee.Name.Local + "/>") + } + + // Should not happen + return nil, errors.New("unexpected XML token ") } // decodeSASL decodes a packet related to SASL authentication. diff --git a/stanza/stream_features.go b/stanza/stream_features.go index 14358f0..d5bed5c 100644 --- a/stanza/stream_features.go +++ b/stanza/stream_features.go @@ -165,3 +165,21 @@ func (streamErrorDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamErr err := p.DecodeElement(&packet, &se) return packet, err } + +// ============================================================================ +// StreamClose "Packet" + +// This is just a closing tag and hold no information +type StreamClosePacket struct{} + +func (StreamClosePacket) Name() string { + return "stream:stream" +} + +type streamCloseDecoder struct{} + +var streamClose streamCloseDecoder + +func (streamCloseDecoder) decode(_ xml.EndElement) StreamClosePacket { + return StreamClosePacket{} +} diff --git a/stream_manager.go b/stream_manager.go index aebd8a4..18e1434 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -29,7 +29,7 @@ type StreamClient interface { Send(packet stanza.Packet) error SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) SendRaw(packet string) error - Disconnect() + Disconnect() error SetHandler(handler EventHandler) } diff --git a/transport.go b/transport.go index c6134fb..abf7f4a 100644 --- a/transport.go +++ b/transport.go @@ -40,6 +40,9 @@ type Transport interface { Read(p []byte) (n int, err error) Write(p []byte) (n int, err error) Close() error + // ReceivedStreamClose signals to the transport that a has been received and that the tcp connection + // should be closed. + ReceivedStreamClose() } // NewClientTransport creates a new Transport instance for clients. diff --git a/websocket_transport.go b/websocket_transport.go index 69c0183..7631fc8 100644 --- a/websocket_transport.go +++ b/websocket_transport.go @@ -18,7 +18,7 @@ const maxPacketSize = 32768 const pingTimeout = time.Duration(5) * time.Second -var ServerDoesNotSupportXmppOverWebsocket = errors.New("The websocket server does not support the xmpp subprotocol") +var ServerDoesNotSupportXmppOverWebsocket = errors.New("the websocket server does not support the xmpp subprotocol") // The decoder is expected to be initialized after connecting to a server. type WebsocketTransport struct { @@ -47,6 +47,7 @@ func (t *WebsocketTransport) Connect() (string, error) { wsConn, response, err := websocket.Dial(ctx, t.Config.Address, &websocket.DialOptions{ Subprotocols: []string{"xmpp"}, }) + if err != nil { return "", NewConnError(err, true) } @@ -177,3 +178,8 @@ func (t *WebsocketTransport) cleanup(code websocket.StatusCode) error { } return err } + +// ReceivedStreamClose is not used for websockets for now +func (t *WebsocketTransport) ReceivedStreamClose() { + return +} diff --git a/xmpp_transport.go b/xmpp_transport.go index 34b0d3e..6e1209f 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -24,6 +24,7 @@ type XMPPTransport struct { readWriter io.ReadWriter logFile io.Writer isSecure bool + closeChan chan stanza.StreamClosePacket } var componentStreamOpen = fmt.Sprintf("", stanza.NSComponent, stanza.NSStream) @@ -38,13 +39,14 @@ func (t *XMPPTransport) Connect() (string, error) { return "", NewConnError(err, true) } + t.closeChan = make(chan stanza.StreamClosePacket) t.readWriter = newStreamLogger(t.conn, t.logFile) t.decoder = xml.NewDecoder(bufio.NewReaderSize(t.readWriter, maxPacketSize)) t.decoder.CharsetReader = t.Config.CharsetReader return t.StartStream() } -func (t XMPPTransport) StartStream() (string, error) { +func (t *XMPPTransport) StartStream() (string, error) { if _, err := fmt.Fprintf(t, t.openStatement, t.Config.Domain); err != nil { t.Close() return "", NewConnError(err, true) @@ -58,19 +60,19 @@ func (t XMPPTransport) StartStream() (string, error) { return sessionID, nil } -func (t XMPPTransport) DoesStartTLS() bool { +func (t *XMPPTransport) DoesStartTLS() bool { return true } -func (t XMPPTransport) GetDomain() string { +func (t *XMPPTransport) GetDomain() string { return t.Config.Domain } -func (t XMPPTransport) GetDecoder() *xml.Decoder { +func (t *XMPPTransport) GetDecoder() *xml.Decoder { return t.decoder } -func (t XMPPTransport) IsSecure() bool { +func (t *XMPPTransport) IsSecure() bool { return t.isSecure } @@ -105,7 +107,7 @@ func (t *XMPPTransport) StartTLS() error { return nil } -func (t XMPPTransport) Ping() error { +func (t *XMPPTransport) Ping() error { n, err := t.conn.Write([]byte("\n")) if err != nil { return err @@ -116,24 +118,31 @@ func (t XMPPTransport) Ping() error { return nil } -func (t XMPPTransport) Read(p []byte) (n int, err error) { +func (t *XMPPTransport) Read(p []byte) (n int, err error) { if t.readWriter == nil { return 0, errors.New("cannot read: not connected, no readwriter") } return t.readWriter.Read(p) } -func (t XMPPTransport) Write(p []byte) (n int, err error) { +func (t *XMPPTransport) Write(p []byte) (n int, err error) { if t.readWriter == nil { return 0, errors.New("cannot write: not connected, no readwriter") } return t.readWriter.Write(p) } -func (t XMPPTransport) Close() error { +func (t *XMPPTransport) Close() error { if t.readWriter != nil { - _, _ = t.readWriter.Write([]byte("")) + _, _ = t.readWriter.Write([]byte(stanza.StreamClose)) } + + // Try to wait for the stream close tag from the server. After a timeout, disconnect anyway. + select { + case <-t.closeChan: + case <-time.After(time.Duration(t.Config.ConnectTimeout) * time.Second): + } + if t.conn != nil { return t.conn.Close() } @@ -143,3 +152,7 @@ func (t XMPPTransport) Close() error { func (t *XMPPTransport) LogTraffic(logFile io.Writer) { t.logFile = logFile } + +func (t *XMPPTransport) ReceivedStreamClose() { + t.closeChan <- stanza.StreamClosePacket{} +} From ab80709aeb198f2ed062744de9fb221db1e9db2e Mon Sep 17 00:00:00 2001 From: Jerome Sautret Date: Thu, 26 Dec 2019 15:05:34 +0100 Subject: [PATCH 28/54] Added xmpp_component2 example. --- _examples/go.sum | 104 ++++++++++++++++++++++++++++ _examples/xmpp_component2/README.md | 4 ++ _examples/xmpp_component2/main.go | 75 ++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 _examples/xmpp_component2/README.md create mode 100644 _examples/xmpp_component2/main.go diff --git a/_examples/go.sum b/_examples/go.sum index 19c4ace..286bc95 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -1,23 +1,55 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -25,14 +57,26 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -43,39 +87,86 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go= github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -86,22 +177,35 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= diff --git a/_examples/xmpp_component2/README.md b/_examples/xmpp_component2/README.md new file mode 100644 index 0000000..9ae4885 --- /dev/null +++ b/_examples/xmpp_component2/README.md @@ -0,0 +1,4 @@ +# xmpp_component2 + + +This program is an example of the simplest XMPP component: it connects to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response. diff --git a/_examples/xmpp_component2/main.go b/_examples/xmpp_component2/main.go new file mode 100644 index 0000000..6bde7eb --- /dev/null +++ b/_examples/xmpp_component2/main.go @@ -0,0 +1,75 @@ +package main + +/* + +Connect to an XMPP server using XEP 114 protocol, perform a discovery query on the server and print the response + +*/ + +import ( + "context" + "fmt" + "time" + + xmpp "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" +) + +const ( + domain = "mycomponent.localhost" + address = "build.vpn.p1:8888" +) + +// Init and return a component +func makeComponent() *xmpp.Component { + opts := xmpp.ComponentOptions{ + TransportConfiguration: xmpp.TransportConfiguration{ + Address: address, + Domain: domain, + }, + Domain: domain, + Secret: "secret", + } + router := xmpp.NewRouter() + c, err := xmpp.NewComponent(opts, router, handleError) + if err != nil { + panic(err) + } + return c +} + +func handleError(err error) { + fmt.Println(err.Error()) +} + +func main() { + c := makeComponent() + + // Connect Component to the server + fmt.Printf("Connecting to %v\n", address) + err := c.Connect() + if err != nil { + panic(err) + } + + // make a disco iq + iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, + From: domain, + To: "localhost", + Id: "my-iq1"}) + disco := iqReq.DiscoInfo() + iqReq.Payload = disco + + // res is the channel used to receive the result iq + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + res, _ := c.SendIQ(ctx, iqReq) + + select { + case iqResponse := <-res: + // Got response from server + fmt.Print(iqResponse.Payload) + case <-time.After(100 * time.Millisecond): + cancel() + panic("No iq response was received in time") + } +} From 6e2ba9ca57103f9455612663842575c03ef73e5c Mon Sep 17 00:00:00 2001 From: Jerome Sautret Date: Thu, 26 Dec 2019 15:51:19 +0100 Subject: [PATCH 29/54] Remove context leak warnings --- client_test.go | 10 ++++- component_test.go | 11 +++-- go.sum | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/client_test.go b/client_test.go index f455fdf..5af099a 100644 --- a/client_test.go +++ b/client_test.go @@ -170,7 +170,7 @@ func TestClient_SendIQ(t *testing.T) { } client, mock := mockClientConnection(t, h, testClientIqPort) - ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) disco := iqReq.DiscoInfo() iqReq.Payload = disco @@ -189,16 +189,20 @@ func TestClient_SendIQ(t *testing.T) { select { case <-res: // If the server responds with an IQ, we pass the test case err := <-errChan: // If the server sends an error, or there is a connection error + cancel() t.Fatal(err.Error()) case <-time.After(defaultChannelTimeout): // If we timeout + cancel() t.Fatal("Failed to receive response, to sent IQ, from mock server") } select { case <-done: mock.Stop() case <-time.After(defaultChannelTimeout): + cancel() t.Fatal("The mock server failed to finish its job !") } + cancel() } func TestClient_SendIQFail(t *testing.T) { @@ -214,7 +218,7 @@ func TestClient_SendIQFail(t *testing.T) { //================== // Create an IQ to send - ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) disco := iqReq.DiscoInfo() iqReq.Payload = disco @@ -242,8 +246,10 @@ func TestClient_SendIQFail(t *testing.T) { case <-done: mock.Stop() case <-time.After(defaultChannelTimeout): + cancel() t.Errorf("The mock server failed to finish its job !") } + cancel() } func TestClient_SendRaw(t *testing.T) { diff --git a/component_test.go b/component_test.go index f4d1a07..73d8947 100644 --- a/component_test.go +++ b/component_test.go @@ -5,11 +5,12 @@ import ( "encoding/xml" "errors" "fmt" - "github.com/google/uuid" - "gosrc.io/xmpp/stanza" "strings" "testing" "time" + + "github.com/google/uuid" + "gosrc.io/xmpp/stanza" ) // Tests are ran in parallel, so each test creating a server must use a different port so we do not get any @@ -129,7 +130,7 @@ func TestSendIq(t *testing.T) { //Connecting to a mock server, initialized with given port and handler function c, m := mockComponentConnection(t, testSendIqPort, h) - ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) disco := iqReq.DiscoInfo() iqReq.Payload = disco @@ -158,6 +159,7 @@ func TestSendIq(t *testing.T) { case <-time.After(defaultChannelTimeout): t.Errorf("The mock server failed to finish its job !") } + cancel() } // Checking that error handling is done properly client side when an invalid IQ is sent and the server responds in kind. @@ -170,7 +172,7 @@ func TestSendIqFail(t *testing.T) { //Connecting to a mock server, initialized with given port and handler function c, m := mockComponentConnection(t, testSendIqFailPort, h) - ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified @@ -204,6 +206,7 @@ func TestSendIqFail(t *testing.T) { case <-time.After(defaultChannelTimeout): t.Errorf("The mock server failed to finish its job !") } + cancel() } // Tests sending raw xml to the mock server. diff --git a/go.sum b/go.sum index ae38d07..38fd55b 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,55 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -27,14 +59,26 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -45,36 +89,83 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -85,22 +176,35 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= From 947fcf04320deb80b7ca314105002f44621a5b1e Mon Sep 17 00:00:00 2001 From: remicorniere Date: Thu, 9 Jan 2020 14:33:11 +0000 Subject: [PATCH 30/54] PubSub protocol support (#142) * PubSub protocol support Added support for : - XEP-0050 (Command)) - XEP-0060 (PubSub) - XEP-0004 (Forms) Fixed the NewClient function by adding parsing of the domain from the JID if no domain is provided in transport config. Updated xmpp_jukebox example * Delete useless pubsub errors * README.md update Fixed import in echo example * Typo * Fixed raw send on client example * Fixed jukebox example and added a README.md --- README.md | 17 +- _examples/delegation/delegation.go | 4 +- _examples/xmpp_chat_client/interface.go | 6 +- _examples/xmpp_echo/xmpp_echo.go | 6 +- _examples/xmpp_jukebox/README.md | 37 + _examples/xmpp_jukebox/xmpp_jukebox.go | 35 +- client.go | 11 +- cmd/fluuxmpp/send.go | 5 +- cmd/fluuxmpp/xmppmuc.go | 4 +- config.go | 3 +- stanza/commands.go | 136 ++++ stanza/commands_test.go | 53 ++ stanza/component_test.go | 2 +- stanza/error.go | 16 +- stanza/form.go | 67 ++ stanza/form_test.go | 107 +++ stanza/iq_disco_test.go | 4 +- stanza/iq_roster_test.go | 4 +- jid.go => stanza/jid.go | 10 +- jid_test.go => stanza/jid_test.go | 2 +- stanza/msg_pubsub_event.go | 214 ++++++ stanza/msg_pubsub_event_test.go | 162 +++++ stanza/pep.go | 2 +- stanza/pubsub.go | 370 +++++++++- stanza/pubsub_owner.go | 377 ++++++++++ stanza/pubsub_owner_test.go | 833 +++++++++++++++++++++ stanza/pubsub_test.go | 921 ++++++++++++++++++++++++ stanza/xmpp_test.go | 16 + tcp_server_mock.go | 2 +- 29 files changed, 3375 insertions(+), 51 deletions(-) create mode 100644 _examples/xmpp_jukebox/README.md create mode 100644 stanza/commands.go create mode 100644 stanza/commands_test.go create mode 100644 stanza/form.go create mode 100644 stanza/form_test.go rename jid.go => stanza/jid.go (87%) rename jid_test.go => stanza/jid_test.go (99%) create mode 100644 stanza/msg_pubsub_event.go create mode 100644 stanza/msg_pubsub_event_test.go create mode 100644 stanza/pubsub_owner.go create mode 100644 stanza/pubsub_owner_test.go create mode 100644 stanza/pubsub_test.go diff --git a/README.md b/README.md index 08b911c..62c6518 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,13 @@ config := xmpp.Config{ - [XEP-0355: Namespace Delegation](https://xmpp.org/extensions/xep-0355.html) - [XEP-0356: Privileged Entity](https://xmpp.org/extensions/xep-0356.html) +### Extensions + - [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html) + Note : "6.5.4 Returning Some Items" requires support for [XEP-0059: Result Set Management](https://xmpp.org/extensions/xep-0059.html), + and is therefore not supported yet. + - [XEP-0004: Data Forms](https://xmpp.org/extensions/xep-0004.html) + - [XEP-0050: Ad-Hoc Commands](https://xmpp.org/extensions/xep-0050.html) + ## Package overview ### Stanza subpackage @@ -108,15 +115,16 @@ func main() { Address: "localhost:5222", }, Jid: "test@localhost", - Credential: xmpp.Password("Test"), + Credential: xmpp.Password("test"), StreamLogger: os.Stdout, Insecure: true, + // TLSConfig: tls.Config{InsecureSkipVerify: true}, } router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -138,6 +146,11 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) { reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body} _ = s.Send(reply) } + +func errorHandler(err error) { + fmt.Println(err.Error()) +} + ``` ## Reference documentation diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index 81642c6..d07587a 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -171,7 +171,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { return } - pubsub, ok := forwardedIQ.Payload.(*stanza.PubSub) + pubsub, ok := forwardedIQ.Payload.(*stanza.PubSubGeneric) if !ok { // We only support pubsub delegation return @@ -180,7 +180,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { if pubsub.Publish.XMLName.Local == "publish" { // Prepare pubsub IQ reply iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) - payload := stanza.PubSub{ + payload := stanza.PubSubGeneric{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/pubsub", Local: "pubsub", diff --git a/_examples/xmpp_chat_client/interface.go b/_examples/xmpp_chat_client/interface.go index 0c05edd..9f2c758 100644 --- a/_examples/xmpp_chat_client/interface.go +++ b/_examples/xmpp_chat_client/interface.go @@ -136,7 +136,11 @@ func writeInput(g *gocui.Gui, v *gocui.View) error { input := strings.Join(v.ViewBufferLines(), "\n") fmt.Fprintln(chatLogWindow, "Me : ", input) - textChan <- input + if viewState.input == rawInputWindow { + rawTextChan <- input + } else { + textChan <- input + } v.Clear() v.EditDeleteToStartOfLine() diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go index 5654a2b..b6c6766 100644 --- a/_examples/xmpp_echo/xmpp_echo.go +++ b/_examples/xmpp_echo/xmpp_echo.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router) + client, err := xmpp.NewClient(config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } @@ -50,3 +50,7 @@ func handleMessage(s xmpp.Sender, p stanza.Packet) { reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: msg.Body} _ = s.Send(reply) } + +func errorHandler(err error) { + fmt.Println(err.Error()) +} diff --git a/_examples/xmpp_jukebox/README.md b/_examples/xmpp_jukebox/README.md new file mode 100644 index 0000000..9eeb3a4 --- /dev/null +++ b/_examples/xmpp_jukebox/README.md @@ -0,0 +1,37 @@ +# Jukebox example + +## Requirements +- You need mpg123 installed on your computer because the example runs it as a command : +[Official MPG123 website](https://mpg123.de/) +Most linux distributions have a package for it. +- You need a soundcloud ID to play a music from the website through mpg123. You currently cannot play music files with this example. +Your user ID is available in your account settings on the [soundcloud website](https://soundcloud.com/) +**One is provided for convenience.** +- You need a running jabber server. You can run your local instance of [ejabberd](https://www.ejabberd.im/) for example. +- You need a registered user on the running jabber server. + +## Run +You can edit the soundcloud ID in the example file with your own, or use the provided one : +```go +const scClientID = "dde6a0075614ac4f3bea423863076b22" +``` + +To run the example, build it with (while in the example directory) : +``` +go build xmpp_jukebox.go +``` + +then run it with (update the command arguments accordingly): +``` +./xmpp_jukebox -jid=MY_USERE@MY_DOMAIN/jukebox -password=MY_PASSWORD -address=MY_SERVER:MY_SERVER_PORT +``` +Make sure to have a resource, for instance "/jukebox", on your jid. + +Then you can send the following stanza to "MY_USERE@MY_DOMAIN/jukebox" (with the resource) to play a song (update the soundcloud URL accordingly) : +```xml + + + + + +``` diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index ce7ebc9..57137b8 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -3,6 +3,7 @@ package main import ( + "encoding/xml" "flag" "fmt" "log" @@ -19,7 +20,7 @@ import ( const scClientID = "dde6a0075614ac4f3bea423863076b22" func main() { - jid := flag.String("jid", "", "jukebok XMPP JID, resource is optional") + jid := flag.String("jid", "", "jukebok XMPP Jid, resource is optional") password := flag.String("password", "", "XMPP account password") address := flag.String("address", "", "If needed, XMPP server DNSName or IP and optional port (ie myserver:5222)") flag.Parse() @@ -48,7 +49,7 @@ func main() { handleMessage(s, p, player) }) router.NewRoute(). - Packet("message"). + Packet("iq"). HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { handleIQ(s, p, player) }) @@ -108,11 +109,29 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { } func sendUserTune(s xmpp.Sender, artist string, title string) { - tune := stanza.Tune{Artist: artist, Title: title} - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, 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) + rq, err := stanza.NewPublishItemRq("localhost", + "http://jabber.org/protocol/tune", + "", + stanza.Item{ + XMLName: xml.Name{Space: "http://jabber.org/protocol/tune", Local: "tune"}, + Any: &stanza.Node{ + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Local: "artist"}, + Content: artist, + }, + { + XMLName: xml.Name{Local: "title"}, + Content: title, + }, + }, + }, + }) + if err != nil { + fmt.Printf("failed to build the publish request : %s", err.Error()) + return + } + _ = s.Send(rq) } func playSCURL(p *mpg123.Player, rawURL string) { @@ -120,7 +139,7 @@ func playSCURL(p *mpg123.Player, rawURL string) { // TODO: Maybe we need to check the track itself to get the stream URL from reply ? url := soundcloud.FormatStreamURL(songID) - _ = p.Play(url) + _ = p.Play(strings.ReplaceAll(url, "YOUR_SOUNDCLOUD_CLIENTID", scClientID)) } // TODO diff --git a/client.go b/client.go index be15540..a795934 100644 --- a/client.go +++ b/client.go @@ -107,14 +107,14 @@ Setting up the client / Checking the parameters */ // NewClient generates a new XMPP client, based on Config passed as parameters. -// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the JID. +// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the Jid. // Default the port to 5222. func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { if config.KeepaliveInterval == 0 { config.KeepaliveInterval = time.Second * 30 } - // Parse JID - if config.parsedJid, err = NewJid(config.Jid); err != nil { + // Parse Jid + if config.parsedJid, err = stanza.NewJid(config.Jid); err != nil { err = errors.New("missing jid") return nil, NewConnError(err, true) } @@ -142,6 +142,11 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e } } } + if config.Domain == "" { + // Fallback to jid domain + config.Domain = config.parsedJid.Domain + } + c = new(Client) c.config = config c.router = r diff --git a/cmd/fluuxmpp/send.go b/cmd/fluuxmpp/send.go index 0942928..5a10c9e 100644 --- a/cmd/fluuxmpp/send.go +++ b/cmd/fluuxmpp/send.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "gosrc.io/xmpp/stanza" "os" "strings" "sync" @@ -48,7 +49,7 @@ func sendxmpp(cmd *cobra.Command, args []string) { wg.Add(1) // FIXME: Remove global variables - var mucsToLeave []*xmpp.Jid + var mucsToLeave []*stanza.Jid cm := xmpp.NewStreamManager(client, func(c xmpp.Sender) { defer wg.Done() @@ -57,7 +58,7 @@ func sendxmpp(cmd *cobra.Command, args []string) { if isMUCRecipient { for _, muc := range receiver { - jid, err := xmpp.NewJid(muc) + jid, err := stanza.NewJid(muc) if err != nil { log.WithField("muc", muc).Errorf("skipping invalid muc jid: %w", err) continue diff --git a/cmd/fluuxmpp/xmppmuc.go b/cmd/fluuxmpp/xmppmuc.go index 984b014..a00fdfc 100644 --- a/cmd/fluuxmpp/xmppmuc.go +++ b/cmd/fluuxmpp/xmppmuc.go @@ -7,7 +7,7 @@ import ( "gosrc.io/xmpp/stanza" ) -func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error { +func joinMUC(c xmpp.Sender, toJID *stanza.Jid) error { return c.Send(stanza.Presence{Attrs: stanza.Attrs{To: toJID.Full()}, Extensions: []stanza.PresExtension{ stanza.MucPresence{ @@ -16,7 +16,7 @@ func joinMUC(c xmpp.Sender, toJID *xmpp.Jid) error { }) } -func leaveMUCs(c xmpp.Sender, mucsToLeave []*xmpp.Jid) { +func leaveMUCs(c xmpp.Sender, mucsToLeave []*stanza.Jid) { for _, muc := range mucsToLeave { if err := c.Send(stanza.Presence{Attrs: stanza.Attrs{ To: muc.Full(), diff --git a/config.go b/config.go index da4d4ab..178da2e 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package xmpp import ( + "gosrc.io/xmpp/stanza" "os" "time" ) @@ -11,7 +12,7 @@ type Config struct { TransportConfiguration Jid string - parsedJid *Jid // For easier manipulation + parsedJid *stanza.Jid // For easier manipulation Credential Credential StreamLogger *os.File // Used for debugging Lang string // TODO: should default to 'en' diff --git a/stanza/commands.go b/stanza/commands.go new file mode 100644 index 0000000..5a3191f --- /dev/null +++ b/stanza/commands.go @@ -0,0 +1,136 @@ +package stanza + +import "encoding/xml" + +// Implements the XEP-0050 extension + +const ( + CommandActionCancel = "cancel" + CommandActionComplete = "complete" + CommandActionExecute = "execute" + CommandActionNext = "next" + CommandActionPrevious = "prev" + + CommandStatusCancelled = "canceled" + CommandStatusCompleted = "completed" + CommandStatusExecuting = "executing" + + CommandNoteTypeErr = "error" + CommandNoteTypeInfo = "info" + CommandNoteTypeWarn = "warn" +) + +type Command struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"` + + CommandElement CommandElement `xml:",any"` + + BadAction *struct{} `xml:"bad-action,omitempty"` + BadLocale *struct{} `xml:"bad-locale,omitempty"` + BadPayload *struct{} `xml:"bad-payload,omitempty"` + BadSessionId *struct{} `xml:"bad-sessionid,omitempty"` + MalformedAction *struct{} `xml:"malformed-action,omitempty"` + SessionExpired *struct{} `xml:"session-expired,omitempty"` + + // Attributes + Action string `xml:"action,attr,omitempty"` + Node string `xml:"node,attr"` + SessionId string `xml:"sessionid,attr,omitempty"` + Status string `xml:"status,attr,omitempty"` + Lang string `xml:"lang,attr,omitempty"` +} + +func (c *Command) Namespace() string { + return c.XMLName.Space +} + +type CommandElement interface { + Ref() string +} + +type Actions struct { + Prev *struct{} `xml:"prev,omitempty"` + Next *struct{} `xml:"next,omitempty"` + Complete *struct{} `xml:"complete,omitempty"` + + Execute string `xml:"execute,attr,omitempty"` +} + +func (a *Actions) Ref() string { + return "actions" +} + +type Note struct { + Text string `xml:",cdata"` + Type string `xml:"type,attr,omitempty"` +} + +func (n *Note) Ref() string { + return "note" +} + +func (n *Node) Ref() string { + return "node" +} + +func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + c.XMLName = start.Name + + // Extract packet attributes + for _, attr := range start.Attr { + if attr.Name.Local == "action" { + c.Action = attr.Value + } + if attr.Name.Local == "node" { + c.Node = attr.Value + } + if attr.Name.Local == "sessionid" { + c.SessionId = attr.Value + } + if attr.Name.Local == "status" { + c.Status = attr.Value + } + if attr.Name.Local == "lang" { + c.Lang = attr.Value + } + } + + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + // Decode sub-elements + var err error + switch tt.Name.Local { + + case "affiliations": + a := Actions{} + d.DecodeElement(&a, &tt) + c.CommandElement = &a + case "configure": + nt := Note{} + d.DecodeElement(&nt, &tt) + c.CommandElement = &nt + default: + n := Node{} + e := d.DecodeElement(&n, &tt) + _ = e + c.CommandElement = &n + if err != nil { + return err + } + } + + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} diff --git a/stanza/commands_test.go b/stanza/commands_test.go new file mode 100644 index 0000000..4cdee0f --- /dev/null +++ b/stanza/commands_test.go @@ -0,0 +1,53 @@ +package stanza_test + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "testing" +) + +func TestMarshalCommands(t *testing.T) { + input := "Available Servi" + + "ces<" + + "field xmlns=\"jabber:x:data\" var=\"service\">httpdoffoffononpostgresqloffoffon" + + "onjabberdoffoffonon" + + var c stanza.Command + err := xml.Unmarshal([]byte(input), &c) + + if err != nil { + t.Fatalf("failed to unmarshal initial input") + } + + data, err := xml.Marshal(c) + if err != nil { + t.Fatalf("failed to marshal unmarshalled input") + } + + if err := compareMarshal(input, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} diff --git a/stanza/component_test.go b/stanza/component_test.go index d02531e..648131c 100644 --- a/stanza/component_test.go +++ b/stanza/component_test.go @@ -67,7 +67,7 @@ func TestParsingDelegationIQ(t *testing.T) { return } if forwardedIQ.Payload != nil { - if pubsub, ok := forwardedIQ.Payload.(*PubSub); ok { + if pubsub, ok := forwardedIQ.Payload.(*PubSubGeneric); ok { node = pubsub.Publish.Node } } diff --git a/stanza/error.go b/stanza/error.go index 0f416e4..5f42018 100644 --- a/stanza/error.go +++ b/stanza/error.go @@ -3,6 +3,7 @@ package stanza import ( "encoding/xml" "strconv" + "strings" ) // ============================================================================ @@ -53,10 +54,19 @@ func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } textName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "text"} - if elt.XMLName == textName { + // TODO : change the pubsub handling ? It kind of dilutes the information + // Handles : 6.1.3.11 Node Has Moved for XEP-0060 (PubSubGeneric) + goneName := xml.Name{Space: "urn:ietf:params:xml:ns:xmpp-stanzas", Local: "gone"} + if elt.XMLName == textName || // Regular error text + elt.XMLName == goneName { // Gone text for pubsub x.Text = elt.Content - } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" { - x.Reason = elt.XMLName.Local + } else if elt.XMLName.Space == "urn:ietf:params:xml:ns:xmpp-stanzas" || + elt.XMLName.Space == "http://jabber.org/protocol/pubsub#errors" { + if strings.TrimSpace(x.Reason) != "" { + x.Reason = strings.Join([]string{elt.XMLName.Local}, ":") + } else { + x.Reason = elt.XMLName.Local + } } case xml.EndElement: diff --git a/stanza/form.go b/stanza/form.go new file mode 100644 index 0000000..b9a9932 --- /dev/null +++ b/stanza/form.go @@ -0,0 +1,67 @@ +package stanza + +import "encoding/xml" + +type FormType string + +const ( + FormTypeCancel = "cancel" + FormTypeForm = "form" + FormTypeResult = "result" + FormTypeSubmit = "submit" +) + +// See XEP-0004 and XEP-0068 +// Pointer semantics +type Form struct { + XMLName xml.Name `xml:"jabber:x:data x"` + Instructions []string `xml:"instructions"` + Title string `xml:"title,omitempty"` + Fields []Field `xml:"field,omitempty"` + Reported *FormItem `xml:"reported"` + Items []FormItem + Type string `xml:"type,attr"` +} + +type FormItem struct { + Fields []Field +} + +type Field struct { + XMLName xml.Name `xml:"field"` + Description string `xml:"desc,omitempty"` + Required *string `xml:"required"` + ValuesList []string `xml:"value"` + Options []Option `xml:"option,omitempty"` + Var string `xml:"var,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Label string `xml:"label,attr,omitempty"` +} + +func NewForm(fields []Field, formType string) *Form { + return &Form{ + Type: formType, + Fields: fields, + } +} + +type FieldType string + +const ( + FieldTypeBool = "boolean" + FieldTypeFixed = "fixed" + FieldTypeHidden = "hidden" + FieldTypeJidMulti = "jid-multi" + FieldTypeJidSingle = "jid-single" + FieldTypeListMulti = "list-multi" + FieldTypeListSingle = "list-single" + FieldTypeTextMulti = "text-multi" + FieldTypeTextPrivate = "text-private" + FieldTypeTextSingle = "text-Single" +) + +type Option struct { + XMLName xml.Name `xml:"option"` + Label string `xml:"label,attr,omitempty"` + ValuesList []string `xml:"value"` +} diff --git a/stanza/form_test.go b/stanza/form_test.go new file mode 100644 index 0000000..a68d88e --- /dev/null +++ b/stanza/form_test.go @@ -0,0 +1,107 @@ +package stanza + +import ( + "encoding/xml" + "strings" + "testing" +) + +const ( + formSubmit = "" + + "" + + "" + + "" + + "http://jabber.org/protocol/pubsub#node_config" + + "" + + "" + + "Princely Musings (Atom)" + + "" + + "" + + "1" + + "" + + "" + + "roster" + + "" + + "" + + "friends" + + "servants" + + "courtiers" + + "" + + "" + + "http://www.w3.org/2005/Atom" + + "" + + "" + + "headline" + + "" + + "" + + "" + + "" + + "" + + "" + + clientJid = "hamlet@denmark.lit/elsinore" + serviceJid = "pubsub.shakespeare.lit" + iqId = "config1" + serviceNode = "princely_musings" +) + +func TestMarshalFormSubmit(t *testing.T) { + formIQ := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet}) + formIQ.Payload = &PubSubOwner{ + OwnerUseCase: &ConfigureOwner{ + Node: serviceNode, + Form: &Form{ + Type: FormTypeSubmit, + Fields: []Field{ + {Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}}, + {Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}}, + {Var: "pubsub#access_model", ValuesList: []string{"roster"}}, + {Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}}, + {Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}}, + { + Var: "pubsub#notification_type", + Type: "list-single", + Label: "Specify the delivery style for event notifications", + ValuesList: []string{"headline"}, + Options: []Option{ + {ValuesList: []string{"normal"}}, + {ValuesList: []string{"headline"}}, + }, + }, + }, + }, + }, + } + b, err := xml.Marshal(formIQ.Payload) + if err != nil { + t.Fatalf("Could not marshal formIQ : %v", err) + } + + if strings.ReplaceAll(string(b), " ", "") != strings.ReplaceAll(formSubmit, " ", "") { + t.Fatalf("Expected formIQ and marshalled one are different.\nExepected : %s\nMarshalled : %s", formSubmit, string(b)) + } + +} + +func TestUnmarshalFormSubmit(t *testing.T) { + var f PubSubOwner + mErr := xml.Unmarshal([]byte(formSubmit), &f) + if mErr != nil { + t.Fatalf("failed to unmarshal formSubmit ! %s", mErr) + } + + data, err := xml.Marshal(&f) + if err != nil { + t.Fatalf("failed to marshal formSubmit") + } + + if strings.ReplaceAll(string(data), " ", "") != strings.ReplaceAll(formSubmit, " ", "") { + t.Fatalf("failed unmarshal/marshal for formSubmit : %s\n%s", string(data), formSubmit) + } +} diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go index 012952e..1f8ab8b 100644 --- a/stanza/iq_disco_test.go +++ b/stanza/iq_disco_test.go @@ -73,11 +73,11 @@ func TestDiscoItems_Builder(t *testing.T) { {xml.Name{}, "catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"}, {xml.Name{}, "catalog.shakespeare.lit", "music", "Music from the time of Shakespeare"}} if len(pp.Items) != len(items) { - t.Errorf("Items length mismatch: %#v", pp.Items) + t.Errorf("List length mismatch: %#v", pp.Items) } else { for i, item := range pp.Items { if item.JID != items[i].JID { - t.Errorf("JID Mismatch (expected: %s): %s", items[i].JID, item.JID) + t.Errorf("Jid Mismatch (expected: %s): %s", items[i].JID, item.JID) } if item.Node != items[i].Node { t.Errorf("Node Mismatch (expected: %s): %s", items[i].JID, item.JID) diff --git a/stanza/iq_roster_test.go b/stanza/iq_roster_test.go index 7228084..ca891df 100644 --- a/stanza/iq_roster_test.go +++ b/stanza/iq_roster_test.go @@ -69,11 +69,11 @@ func TestRosterBuilder(t *testing.T) { }, } if len(pp.Items) != len(items) { - t.Errorf("Items length mismatch: %#v", pp.Items) + t.Errorf("List length mismatch: %#v", pp.Items) } else { for i, item := range pp.Items { if item.Jid != items[i].Jid { - t.Errorf("JID Mismatch (expected: %s): %s", items[i].Jid, item.Jid) + t.Errorf("Jid Mismatch (expected: %s): %s", items[i].Jid, item.Jid) } if !reflect.DeepEqual(item.Groups, items[i].Groups) { t.Errorf("Node Mismatch (expected: %s): %s", items[i].Jid, item.Jid) diff --git a/jid.go b/stanza/jid.go similarity index 87% rename from jid.go rename to stanza/jid.go index cee292a..2fc9cc6 100644 --- a/jid.go +++ b/stanza/jid.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "fmt" @@ -20,9 +20,9 @@ func NewJid(sjid string) (*Jid, error) { } s1 := strings.SplitN(sjid, "@", 2) - if len(s1) == 1 { // This is a server or component JID + if len(s1) == 1 { // This is a server or component Jid jid.Domain = s1[0] - } else { // JID has a local username part + } else { // Jid has a local username part if s1[0] == "" { return jid, fmt.Errorf("invalid jid '%s", sjid) } @@ -41,10 +41,10 @@ func NewJid(sjid string) (*Jid, error) { } if !isUsernameValid(jid.Node) { - return jid, fmt.Errorf("invalid Node in JID '%s'", sjid) + return jid, fmt.Errorf("invalid Node in Jid '%s'", sjid) } if !isDomainValid(jid.Domain) { - return jid, fmt.Errorf("invalid domain in JID '%s'", sjid) + return jid, fmt.Errorf("invalid domain in Jid '%s'", sjid) } return jid, nil diff --git a/jid_test.go b/stanza/jid_test.go similarity index 99% rename from jid_test.go rename to stanza/jid_test.go index c6fee03..db86a9d 100644 --- a/jid_test.go +++ b/stanza/jid_test.go @@ -1,4 +1,4 @@ -package xmpp +package stanza import ( "testing" diff --git a/stanza/msg_pubsub_event.go b/stanza/msg_pubsub_event.go new file mode 100644 index 0000000..6ee3dfb --- /dev/null +++ b/stanza/msg_pubsub_event.go @@ -0,0 +1,214 @@ +package stanza + +import ( + "encoding/xml" +) + +// Implementation of the http://jabber.org/protocol/pubsub#event namespace +type PubSubEvent struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#event event"` + MsgExtension + EventElement EventElement + //List ItemsEvent +} + +func init() { + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, PubSubEvent{}) +} + +type EventElement interface { + Name() string +} + +// ********************* +// Collection +// ********************* + +const PubSubCollectionEventName = "Collection" + +type CollectionEvent struct { + AssocDisassoc AssocDisassoc + Node string `xml:"node,attr,omitempty"` +} + +func (c CollectionEvent) Name() string { + return PubSubCollectionEventName +} + +// ********************* +// Associate/Disassociate +// ********************* +type AssocDisassoc interface { + GetAssocDisassoc() string +} + +// ********************* +// Associate +// ********************* +const Assoc = "Associate" + +type AssociateEvent struct { + XMLName xml.Name `xml:"associate"` + Node string `xml:"node,attr"` +} + +func (a *AssociateEvent) GetAssocDisassoc() string { + return Assoc +} + +// ********************* +// Disassociate +// ********************* +const Disassoc = "Disassociate" + +type DisassociateEvent struct { + XMLName xml.Name `xml:"disassociate"` + Node string `xml:"node,attr"` +} + +func (e *DisassociateEvent) GetAssocDisassoc() string { + return Disassoc +} + +// ********************* +// Configuration +// ********************* + +const PubSubConfigEventName = "Configuration" + +type ConfigurationEvent struct { + Node string `xml:"node,attr,omitempty"` + Form *Form +} + +func (c ConfigurationEvent) Name() string { + return PubSubConfigEventName +} + +// ********************* +// Delete +// ********************* +const PubSubDeleteEventName = "Delete" + +type DeleteEvent struct { + Node string `xml:"node,attr"` + Redirect *RedirectEvent `xml:"redirect"` +} + +func (c DeleteEvent) Name() string { + return PubSubConfigEventName +} + +// ********************* +// Redirect +// ********************* +type RedirectEvent struct { + URI string `xml:"uri,attr"` +} + +// ********************* +// List +// ********************* + +const PubSubItemsEventName = "List" + +type ItemsEvent struct { + XMLName xml.Name `xml:"items"` + Items []ItemEvent `xml:"item,omitempty"` + Node string `xml:"node,attr"` + Retract *RetractEvent `xml:"retract"` +} + +type ItemEvent struct { + XMLName xml.Name `xml:"item"` + Id string `xml:"id,attr,omitempty"` + Publisher string `xml:"publisher,attr,omitempty"` + Any *Node `xml:",any"` +} + +func (i ItemsEvent) Name() string { + return PubSubItemsEventName +} + +// ********************* +// List +// ********************* + +type RetractEvent struct { + XMLName xml.Name `xml:"retract"` + ID string `xml:"node,attr"` +} + +// ********************* +// Purge +// ********************* +const PubSubPurgeEventName = "Purge" + +type PurgeEvent struct { + XMLName xml.Name `xml:"purge"` + Node string `xml:"node,attr"` +} + +func (p PurgeEvent) Name() string { + return PubSubPurgeEventName +} + +// ********************* +// Subscription +// ********************* +const PubSubSubscriptionEventName = "Subscription" + +type SubscriptionEvent struct { + SubStatus string `xml:"subscription,attr,omitempty"` + Expiry string `xml:"expiry,attr,omitempty"` + SubInfo `xml:",omitempty"` +} + +func (s SubscriptionEvent) Name() string { + return PubSubSubscriptionEventName +} + +func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + pse.XMLName = start.Name + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + var ee EventElement + switch tt := t.(type) { + case xml.StartElement: + switch tt.Name.Local { + case "collection": + ee = &CollectionEvent{} + case "configuration": + ee = &ConfigurationEvent{} + case "delete": + ee = &DeleteEvent{} + case "items": + ee = &ItemsEvent{} + case "purge": + ee = &PurgeEvent{} + case "subscription": + ee = &SubscriptionEvent{} + default: + ee = nil + } + // known child element found, decode it + if ee != nil { + err = d.DecodeElement(ee, &tt) + if err != nil { + return err + } + pse.EventElement = ee + } + case xml.EndElement: + if tt == start.End() { + return nil + } + } + + } + return nil +} diff --git a/stanza/msg_pubsub_event_test.go b/stanza/msg_pubsub_event_test.go new file mode 100644 index 0000000..29a795d --- /dev/null +++ b/stanza/msg_pubsub_event_test.go @@ -0,0 +1,162 @@ +package stanza_test + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "strings" + "testing" +) + +func TestDecodeMsgEvent(t *testing.T) { + str := ` + + + + + Soliloquy + + To be, or not to be: that is the question: + Whether 'tis nobler in the mind to suffer + The slings and arrows of outrageous fortune, + Or to take arms against a sea of troubles, + And by opposing end them? + + + tag:denmark.lit,2003:entry-32397 + 2003-12-13T18:30:02Z + 2003-12-13T18:30:02Z + + + + + + ` + parsedMessage := stanza.Message{} + if err := xml.Unmarshal([]byte(str), &parsedMessage); err != nil { + t.Errorf("message receipt unmarshall error: %v", err) + return + } + + if parsedMessage.Body != "" { + t.Errorf("Unexpected body: '%s'", parsedMessage.Body) + } + + if len(parsedMessage.Extensions) < 1 { + t.Errorf("no extension found on parsed message") + return + } + + switch ext := parsedMessage.Extensions[0].(type) { + case *stanza.PubSubEvent: + if ext.XMLName.Local != "event" { + t.Fatalf("unexpected extension: %s:%s", ext.XMLName.Space, ext.XMLName.Local) + } + tmp, ok := parsedMessage.Extensions[0].(*stanza.PubSubEvent) + if !ok { + t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local) + } + ie, ok := tmp.EventElement.(*stanza.ItemsEvent) + if !ok { + t.Fatalf("unexpected extension element: %s:%s", ext.XMLName.Space, ext.XMLName.Local) + } + if ie.Items[0].Any.Nodes[0].Content != "Soliloquy" { + t.Fatalf("could not read title ! Read this : %s", ie.Items[0].Any.Nodes[0].Content) + } + + if len(ie.Items[0].Any.Nodes) != 6 { + t.Fatalf("some nodes were not correctly parsed") + } + default: + t.Fatalf("could not find pubsub event extension") + } + +} + +func TestEncodeEvent(t *testing.T) { + expected := "" + + "" + + "My pub item title" + + "My pub item content summary" + + "My pub item content ID2003-12-13T18:30:02Z" + + "2003-12-13T18:30:02Z" + message := stanza.Message{ + Extensions: []stanza.MsgExtension{ + stanza.PubSubEvent{ + EventElement: stanza.ItemsEvent{ + Items: []stanza.ItemEvent{ + { + Id: "ae890ac52d0df67ed7cfdf51b644e901", + Any: &stanza.Node{ + XMLName: xml.Name{ + Space: "http://www.w3.org/2005/Atom", + Local: "entry", + }, + Attrs: nil, + Content: "", + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Space: "", Local: "title"}, + Attrs: nil, + Content: "My pub item title", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "summary"}, + Attrs: nil, + Content: "My pub item content summary", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "link"}, + Attrs: []xml.Attr{ + { + Name: xml.Name{Space: "", Local: "rel"}, + Value: "alternate", + }, + { + Name: xml.Name{Space: "", Local: "type"}, + Value: "text/html", + }, + { + Name: xml.Name{Space: "", Local: "href"}, + Value: "http://denmark.lit/2003/12/13/atom03", + }, + }, + }, + { + XMLName: xml.Name{Space: "", Local: "id"}, + Attrs: nil, + Content: "My pub item content ID", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "published"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "updated"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + }, + }, + }, + }, + Node: "princely_musings", + Retract: nil, + }, + }, + }, + } + + data, _ := xml.Marshal(message) + if strings.TrimSpace(string(data)) != strings.TrimSpace(expected) { + t.Errorf("event was not encoded properly : \nexpected:%s \ngot: %s", expected, string(data)) + } + +} diff --git a/stanza/pep.go b/stanza/pep.go index 7de57ea..cfd50c1 100644 --- a/stanza/pep.go +++ b/stanza/pep.go @@ -15,7 +15,7 @@ type Tune struct { Uri string `xml:"uri,omitempty"` } -// Mood defines deta model for XEP-0107 - User Mood +// Mood defines data model for XEP-0107 - User Mood // See: https://xmpp.org/extensions/xep-0107.html type Mood struct { MsgExtension // Mood can be added as a message extension diff --git a/stanza/pubsub.go b/stanza/pubsub.go index 010f8b0..1cbdf16 100644 --- a/stanza/pubsub.go +++ b/stanza/pubsub.go @@ -2,39 +2,383 @@ package stanza import ( "encoding/xml" + "errors" + "strings" ) -type PubSub struct { +type PubSubGeneric struct { XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"` - Publish *Publish - Retract *Retract - // TODO + + Create *Create `xml:"create,omitempty"` + Configure *Configure `xml:"configure,omitempty"` + + Subscribe *SubInfo `xml:"subscribe,omitempty"` + SubOptions *SubOptions `xml:"options,omitempty"` + + Publish *Publish `xml:"publish,omitempty"` + PublishOptions *PublishOptions `xml:"publish-options"` + + Affiliations *Affiliations `xml:"affiliations,omitempty"` + Default *Default `xml:"default,omitempty"` + + Items *Items `xml:"items,omitempty"` + Retract *Retract `xml:"retract,omitempty"` + Subscription *Subscription `xml:"subscription,omitempty"` + + Subscriptions *Subscriptions `xml:"subscriptions,omitempty"` + // To use in responses to sub/unsub for instance + // Subscription options + Unsubscribe *SubInfo `xml:"unsubscribe,omitempty"` } -func (p *PubSub) Namespace() string { +func (p *PubSubGeneric) Namespace() string { return p.XMLName.Space } +type Affiliations struct { + List []Affiliation `xml:"affiliation"` + Node string `xml:"node,attr,omitempty"` +} + +type Affiliation struct { + AffiliationStatus string `xml:"affiliation"` + Node string `xml:"node,attr"` +} + +type Create struct { + Node string `xml:"node,attr,omitempty"` +} + +type SubOptions struct { + SubInfo + Form *Form `xml:"x"` +} + +type Configure struct { + Form *Form `xml:"x"` +} +type Default struct { + Node string `xml:"node,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Form *Form `xml:"x"` +} + +type Subscribe struct { + XMLName xml.Name `xml:"subscribe"` + SubInfo +} +type Unsubscribe struct { + XMLName xml.Name `xml:"unsubscribe"` + SubInfo +} + +// SubInfo represents information about a subscription +// Node is the node related to the subscription +// Jid is the subscription JID of the subscribed entity +// SubID is the subscription ID +type SubInfo struct { + Node string `xml:"node,attr,omitempty"` + Jid string `xml:"jid,attr,omitempty"` + // Sub ID is optional + SubId *string `xml:"subid,attr,omitempty"` +} + +// validate checks if a node and a jid are present in the sub info, and if this jid is valid. +func (si *SubInfo) validate() error { + // Requests MUST contain a valid JID + if _, err := NewJid(si.Jid); err != nil { + return err + } + // SubInfo must contain both a valid JID and a node. See XEP-0060 + if strings.TrimSpace(si.Node) == "" { + return errors.New("SubInfo must contain the node AND the subscriber JID in subscription config options requests") + } + return nil +} + +// Handles the "5.6 Retrieve Subscriptions" of XEP-0060 +type Subscriptions struct { + XMLName xml.Name `xml:"subscriptions"` + List []Subscription `xml:"subscription,omitempty"` +} + +// Handles the "5.6 Retrieve Subscriptions" and the 6.1 Subscribe to a Node and so on of XEP-0060 +type Subscription struct { + SubStatus string `xml:"subscription,attr,omitempty"` + SubInfo `xml:",omitempty"` + // Seems like we can't marshal a self-closing tag for now : https://github.com/golang/go/issues/21399 + // subscribe-options should be like this as per XEP-0060: + // + // + // + // Used to indicate if configuration options is required. + Required *struct{} +} + +type PublishOptions struct { + XMLName xml.Name `xml:"publish-options"` + Form *Form +} + type Publish struct { XMLName xml.Name `xml:"publish"` Node string `xml:"node,attr"` - Item Item + Items []Item `xml:"item,omitempty"` // xsd says there can be many. See also 12.10 Batch Processing of XEP-0060 +} + +type Items struct { + List []Item `xml:"item,omitempty"` + MaxItems int `xml:"max_items,attr,omitempty"` + Node string `xml:"node,attr"` + SubId string `xml:"subid,attr,omitempty"` } type Item struct { - XMLName xml.Name `xml:"item"` - Id string `xml:"id,attr,omitempty"` - Tune *Tune - Mood *Mood + XMLName xml.Name `xml:"item"` + Id string `xml:"id,attr,omitempty"` + Publisher string `xml:"publisher,attr,omitempty"` + Any *Node `xml:",any"` } type Retract struct { XMLName xml.Name `xml:"retract"` Node string `xml:"node,attr"` - Notify string `xml:"notify,attr"` - Item Item + Notify *bool `xml:"notify,attr,omitempty"` + Items []Item `xml:"item"` +} + +type PubSubOption struct { + XMLName xml.Name `xml:"jabber:x:data options"` + Form `xml:"x"` +} + +// NewSubRq builds a subscription request to a node at the given service. +// It's a Set type IQ. +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.1 Subscribe to a Node +func NewSubRq(serviceId string, subInfo SubInfo) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Subscribe: &subInfo, + } + return iq, nil +} + +// NewUnsubRq builds an unsub request to a node at the given service. +// It's a Set type IQ +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.2 Unsubscribe from a Node +func NewUnsubRq(serviceId string, subInfo SubInfo) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Unsubscribe: &subInfo, + } + return iq, nil +} + +// NewSubOptsRq builds a request for the subscription options. +// It's a Get type IQ +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.3 Configure Subscription Options +func NewSubOptsRq(serviceId string, subInfo SubInfo) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + SubOptions: &SubOptions{ + SubInfo: subInfo, + }, + } + return iq, nil +} + +// NewFormSubmission builds a form submission pubsub IQ +// Information about the subscription and the requester are separated. subInfo contains information about the subscription. +// 6.3.5 Form Submission +func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + if form.Type != FormTypeSubmit { + return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type) + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + SubOptions: &SubOptions{ + SubInfo: subInfo, + Form: form, + }, + } + return iq, nil +} + +// NewSubAndConfig builds a subscribe request that contains configuration options for the service +// From XEP-0060 : The element MUST follow the element and +// MUST NOT possess a 'node' attribute or 'jid' attribute, +// since the value of the element's 'node' attribute specifies the desired NodeID and +// the value of the element's 'jid' attribute specifies the subscriber's JID +// 6.3.7 Subscribe and Configure +func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error) { + if e := subInfo.validate(); e != nil { + return IQ{}, e + } + if form.Type != FormTypeSubmit { + return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type) + } + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Subscribe: &subInfo, + SubOptions: &SubOptions{ + SubInfo: SubInfo{SubId: subInfo.SubId}, + Form: form, + }, + } + return iq, nil + +} + +// NewItemsRequest creates a request to query existing items from a node. +// Specify a "maxItems" value to request only the last maxItems items. If 0, requests all items. +// 6.5.2 Requesting All List AND 6.5.7 Requesting the Most Recent List +func NewItemsRequest(serviceId string, node string, maxItems int) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Items: &Items{Node: node}, + } + + if maxItems != 0 { + ps, _ := iq.Payload.(*PubSubGeneric) + ps.Items.MaxItems = maxItems + } + return iq, nil +} + +// NewItemsRequest creates a request to get a specific item from a node. +// 6.5.8 Requesting a Particular Item +func NewSpecificItemRequest(serviceId, node, itemId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Items: &Items{Node: node, + List: []Item{ + { + Id: itemId, + }, + }, + }, + } + return iq, nil +} + +// NewPublishItemRq creates a request to publish a single item to a node identified by its provided ID +func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (IQ, error) { + // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot publish without a target node ID") + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Publish: &Publish{Node: nodeID, Items: []Item{item}}, + } + + // "The element provided by the publisher MAY possess an 'id' attribute, + // specifying a unique ItemID for the item. + // If an ItemID is not provided in the publish request, + // the pubsub service MUST generate one and MUST ensure that it is unique for that node." + if strings.TrimSpace(pubItemID) != "" { + ps, _ := iq.Payload.(*PubSubGeneric) + ps.Publish.Items[0].Id = pubItemID + } + return iq, nil +} + +// NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options +// A pubsub service MAY support the ability to specify options along with a publish request +//(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature). +func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (IQ, error) { + // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot publish without a target node ID") + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Publish: &Publish{Node: nodeID, Items: items}, + PublishOptions: options, + } + + return iq, nil +} + +// NewDelItemFromNode creates a request to delete and item from a node, given its id. +// To delete an item, the publisher sends a retract request. +// This helper function follows 7.2 Delete an Item from a Node +func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (IQ, error) { + // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot delete item without a target node ID") + } + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Retract: &Retract{Node: nodeID, Items: []Item{{Id: itemId}}, Notify: notify}, + } + return iq, nil +} + +// NewCreateAndConfigNode makes a request for node creation that has the desired node configuration. +// See 8.1.3 Create and Configure a Node +func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Create: &Create{Node: nodeID}, + Configure: &Configure{Form: confForm}, + } + return iq, nil +} + +// NewCreateNode builds a request to create a node on the service referenced by "serviceId" +// See 8.1 Create a Node +func NewCreateNode(serviceId, nodeName string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Create: &Create{Node: nodeName}, + } + return iq, nil +} + +// NewRetrieveAllSubsRequest builds a request to retrieve all subscriptions from all nodes +// In order to make the request, the requesting entity MUST send an IQ-get whose +// child contains an empty element with no attributes. +func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Subscriptions: &Subscriptions{}, + } + return iq, nil +} + +// NewRetrieveAllAffilsRequest builds a request to retrieve all affiliations from all nodes +// In order to make the request of the service, the requesting entity includes an empty element with no attributes. +func NewRetrieveAllAffilsRequest(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubGeneric{ + Affiliations: &Affiliations{}, + } + return iq, nil } func init() { - TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub", Local: "pubsub"}, PubSubGeneric{}) } diff --git a/stanza/pubsub_owner.go b/stanza/pubsub_owner.go new file mode 100644 index 0000000..054303f --- /dev/null +++ b/stanza/pubsub_owner.go @@ -0,0 +1,377 @@ +package stanza + +import ( + "encoding/xml" + "errors" + "strings" +) + +type PubSubOwner struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"` + OwnerUseCase OwnerUseCase +} + +func (pso *PubSubOwner) Namespace() string { + return pso.XMLName.Space +} + +type OwnerUseCase interface { + UseCase() string +} + +type AffiliationsOwner struct { + XMLName xml.Name `xml:"affiliations"` + Affiliations []AffiliationOwner `xml:"affiliation,omitempty"` + Node string `xml:"node,attr"` +} + +func (AffiliationsOwner) UseCase() string { + return "affiliations" +} + +type AffiliationOwner struct { + XMLName xml.Name `xml:"affiliation"` + AffiliationStatus string `xml:"affiliation,attr"` + Jid string `xml:"jid,attr"` +} + +const ( + AffiliationStatusMember = "member" + AffiliationStatusNone = "none" + AffiliationStatusOutcast = "outcast" + AffiliationStatusOwner = "owner" + AffiliationStatusPublisher = "publisher" + AffiliationStatusPublishOnly = "publish-only" +) + +type ConfigureOwner struct { + XMLName xml.Name `xml:"configure"` + Node string `xml:"node,attr,omitempty"` + Form *Form `xml:"x,omitempty"` +} + +func (*ConfigureOwner) UseCase() string { + return "configure" +} + +type DefaultOwner struct { + XMLName xml.Name `xml:"default"` + Form *Form `xml:"x,omitempty"` +} + +func (*DefaultOwner) UseCase() string { + return "default" +} + +type DeleteOwner struct { + XMLName xml.Name `xml:"delete"` + RedirectOwner *RedirectOwner `xml:"redirect,omitempty"` + Node string `xml:"node,attr,omitempty"` +} + +func (*DeleteOwner) UseCase() string { + return "delete" +} + +type RedirectOwner struct { + XMLName xml.Name `xml:"redirect"` + URI string `xml:"uri,attr"` +} + +type PurgeOwner struct { + XMLName xml.Name `xml:"purge"` + Node string `xml:"node,attr"` +} + +func (*PurgeOwner) UseCase() string { + return "purge" +} + +type SubscriptionsOwner struct { + XMLName xml.Name `xml:"subscriptions"` + Subscriptions []SubscriptionOwner `xml:"subscription"` + Node string `xml:"node,attr"` +} + +func (*SubscriptionsOwner) UseCase() string { + return "subscriptions" +} + +type SubscriptionOwner struct { + SubscriptionStatus string `xml:"subscription"` + Jid string `xml:"jid,attr"` +} + +const ( + SubscriptionStatusNone = "none" + SubscriptionStatusPending = "pending" + SubscriptionStatusSubscribed = "subscribed" + SubscriptionStatusUnconfigured = "unconfigured" +) + +// NewConfigureNode creates a request to configure a node on the given service. +// A form will be returned by the service, to which the user must respond using for instance the NewFormSubmission function. +// See 8.2 Configure a Node +func NewConfigureNode(serviceId, nodeName string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &ConfigureOwner{Node: nodeName}, + } + return iq, nil +} + +// NewDelNode creates a request to delete node "nodeID" from the "serviceId" service +// See 8.4 Delete a Node +func NewDelNode(serviceId, nodeID string) (IQ, error) { + if strings.TrimSpace(nodeID) == "" { + return IQ{}, errors.New("cannot delete a node without a target node ID") + } + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &DeleteOwner{Node: nodeID}, + } + return iq, nil +} + +// NewPurgeAllItems creates a new purge request for the "nodeId" node, at "serviceId" service +// See 8.5 Purge All Node Items +func NewPurgeAllItems(serviceId, nodeId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &PurgeOwner{Node: nodeId}, + } + return iq, nil +} + +// NewRequestDefaultConfig build a request to ask the service for the default config of its nodes +// See 8.3 Request Default Node Configuration Options +func NewRequestDefaultConfig(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &DefaultOwner{}, + } + return iq, nil +} + +// NewApproveSubRequest creates a new sub approval response to a request from the service to the owner of the node +// In order to approve the request, the owner shall submit the form and set the "pubsub#allow" field to a value of "1" or "true" +// For tracking purposes the message MUST reflect the 'id' attribute originally provided in the request. +// See 8.6 Manage Subscription Requests +func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, error) { + if serviceId == "" { + return Message{}, errors.New("need a target service serviceId send approval serviceId") + } + if reqID == "" { + return Message{}, errors.New("the request ID is empty but must be used for the approval") + } + if apprForm == nil { + return Message{}, errors.New("approval form is nil") + } + apprMess := NewMessage(Attrs{To: serviceId}) + apprMess.Extensions = []MsgExtension{apprForm} + apprMess.Id = reqID + + return apprMess, nil +} + +// NewGetPendingSubRequests creates a new request for all pending subscriptions to all their nodes at a service +// This feature MUST be implemented using the Ad-Hoc Commands (XEP-0050) protocol +// 8.7 Process Pending Subscription Requests +func NewGetPendingSubRequests(serviceId string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &Command{ + // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" + Node: "http://jabber.org/protocol/pubsub#get-pending", + Action: CommandActionExecute, + } + return iq, nil +} + +// NewGetPendingSubRequests creates a new request for all pending subscriptions to be approved on a given node +// Upon receiving the data form for managing subscription requests, the owner then MAY request pending subscription +// approval requests for a given node. +// See 8.7.4 Per-Node Request +func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error) { + if sessionId == "" { + return IQ{}, errors.New("the sessionId must be maintained for the command") + } + + form := &Form{ + Type: FormTypeSubmit, + Fields: []Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}}, + } + data, err := xml.Marshal(form) + if err != nil { + return IQ{}, err + } + var n Node + xml.Unmarshal(data, &n) + + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &Command{ + // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" + Node: "http://jabber.org/protocol/pubsub#get-pending", + Action: CommandActionExecute, + SessionId: sessionId, + CommandElement: &n, + } + return iq, nil +} + +// NewSubListRequest creates a request to list subscriptions of the client, for all nodes at the service. +// It's a Get type IQ +// 8.8.1 Retrieve Subscriptions +func NewSubListRqPl(serviceId, nodeID string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &SubscriptionsOwner{Node: nodeID}, + } + return iq, nil +} + +func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs}, + } + return iq, nil +} + +// NewModifAffiliationRequest creates a request to either modify one or more affiliations, or delete one or more affiliations +// 8.9.2 Modify Affiliation & 8.9.2.4 Multiple Simultaneous Modifications & 8.9.3 Delete an Entity (just set the status to "none") +func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &AffiliationsOwner{ + Node: nodeID, + Affiliations: newAffils, + }, + } + return iq, nil +} + +// NewAffiliationListRequest creates a request to list all affiliated entities +// See 8.9.1 Retrieve List List +func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) { + iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq.Payload = &PubSubOwner{ + OwnerUseCase: &AffiliationsOwner{ + Node: nodeID, + }, + } + return iq, nil +} + +// GetFormFields gets the fields from a form in a IQ stanza of type result, as a map. +// Key is the "var" attribute of the field, and field is the value. +// The user can then select and modify the fields they want to alter, and submit a new form to the service using the +// NewFormSubmission function to build the IQ. +// TODO : remove restriction on IQ type ? +func (iq *IQ) GetFormFields() (map[string]Field, error) { + if iq.Type != IQTypeResult { + return nil, errors.New("this IQ is not a result type IQ. Cannot extract the form from it") + } + switch payload := iq.Payload.(type) { + // We support IOT Control IQ + case *PubSubGeneric: + fieldMap := make(map[string]Field) + for _, elt := range payload.Configure.Form.Fields { + fieldMap[elt.Var] = elt + } + return fieldMap, nil + case *PubSubOwner: + fieldMap := make(map[string]Field) + co, ok := payload.OwnerUseCase.(*ConfigureOwner) + if !ok { + return nil, errors.New("this IQ does not contain a PubSub payload with a configure tag for the owner namespace") + } + for _, elt := range co.Form.Fields { + fieldMap[elt.Var] = elt + } + return fieldMap, nil + default: + if iq.Any != nil { + fieldMap := make(map[string]Field) + if iq.Any.XMLName.Local != "command" { + return nil, errors.New("this IQ does not contain a form") + } + + for _, nde := range iq.Any.Nodes { + if nde.XMLName.Local == "x" { + for _, n := range nde.Nodes { + if n.XMLName.Local == "field" { + f := Field{} + data, err := xml.Marshal(n) + if err != nil { + continue + } + err = xml.Unmarshal(data, &f) + if err == nil { + fieldMap[f.Var] = f + } + } + } + } + } + return fieldMap, nil + } + return nil, errors.New("this IQ does not contain a form") + } +} + +func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + pso.XMLName = start.Name + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + // Decode sub-elements + var err error + switch tt.Name.Local { + + case "affiliations": + aff := AffiliationsOwner{} + d.DecodeElement(&aff, &tt) + pso.OwnerUseCase = &aff + case "configure": + co := ConfigureOwner{} + d.DecodeElement(&co, &tt) + pso.OwnerUseCase = &co + case "default": + def := DefaultOwner{} + d.DecodeElement(&def, &tt) + pso.OwnerUseCase = &def + case "delete": + del := DeleteOwner{} + d.DecodeElement(&del, &tt) + pso.OwnerUseCase = &del + case "purge": + pu := PurgeOwner{} + d.DecodeElement(&pu, &tt) + pso.OwnerUseCase = &pu + case "subscriptions": + subs := SubscriptionsOwner{} + d.DecodeElement(&subs, &tt) + pso.OwnerUseCase = &subs + if err != nil { + return err + } + } + + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/pubsub#owner", Local: "pubsub"}, PubSubOwner{}) +} diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go new file mode 100644 index 0000000..8af6194 --- /dev/null +++ b/stanza/pubsub_owner_test.go @@ -0,0 +1,833 @@ +package stanza_test + +import ( + "encoding/xml" + "errors" + "gosrc.io/xmpp/stanza" + "testing" +) + +// ****************************** +// * 8.2 Configure a Node +// ****************************** +func TestNewConfigureNode(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "config1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.Node == "" { + t.Fatalf("could not parse node from config tag") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewConfigureNodeResp(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#node_config + + + 0 + + + 1028 + + + + + + never + + + 0 + + + + + headline + + + http://www.w3.org/2005/Atom + + + + + + +` + + pubsub, err := getPubSubOwnerPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.Form == nil { + t.Fatalf("form is nil in the parsed config tag") + } + + if len(ownrUsecase.Form.Fields) != 8 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// ************************************************* +// * 8.3 Request Default Node Configuration Options +// ************************************************* + +func TestNewRequestDefaultConfig(t *testing.T) { + expectedReq := " " + + " " + + subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit") + subR.Id = "def1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + _, ok = pubsub.OwnerUseCase.(*stanza.DefaultOwner) + if !ok { + t.Fatalf("owner use case is not a default tag") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewRequestDefaultConfigResp(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#node_config + + + 0 + + + 1028 + + + + + + never + + + 0 + + + + + headline + + + http://www.w3.org/2005/Atom + + + + + + +` + + pubsub, err := getPubSubOwnerPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.Form == nil { + t.Fatalf("form is nil in the parsed config tag") + } + + if len(ownrUsecase.Form.Fields) != 8 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// *********************** +// * 8.4 Delete a Node +// *********************** + +func TestNewDelNode(t *testing.T) { + expectedReq := "" + + " " + + " " + + subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "delete1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + _, ok = pubsub.OwnerUseCase.(*stanza.DeleteOwner) + if !ok { + t.Fatalf("owner use case is not a delete tag") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewDelNodeResp(t *testing.T) { + response := ` + + + + + + + +` + + pubsub, err := getPubSubOwnerPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + ownrUsecase, ok := pubsub.OwnerUseCase.(*stanza.DeleteOwner) + if !ok { + t.Fatalf("owner use case is not a configure tag") + } + + if ownrUsecase.RedirectOwner == nil { + t.Fatalf("redirect is nil in the delete tag") + } + + if ownrUsecase.RedirectOwner.URI == "" { + t.Fatalf("could not parse redirect uri") + } +} + +// **************************** +// * 8.5 Purge All Node Items +// **************************** + +func TestNewPurgeAllItems(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "purge1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if pubsub.OwnerUseCase == nil { + t.Fatalf("owner use case is nil") + } + + purge, ok := pubsub.OwnerUseCase.(*stanza.PurgeOwner) + if !ok { + t.Fatalf("owner use case is not a delete tag") + } + + if purge.Node == "" { + t.Fatalf("could not parse purge targer node") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// ************************************ +// * 8.6 Manage Subscription Requests +// ************************************ +func TestNewApproveSubRequest(t *testing.T) { + expectedReq := " " + + " " + + "http://jabber.org/protocol/pubsub#subscribe_authorization " + + " 123-abc princely_musings " + + " horatio@denmark.lit " + + "true " + + apprForm := &stanza.Form{ + Type: stanza.FormTypeSubmit, + Fields: []stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}}, + {Var: "pubsub#subid", ValuesList: []string{"123-abc"}}, + {Var: "pubsub#node", ValuesList: []string{"princely_musings"}}, + {Var: "pubsub#subscriber_jid", ValuesList: []string{"horatio@denmark.lit"}}, + {Var: "pubsub#allow", ValuesList: []string{"true"}}, + }, + } + + subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm) + subR.Id = "approve1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + frm, ok := subR.Extensions[0].(*stanza.Form) + if !ok { + t.Fatalf("extension is not a from !") + } + + var allowField *stanza.Field + + for _, f := range frm.Fields { + if f.Var == "pubsub#allow" { + allowField = &f + } + } + if allowField == nil || allowField.ValuesList[0] != "true" { + t.Fatalf("could not correctly parse the allow field in the response from") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// ******************************************** +// * 8.7 Process Pending Subscription Requests +// ******************************************** + +func TestNewGetPendingSubRequests(t *testing.T) { + expectedReq := " " + + "" + + " " + + subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit") + subR.Id = "pending1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + command, ok := subR.Payload.(*stanza.Command) + if !ok { + t.Fatalf("payload is not a command !") + } + + if command.Action != stanza.CommandActionExecute { + t.Fatalf("command should be execute !") + } + + if command.Node != "http://jabber.org/protocol/pubsub#get-pending" { + t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewGetPendingSubRequestsResp(t *testing.T) { + response := ` + + + + + http://jabber.org/protocol/pubsub#subscribe_authorization + + + + + + + + +` + + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + if err != nil { + t.Fatalf("could not parse iq") + } + + _, ok := respIQ.Payload.(*stanza.Command) + if !ok { + errors.New("this iq payload is not a command") + } + + fMap, err := respIQ.GetFormFields() + if err != nil || len(fMap) != 2 { + errors.New("could not parse command form fields") + } + +} + +// ******************************************** +// * 8.7 Process Pending Subscription Requests +// ******************************************** + +func TestNewApprovePendingSubRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + "princely_musings " + + subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit", + "pubsub-get-pending:20031021T150901Z-600", + "princely_musings") + subR.Id = "pending2" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + command, ok := subR.Payload.(*stanza.Command) + if !ok { + t.Fatalf("payload is not a command !") + } + + if command.Action != stanza.CommandActionExecute { + t.Fatalf("command should be execute !") + } + + //if command.Node != "http://jabber.org/protocol/pubsub#get-pending"{ + // t.Fatalf("command node should be http://jabber.org/protocol/pubsub#get-pending !") + //} + // + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// ******************************************** +// * 8.8.1 Retrieve Subscriptions List +// ******************************************** + +func TestNewSubListRqPl(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "subman1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain a subscriptions node !") + } + + if subs.Node != "princely_musings" { + t.Fatalf("subs node attribute should be princely_musings. Found %s", subs.Node) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewSubListRqPlResp(t *testing.T) { + response := ` + + + + + + + + + + +` + + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + if err != nil { + t.Fatalf("could not parse iq") + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) + if !ok { + errors.New("this iq payload is not a command") + } + + subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain a subscriptions node !") + } + + if len(subs.Subscriptions) != 4 { + t.Fatalf("expected to find 4 subscriptions but got %d", len(subs.Subscriptions)) + } + +} + +// ******************************************** +// * 8.9.1 Retrieve Affiliations List +// ******************************************** + +func TestNewAffiliationListRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "ent1" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain an affiliations node !") + } + + if affils.Node != "princely_musings" { + t.Fatalf("affils node attribute should be princely_musings. Found %s", affils.Node) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewAffiliationListRequestResp(t *testing.T) { + response := ` + + + + + + + + +` + + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + if err != nil { + t.Fatalf("could not parse iq") + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) + if !ok { + errors.New("this iq payload is not a command") + } + + affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain an affiliations node !") + } + + if len(affils.Affiliations) != 2 { + t.Fatalf("expected to find 2 subscriptions but got %d", len(affils.Affiliations)) + } + +} + +// ******************************************** +// * 8.9.2 Modify Affiliation +// ******************************************** + +func TestNewModifAffiliationRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + " " + + " " + + "" + + affils := []stanza.AffiliationOwner{ + { + AffiliationStatus: stanza.AffiliationStatusNone, + Jid: "hamlet@denmark.lit", + }, + { + AffiliationStatus: stanza.AffiliationStatusNone, + Jid: "polonius@denmark.lit", + }, + { + AffiliationStatus: stanza.AffiliationStatusPublisher, + Jid: "bard@shakespeare.lit", + }, + } + + subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils) + subR.Id = "ent3" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + as, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) + if !ok { + t.Fatalf("pubsub doesn not contain an affiliations node !") + } + + if as.Node != "princely_musings" { + t.Fatalf("affils node attribute should be princely_musings. Found %s", as.Node) + } + if len(as.Affiliations) != 3 { + t.Fatalf("expected 3 affiliations, found %d", len(as.Affiliations)) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestGetFormFields(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#node_config + + + 0 + + + 1028 + + + + + + never + + + 0 + + + + + headline + + + http://www.w3.org/2005/Atom + + + + + + +` + var iq stanza.IQ + err := xml.Unmarshal([]byte(response), &iq) + if err != nil { + t.Fatalf("could not parse IQ") + } + + fields, err := iq.GetFormFields() + if len(fields) != 8 { + t.Fatalf("could not correctly parse fields. Expected 8, found : %v", len(fields)) + } + +} + +func TestGetFormFieldsCmd(t *testing.T) { + response := ` + + + + + http://jabber.org/protocol/pubsub#subscribe_authorization + + + + + + + + +` + var iq stanza.IQ + err := xml.Unmarshal([]byte(response), &iq) + if err != nil { + t.Fatalf("could not parse IQ") + } + + fields, err := iq.GetFormFields() + if len(fields) != 2 { + t.Fatalf("could not correctly parse fields. Expected 2, found : %v", len(fields)) + } + +} + +func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) { + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + return &stanza.PubSubOwner{}, err + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) + if !ok { + errors.New("this iq payload is not a pubsub of the owner namespace") + } + + return pubsub, nil +} diff --git a/stanza/pubsub_test.go b/stanza/pubsub_test.go new file mode 100644 index 0000000..95bf640 --- /dev/null +++ b/stanza/pubsub_test.go @@ -0,0 +1,921 @@ +package stanza_test + +import ( + "encoding/xml" + "errors" + "gosrc.io/xmpp/stanza" + "strings" + "testing" +) + +var submitFormExample = stanza.NewForm([]stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}}, + {Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}}, + {Var: "pubsub#access_model", ValuesList: []string{"roster"}}, + {Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}}, + {Var: "pubsub#type", ValuesList: []string{"http://www.w3.org/2005/Atom"}}, + { + Var: "pubsub#notification_type", + Type: "list-single", + Label: "Specify the delivery style for event notifications", + ValuesList: []string{"headline"}, + Options: []stanza.Option{ + {ValuesList: []string{"normal"}}, + {ValuesList: []string{"headline"}}, + }, + }, +}, stanza.FormTypeSubmit) + +// *********************************** +// * 6.1 Subscribe to a Node +// *********************************** + +func TestNewSubRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo) + subR.Id = "sub1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } + +} + +func TestNewSubResp(t *testing.T) { + response := ` + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + + if pubsub.Subscription == nil { + t.Fatalf("subscription node is nil") + } + if pubsub.Subscription.Node == "" || + pubsub.Subscription.Jid == "" || + pubsub.Subscription.SubId == nil || + pubsub.Subscription.SubStatus == "" { + t.Fatalf("one or more of the subscription attributes was not successfully decoded") + } + +} + +// *********************************** +// * 6.2 Unsubscribe from a Node +// *********************************** + +func TestNewUnSubRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo) + subR.Id = "unsub1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Unsubscribe == nil { + t.Fatalf("Unsubscribe tag should be present in sub config options request") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewUnsubResp(t *testing.T) { + response := ` + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + + if pubsub.Subscription == nil { + t.Fatalf("subscription node is nil") + } + if pubsub.Subscription.Node == "" || + pubsub.Subscription.Jid == "" || + pubsub.Subscription.SubId == nil || + pubsub.Subscription.SubStatus == "" { + t.Fatalf("one or more of the subscription attributes was not successfully decoded") + } + +} + +// *************************************** +// * 6.3 Configure Subscription Options +// *************************************** +func TestNewSubOptsRq(t *testing.T) { + expectedReq := " " + + " " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo) + subR.Id = "options1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.SubOptions == nil { + t.Fatalf("Options tag should be present in sub config options request") + } + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewNewConfOptsRsp(t *testing.T) { + response := ` + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + + 1 + + + 0 + + + false + + + + + + + + chat + online + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + + if pubsub.SubOptions == nil { + t.Fatalf("sub options node is nil") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("the response form is nil") + } + + if len(pubsub.SubOptions.Form.Fields) != 5 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// *************************************** +// * 6.3.5 Form Submission +// *************************************** +func TestNewFormSubmission(t *testing.T) { + expectedReq := " " + + " " + + " " + + " http://jabber.org/protocol/pubsub#node_config " + + "Princely Musings (Atom) " + + "1 roster " + + " friends servants" + + " courtiers http://www.w3.org/2005/Atom " + + " " + + "headline " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + + subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample) + subR.Id = "options2" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.SubOptions == nil { + t.Fatalf("Options tag should be present in sub config options request") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("No form in form submit request !") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// *************************************** +// * 6.3.7 Subscribe and Configure +// *************************************** + +func TestNewSubAndConfig(t *testing.T) { + expectedReq := "" + + " " + + "" + + " " + + " http://jabber.org/protocol/pubsub#node_config " + + "Princely Musings (Atom) " + + "1 roster " + + " friends servants" + + " courtiers http://www.w3.org/2005/Atom " + + " " + + "headline " + + " " + + subInfo := stanza.SubInfo{ + Node: "princely_musings", Jid: "francisco@denmark.lit", + } + + subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample) + subR.Id = "sub1" + if err != nil { + t.Fatalf("Could not create a sub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.SubOptions == nil { + t.Fatalf("Options tag should be present in sub config options request") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("No form in form submit request !") + } + + // The element MUST NOT possess a 'node' attribute or 'jid' attribute + // See XEP-0060 + if pubsub.SubOptions.SubInfo.Node != "" || pubsub.SubOptions.SubInfo.Jid != "" { + t.Fatalf("SubInfo node and jid should be empty for the options tag !") + } + if pubsub.Subscribe.Node == "" || pubsub.Subscribe.Jid == "" { + t.Fatalf("SubInfo node and jid should NOT be empty for the subscribe tag !") + } + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewSubAndConfigResp(t *testing.T) { + response := ` + + + + + + + http://jabber.org/protocol/pubsub#subscribe_options + + + 1 + + + 0 + + + false + + + chat + online + away + + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.Subscription == nil { + t.Fatalf("sub node is nil") + } + + if pubsub.SubOptions == nil { + t.Fatalf("sub options node is nil") + } + if pubsub.SubOptions.Form == nil { + t.Fatalf("the response form is nil") + } + + if len(pubsub.SubOptions.Form.Fields) != 5 { + t.Fatalf("one or more fields in the response form could not be parsed correctly") + } +} + +// *************************************** +// * 6.5.2 Requesting All List +// *************************************** +func TestNewItemsRequest(t *testing.T) { + subR, err := stanza.NewItemsRequest("pubsub.shakespeare.lit", "princely_musings", 0) + if err != nil { + t.Fatalf("Could not create an items request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Items == nil { + t.Fatalf("List tag should be present to request items from a service") + } + if len(pubsub.Items.List) != 0 { + t.Fatalf("There should be no items in the tag to request all items from a service") + } +} +func TestNewItemsResp(t *testing.T) { + response := ` + + + + + + Alone + Now I am alone. O, what a rogue and peasant slave am I! + + tag:denmark.lit,2003:entry-32396 + 2003-12-13T11:09:53Z + 2003-12-13T11:09:53Z + + + + + Soliloquy + To be, or not to be: that is the question: Whether 'tis nobler in the + mind to suffer The slings and arrows of outrageous fortune, Or to take arms + against a sea of troubles, And by opposing end them? + + tag:denmark.lit,2003:entry-32397 + 2003-12-13T18:30:02Z + 2003-12-13T18:30:02Z + + + + + +` + + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.Items == nil { + t.Fatalf("sub options node is nil") + } + if pubsub.Items.List == nil { + t.Fatalf("the response form is nil") + } + + if len(pubsub.Items.List) != 2 { + t.Fatalf("one or more items in the response could not be parsed correctly") + } +} + +// *************************************** +// * 6.5.8 Requesting a Particular Item +// *************************************** +func TestNewSpecificItemRequest(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901") + subR.Id = "items3" + if err != nil { + t.Fatalf("Could not create an items request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Items == nil { + t.Fatalf("List tag should be present to request items from a service") + } + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// *************************************** +// * 7.1 Publish an Item to a Node +// *************************************** +func TestNewPublishItemRq(t *testing.T) { + item := stanza.Item{ + XMLName: xml.Name{}, + Id: "", + Publisher: "", + Any: &stanza.Node{ + XMLName: xml.Name{ + Space: "http://www.w3.org/2005/Atom", + Local: "entry", + }, + Attrs: nil, + Content: "", + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Space: "", Local: "title"}, + Attrs: nil, + Content: "My pub item title", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "summary"}, + Attrs: nil, + Content: "My pub item content summary", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "link"}, + Attrs: []xml.Attr{ + { + Name: xml.Name{Space: "", Local: "rel"}, + Value: "alternate", + }, + { + Name: xml.Name{Space: "", Local: "type"}, + Value: "text/html", + }, + { + Name: xml.Name{Space: "", Local: "href"}, + Value: "http://denmark.lit/2003/12/13/atom03", + }, + }, + }, + { + XMLName: xml.Name{Space: "", Local: "id"}, + Attrs: nil, + Content: "My pub item content ID", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "published"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "updated"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + }, + }, + } + + subR, err := stanza.NewPublishItemRq("pubsub.shakespeare.lit", "princely_musings", "bnd81g37d61f49fgn581", item) + if err != nil { + t.Fatalf("Could not create an item pub request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + + if strings.TrimSpace(pubsub.Publish.Node) == "" { + t.Fatalf("the element MUST possess a 'node' attribute, specifying the NodeID of the node.") + } + if pubsub.Publish.Items[0].Id == "" { + t.Fatalf("an id was provided for the item and it should be used") + } +} + +// *************************************** +// * 7.1.5 Publishing Options +// *************************************** + +func TestNewPublishItemOptsRq(t *testing.T) { + expectedReq := " " + + " " + + " Soliloquy " + + " To be, or not to be: that is the question: Whether \"tis nobler in the mind to suffer The " + + "slings and arrows of outrageous fortune, Or to take arms against a sea of troubles, And by opposing end them? " + + " " + + "tag:denmark.lit,2003:entry-32397 2003-12-13T18:30:02Z " + + "2003-12-13T18:30:02Z " + + " " + + "http://jabber.org/protocol/pubsub#publish-options " + + "presence " + + var iq stanza.IQ + err := xml.Unmarshal([]byte(expectedReq), &iq) + if err != nil { + t.Fatalf("could not unmarshal example request : %s", err) + } + + pubsub, ok := iq.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Publish == nil { + t.Fatalf("Publish tag is empty") + } + if len(pubsub.Publish.Items) != 1 { + t.Fatalf("could not parse item properly") + } +} + +// *************************************** +// * 7.2 Delete an Item from a Node +// *************************************** + +func TestNewDelItemFromNode(t *testing.T) { + expectedReq := " " + + " " + + " " + + subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil) + subR.Id = "retract1" + if err != nil { + t.Fatalf("Could not create a del item request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Retract == nil { + t.Fatalf("Retract tag should be present to del an item from a service") + } + + if strings.TrimSpace(pubsub.Retract.Items[0].Id) == "" { + t.Fatalf("Item id, for the item to delete, should be non empty") + } + if pubsub.Retract.Items[0].Any != nil { + t.Fatalf("Item node must be empty") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +// *************************************** +// * 8.1 Create a Node +// *************************************** + +func TestNewCreateNode(t *testing.T) { + expectedReq := " " + + " " + + subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings") + subR.Id = "create1" + if err != nil { + t.Fatalf("Could not create a create node request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Create == nil { + t.Fatalf("Create tag should be present to create a node on a service") + } + + if strings.TrimSpace(pubsub.Create.Node) == "" { + t.Fatalf("Expected node name to be present") + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestNewCreateNodeResp(t *testing.T) { + response := ` + + + + + +` + pubsub, err := getPubSubGenericPayload(response) + if err != nil { + t.Fatalf(err.Error()) + } + if pubsub.Create == nil { + t.Fatalf("create segment is nil") + } + if pubsub.Create.Node == "" { + t.Fatalf("could not parse generated nodeId") + } + +} + +// *************************************** +// * 8.1.3 Create and Configure a Node +// *************************************** + +func TestNewCreateAndConfigNode(t *testing.T) { + expectedReq := " " + + " " + + " " + + "http://jabber.org/protocol/pubsub#node_config " + + "0 0 " + + " 1028 " + + subR, err := stanza.NewCreateAndConfigNode("pubsub.shakespeare.lit", + "princely_musings", + &stanza.Form{ + Type: stanza.FormTypeSubmit, + Fields: []stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#notify_retract", ValuesList: []string{"0"}}, + {Var: "pubsub#notify_sub", ValuesList: []string{"0"}}, + {Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}}, + }, + }) + subR.Id = "create1" + if err != nil { + t.Fatalf("Could not create a create node request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("payload is not a pubsub !") + } + if pubsub.Create == nil { + t.Fatalf("Create tag should be present to create a node on a service") + } + + if strings.TrimSpace(pubsub.Create.Node) == "" { + t.Fatalf("Expected node name to be present") + } + + if pubsub.Configure == nil { + t.Fatalf("Configure tag should be present to configure a node during its creation on a service") + } + + if pubsub.Configure.Form == nil { + t.Fatalf("Expected a form to be present, to configure the node") + } + if len(pubsub.Configure.Form.Fields) != 4 { + t.Fatalf("Expected 4 elements to be present in the config form but got : %v", len(pubsub.Configure.Form.Fields)) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } + +} + +// ******************************** +// * 5.7 Retrieve Subscriptions +// ******************************** + +func TestNewRetrieveAllSubsRequest(t *testing.T) { + expected := " " + + " " + + subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit") + subR.Id = "subscriptions1" + if err != nil { + t.Fatalf("Could not create a create node request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated del item request : %s", e) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expected, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestRetrieveAllSubsResp(t *testing.T) { + response := ` + + + + + + + + + + + +` + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + t.Fatalf("could not unmarshal response: %s", err) + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("umarshalled payload is not a pubsub") + } + + if pubsub.Subscriptions == nil { + t.Fatalf("subscriptions node is nil") + } + if len(pubsub.Subscriptions.List) != 5 { + t.Fatalf("incorrect number of decoded subscriptions") + } +} + +// ******************************** +// * 5.7 Retrieve Affiliations +// ******************************** + +func TestNewRetrieveAllAffilsRequest(t *testing.T) { + expected := " " + + " " + + subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit") + subR.Id = "affil1" + if err != nil { + t.Fatalf("Could not create retreive all affiliations request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated retreive all affiliations request : %s", e) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expected, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestRetrieveAllAffilsResp(t *testing.T) { + response := ` + + + + + + + + + + +` + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + t.Fatalf("could not unmarshal response: %s", err) + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) + if !ok { + t.Fatalf("umarshalled payload is not a pubsub") + } + + if pubsub.Affiliations == nil { + t.Fatalf("subscriptions node is nil") + } + if len(pubsub.Affiliations.List) != 4 { + t.Fatalf("incorrect number of decoded subscriptions") + } +} + +func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) { + var respIQ stanza.IQ + err := xml.Unmarshal([]byte(response), &respIQ) + + if err != nil { + return &stanza.PubSubGeneric{}, err + } + + pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) + if !ok { + errors.New("this iq payload is not a pubsub") + } + + return pubsub, nil +} diff --git a/stanza/xmpp_test.go b/stanza/xmpp_test.go index 420a053..473616a 100644 --- a/stanza/xmpp_test.go +++ b/stanza/xmpp_test.go @@ -2,12 +2,17 @@ package stanza_test import ( "encoding/xml" + "errors" + "regexp" "testing" "github.com/google/go-cmp/cmp" "gosrc.io/xmpp/stanza" ) +var reLeadcloseWhtsp = regexp.MustCompile(`^[\s\p{Zs}]+|[\s\p{Zs}]+$`) +var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`) + // ============================================================================ // Marshaller / unmarshaller test @@ -63,3 +68,14 @@ func xmlOpts() cmp.Options { } return opts } + +func delSpaces(s string) string { + return reInsideWhtsp.ReplaceAllString(reLeadcloseWhtsp.ReplaceAllString(s, ""), "") +} + +func compareMarshal(expected, data string) error { + if delSpaces(expected) != delSpaces(data) { + return errors.New("failed to verify unmarshal->marshal. Expected :" + expected + "\ngot: " + data) + } + return nil +} diff --git a/tcp_server_mock.go b/tcp_server_mock.go index c8f5d97..1a4f92e 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -280,7 +280,7 @@ func bind(t *testing.T, sc *ServerConn) { %s ` - fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real JID + fmt.Fprintf(sc.connection, result, iq.Id, "test@localhost/test") // TODO use real Jid } } From 75531f457a87c016835d04d62190ff50cdb0d6b9 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Thu, 9 Jan 2020 14:33:38 +0000 Subject: [PATCH 31/54] Change log pub sub (#143) * PubSub protocol support Added support for : - XEP-0050 (Command)) - XEP-0060 (PubSub) - XEP-0004 (Forms) Fixed the NewClient function by adding parsing of the domain from the JID if no domain is provided in transport config. Updated xmpp_jukebox example * Delete useless pubsub errors * README.md update Fixed import in echo example * Typo * Fixed raw send on client example * Fixed jukebox example and added a README.md * Changelog v0.4.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 411590d..07598b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Fluux XMPP Changelog +## v0.4.0 + +### Changes + +- Added support for XEP-0060 (PubSub) +(no support for 6.5.4 Returning Some Items yet as it needs XEP-0059, Result Sets) +- Added support for XEP-0050 (Commands) +- Added support for XEP-0004 (Forms) +- Updated the client example with a TUI +- Make keepalive interval configurable #134 +- Fix updating of EventManager.CurrentState #136 +- Added callbacks for error management in Component and Client. Users must now provide a callback function when using NewClient/Component. +- Moved JID from xmpp package to stanza package + ## v0.3.0 ### Changes From f3218c4afa0df46b9f3819b06c913a1cf9eb0952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Tue, 14 Jan 2020 19:12:54 +0100 Subject: [PATCH 32/54] PubSub example --- .../xmpp_pubsub_client/xmpp_ps_client.go | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 _examples/xmpp_pubsub_client/xmpp_ps_client.go diff --git a/_examples/xmpp_pubsub_client/xmpp_ps_client.go b/_examples/xmpp_pubsub_client/xmpp_ps_client.go new file mode 100644 index 0000000..972b94d --- /dev/null +++ b/_examples/xmpp_pubsub_client/xmpp_ps_client.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "encoding/xml" + "fmt" + "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" + "log" + "time" +) + +const ( + userJID = "testuser2@localhost" + serverAddress = "localhost:5222" + nodeName = "lel_node" + serviceName = "pubsub.localhost" +) + +func main() { + + config := xmpp.Config{ + TransportConfiguration: xmpp.TransportConfiguration{ + Address: serverAddress, + }, + Jid: userJID, + Credential: xmpp.Password("pass123"), + // StreamLogger: os.Stdout, + Insecure: true, + } + router := xmpp.NewRouter() + router.NewRoute().Packet("message"). + HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { + data, _ := xml.Marshal(p) + fmt.Println("Received a publication ! => \n" + string(data)) + }) + + client, err := xmpp.NewClient(config, router, func(err error) { fmt.Println(err) }) + if err != nil { + log.Fatalf("%+v", err) + } + + // ========================== + // Client connection + err = client.Connect() + if err != nil { + log.Fatalf("%+v", err) + } + + // ========================== + // Create a node + rqCreate, err := stanza.NewCreateNode(serviceName, nodeName) + if err != nil { + log.Fatalf("%+v", err) + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + createCh, err := client.SendIQ(ctx, rqCreate) + if err != nil { + log.Fatalf("%+v", err) + } else { + + if createCh != nil { + select { + case respCr := <-createCh: + // Got response from server + if respCr.Error != nil { + if respCr.Error.Reason != "conflict" { + log.Fatalf("%+v", respCr.Error.Text) + } + fmt.Println(respCr.Error.Text) + } else { + fmt.Print("successfully created channel") + } + case <-time.After(100 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time") + } + } + } + + // ==================================== + // Now let's subscribe to this node : + rqSubscribe, _ := stanza.NewSubRq(serviceName, stanza.SubInfo{ + Node: nodeName, + Jid: userJID, + }) + if err != nil { + log.Fatalf("%+v", err) + } + subRespCh, _ := client.SendIQ(ctx, rqSubscribe) + if subRespCh != nil { + select { + case <-subRespCh: + fmt.Println("Subscribed to the service") + case <-time.After(100 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time") + } + } + + // ========================== + // Publish to that node + pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{ + Publisher: "testuser2", + Any: &stanza.Node{ + XMLName: xml.Name{ + Space: "http://www.w3.org/2005/Atom", + Local: "entry", + }, + Nodes: []stanza.Node{ + { + XMLName: xml.Name{Space: "", Local: "title"}, + Attrs: nil, + Content: "My pub item title", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "summary"}, + Attrs: nil, + Content: "My pub item content summary", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "link"}, + Attrs: []xml.Attr{ + { + Name: xml.Name{Space: "", Local: "rel"}, + Value: "alternate", + }, + { + Name: xml.Name{Space: "", Local: "type"}, + Value: "text/html", + }, + { + Name: xml.Name{Space: "", Local: "href"}, + Value: "http://denmark.lit/2003/12/13/atom03", + }, + }, + }, + { + XMLName: xml.Name{Space: "", Local: "id"}, + Attrs: nil, + Content: "My pub item content ID", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "published"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + { + XMLName: xml.Name{Space: "", Local: "updated"}, + Attrs: nil, + Content: "2003-12-13T18:30:02Z", + Nodes: nil, + }, + }, + }, + }) + + if err != nil { + log.Fatalf("%+v", err) + } + pubRespCh, _ := client.SendIQ(ctx, pub) + if pubRespCh != nil { + select { + case <-pubRespCh: + fmt.Println("Published item to the service") + case <-time.After(100 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time") + } + } + + // ============================= + // Let's purge the node now : + purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName) + client.SendIQ(ctx, purgeRq) + + cancel() +} From 9ca9f48c89c69792b0bfc4b00f8808c545ed8bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Tue, 14 Jan 2020 19:21:29 +0100 Subject: [PATCH 33/54] Added README.md --- _examples/xmpp_pubsub_client/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 _examples/xmpp_pubsub_client/README.md diff --git a/_examples/xmpp_pubsub_client/README.md b/_examples/xmpp_pubsub_client/README.md new file mode 100644 index 0000000..bb44a86 --- /dev/null +++ b/_examples/xmpp_pubsub_client/README.md @@ -0,0 +1,17 @@ +# PubSub client example + +## Description +This is a simple example of a client that : +* Creates a node on a service +* Subscribes to that node +* Publishes to that node +* Gets the notification from the publication and prints it on screen + +## Requirements +You need to hve running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html). + +## How to use +Just run : +``` + go run xmpp_ps_client.go +``` \ No newline at end of file From 20e02cc9ad460e6519cf6d89c4722f00dfd4f49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Tue, 14 Jan 2020 22:47:49 +0100 Subject: [PATCH 34/54] Added node config --- .../xmpp_pubsub_client/xmpp_ps_client.go | 42 +++++++++++++++++++ stanza/form.go | 4 +- stanza/form_test.go | 2 +- stanza/pubsub_owner.go | 31 +++++++++++--- stanza/pubsub_owner_test.go | 4 +- stanza/pubsub_test.go | 4 +- 6 files changed, 74 insertions(+), 13 deletions(-) diff --git a/_examples/xmpp_pubsub_client/xmpp_ps_client.go b/_examples/xmpp_pubsub_client/xmpp_ps_client.go index 972b94d..eba9bbd 100644 --- a/_examples/xmpp_pubsub_client/xmpp_ps_client.go +++ b/_examples/xmpp_pubsub_client/xmpp_ps_client.go @@ -178,5 +178,47 @@ func main() { purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName) client.SendIQ(ctx, purgeRq) + // ============================= + // Configure the node : + confRq, _ := stanza.NewConfigureNode(serviceName, nodeName) + confReqCh, err := client.SendIQ(ctx, confRq) + select { + case confForm := <-confReqCh: + fields, err := confForm.GetFormFields() + if err != nil { + log.Fatal("No config fields found !") + } + + // These are some common fields expected to be present. Change processing to your liking + if fields["pubsub#max_payload_size"] != nil { + fields["pubsub#max_payload_size"].ValuesList[0] = "100000" + } + + if fields["pubsub#notification_type"] != nil { + fields["pubsub#notification_type"].ValuesList[0] = "headline" + } + + submitConf, err := stanza.NewFormSubmissionOwner(serviceName, + nodeName, + []*stanza.Field{ + fields["pubsub#max_payload_size"], + fields["pubsub#notification_type"], + }) + + c, _ := client.SendIQ(ctx, submitConf) + select { + case <-c: + fmt.Println("node configuration was successful") + case <-time.After(300 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time") + + } + + case <-time.After(300 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time") + } + cancel() } diff --git a/stanza/form.go b/stanza/form.go index b9a9932..f22c6ce 100644 --- a/stanza/form.go +++ b/stanza/form.go @@ -17,7 +17,7 @@ type Form struct { XMLName xml.Name `xml:"jabber:x:data x"` Instructions []string `xml:"instructions"` Title string `xml:"title,omitempty"` - Fields []Field `xml:"field,omitempty"` + Fields []*Field `xml:"field,omitempty"` Reported *FormItem `xml:"reported"` Items []FormItem Type string `xml:"type,attr"` @@ -38,7 +38,7 @@ type Field struct { Label string `xml:"label,attr,omitempty"` } -func NewForm(fields []Field, formType string) *Form { +func NewForm(fields []*Field, formType string) *Form { return &Form{ Type: formType, Fields: fields, diff --git a/stanza/form_test.go b/stanza/form_test.go index a68d88e..7346db2 100644 --- a/stanza/form_test.go +++ b/stanza/form_test.go @@ -57,7 +57,7 @@ func TestMarshalFormSubmit(t *testing.T) { Node: serviceNode, Form: &Form{ Type: FormTypeSubmit, - Fields: []Field{ + Fields: []*Field{ {Var: "FORM_TYPE", Type: FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, {Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}}, {Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}}, diff --git a/stanza/pubsub_owner.go b/stanza/pubsub_owner.go index 054303f..9adfbef 100644 --- a/stanza/pubsub_owner.go +++ b/stanza/pubsub_owner.go @@ -198,7 +198,7 @@ func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error form := &Form{ Type: FormTypeSubmit, - Fields: []Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}}, + Fields: []*Field{{Var: "pubsub#node", ValuesList: []string{nodeId}}}, } data, err := xml.Marshal(form) if err != nil { @@ -262,25 +262,44 @@ func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) { return iq, nil } +// NewFormSubmission builds a form submission pubsub IQ, in the Owner namespace +// This is typically used to respond to a form issued by the server when configuring a node. +// See 8.2.4 Form Submission +func NewFormSubmissionOwner(serviceId, nodeName string, fields []*Field) (IQ, error) { + if serviceId == "" || nodeName == "" { + return IQ{}, errors.New("serviceId and nodeName must be filled for this request to be valid") + } + + submitConf := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + submitConf.Payload = &PubSubOwner{ + OwnerUseCase: &ConfigureOwner{ + Node: nodeName, + Form: NewForm(fields, + FormTypeSubmit)}, + } + + return submitConf, nil +} + // GetFormFields gets the fields from a form in a IQ stanza of type result, as a map. // Key is the "var" attribute of the field, and field is the value. // The user can then select and modify the fields they want to alter, and submit a new form to the service using the // NewFormSubmission function to build the IQ. // TODO : remove restriction on IQ type ? -func (iq *IQ) GetFormFields() (map[string]Field, error) { +func (iq *IQ) GetFormFields() (map[string]*Field, error) { if iq.Type != IQTypeResult { return nil, errors.New("this IQ is not a result type IQ. Cannot extract the form from it") } switch payload := iq.Payload.(type) { // We support IOT Control IQ case *PubSubGeneric: - fieldMap := make(map[string]Field) + fieldMap := make(map[string]*Field) for _, elt := range payload.Configure.Form.Fields { fieldMap[elt.Var] = elt } return fieldMap, nil case *PubSubOwner: - fieldMap := make(map[string]Field) + fieldMap := make(map[string]*Field) co, ok := payload.OwnerUseCase.(*ConfigureOwner) if !ok { return nil, errors.New("this IQ does not contain a PubSub payload with a configure tag for the owner namespace") @@ -291,7 +310,7 @@ func (iq *IQ) GetFormFields() (map[string]Field, error) { return fieldMap, nil default: if iq.Any != nil { - fieldMap := make(map[string]Field) + fieldMap := make(map[string]*Field) if iq.Any.XMLName.Local != "command" { return nil, errors.New("this IQ does not contain a form") } @@ -307,7 +326,7 @@ func (iq *IQ) GetFormFields() (map[string]Field, error) { } err = xml.Unmarshal(data, &f) if err == nil { - fieldMap[f.Var] = f + fieldMap[f.Var] = &f } } } diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go index 8af6194..665fba3 100644 --- a/stanza/pubsub_owner_test.go +++ b/stanza/pubsub_owner_test.go @@ -357,7 +357,7 @@ func TestNewApproveSubRequest(t *testing.T) { apprForm := &stanza.Form{ Type: stanza.FormTypeSubmit, - Fields: []stanza.Field{ + Fields: []*stanza.Field{ {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#subscribe_authorization"}}, {Var: "pubsub#subid", ValuesList: []string{"123-abc"}}, {Var: "pubsub#node", ValuesList: []string{"princely_musings"}}, @@ -381,7 +381,7 @@ func TestNewApproveSubRequest(t *testing.T) { for _, f := range frm.Fields { if f.Var == "pubsub#allow" { - allowField = &f + allowField = f } } if allowField == nil || allowField.ValuesList[0] != "true" { diff --git a/stanza/pubsub_test.go b/stanza/pubsub_test.go index 95bf640..6b413c4 100644 --- a/stanza/pubsub_test.go +++ b/stanza/pubsub_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -var submitFormExample = stanza.NewForm([]stanza.Field{ +var submitFormExample = stanza.NewForm([]*stanza.Field{ {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, {Var: "pubsub#title", ValuesList: []string{"Princely Musings (Atom)"}}, {Var: "pubsub#deliver_notifications", ValuesList: []string{"1"}}, @@ -741,7 +741,7 @@ func TestNewCreateAndConfigNode(t *testing.T) { "princely_musings", &stanza.Form{ Type: stanza.FormTypeSubmit, - Fields: []stanza.Field{ + Fields: []*stanza.Field{ {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, {Var: "pubsub#notify_retract", ValuesList: []string{"0"}}, {Var: "pubsub#notify_sub", ValuesList: []string{"0"}}, From 1d1adb0c48afed92c0a8585d389968f5fc0966d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Tue, 14 Jan 2020 23:13:13 +0100 Subject: [PATCH 35/54] Example pubsub code cleanup --- .../xmpp_pubsub_client/xmpp_ps_client.go | 166 +++++++++++------- 1 file changed, 102 insertions(+), 64 deletions(-) diff --git a/_examples/xmpp_pubsub_client/xmpp_ps_client.go b/_examples/xmpp_pubsub_client/xmpp_ps_client.go index eba9bbd..14f0fb0 100644 --- a/_examples/xmpp_pubsub_client/xmpp_ps_client.go +++ b/_examples/xmpp_pubsub_client/xmpp_ps_client.go @@ -32,10 +32,10 @@ func main() { router.NewRoute().Packet("message"). HandlerFunc(func(s xmpp.Sender, p stanza.Packet) { data, _ := xml.Marshal(p) - fmt.Println("Received a publication ! => \n" + string(data)) + log.Println("Received a message ! => \n" + string(data)) }) - client, err := xmpp.NewClient(config, router, func(err error) { fmt.Println(err) }) + client, err := xmpp.NewClient(config, router, func(err error) { log.Println(err) }) if err != nil { log.Fatalf("%+v", err) } @@ -49,11 +49,45 @@ func main() { // ========================== // Create a node + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + createNode(ctx, cancel, client) + + // ============================= + // Configure the node. This can also be done in a single message with the creation + configureNode(ctx, cancel, client) + + // ==================================== + // Subscribe to this node : + subToNode(ctx, cancel, client) + + // ========================== + // Publish to that node + pubToNode(ctx, cancel, client) + + // ============================= + // Let's purge the node : + purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName) + purgeCh, err := client.SendIQ(ctx, purgeRq) + select { + case purgeResp := <-purgeCh: + if purgeResp.Error != nil { + cancel() + log.Fatalf("error while purging node : %s", purgeResp.Error.Text) + } + log.Println("node successfully purged") + case <-time.After(1000 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time while purging node") + } + + cancel() +} + +func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) { rqCreate, err := stanza.NewCreateNode(serviceName, nodeName) if err != nil { log.Fatalf("%+v", err) } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) createCh, err := client.SendIQ(ctx, rqCreate) if err != nil { log.Fatalf("%+v", err) @@ -67,20 +101,73 @@ func main() { if respCr.Error.Reason != "conflict" { log.Fatalf("%+v", respCr.Error.Text) } - fmt.Println(respCr.Error.Text) + log.Println(respCr.Error.Text) } else { fmt.Print("successfully created channel") } case <-time.After(100 * time.Millisecond): cancel() - log.Fatal("No iq response was received in time") + log.Fatal("No iq response was received in time while creating node") } } } +} - // ==================================== - // Now let's subscribe to this node : - rqSubscribe, _ := stanza.NewSubRq(serviceName, stanza.SubInfo{ +func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) { + // First, ask for a form with the config options + confRq, _ := stanza.NewConfigureNode(serviceName, nodeName) + confReqCh, err := client.SendIQ(ctx, confRq) + if err != nil { + log.Fatalf("could not send iq : %v", err) + } + select { + case confForm := <-confReqCh: + // If the request was successful, we now have a form with configuration options to update + fields, err := confForm.GetFormFields() + if err != nil { + log.Fatal("No config fields found !") + } + + // These are some common fields expected to be present. Change processing to your liking + if fields["pubsub#max_payload_size"] != nil { + fields["pubsub#max_payload_size"].ValuesList[0] = "100000" + } + + if fields["pubsub#notification_type"] != nil { + fields["pubsub#notification_type"].ValuesList[0] = "headline" + } + + // Send the modified fields as a form + submitConf, err := stanza.NewFormSubmissionOwner(serviceName, + nodeName, + []*stanza.Field{ + fields["pubsub#max_payload_size"], + fields["pubsub#notification_type"], + }) + + c, _ := client.SendIQ(ctx, submitConf) + select { + case confResp := <-c: + if confResp.Error != nil { + cancel() + log.Fatalf("node configuration failed : %s", confResp.Error.Text) + } + log.Println("node configuration was successful") + return + + case <-time.After(300 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time while configuring the node") + } + + case <-time.After(300 * time.Millisecond): + cancel() + log.Fatal("No iq response was received in time while asking for the config form") + } +} + +func subToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) { + rqSubscribe, err := stanza.NewSubRq(serviceName, stanza.SubInfo{ Node: nodeName, Jid: userJID, }) @@ -91,15 +178,15 @@ func main() { if subRespCh != nil { select { case <-subRespCh: - fmt.Println("Subscribed to the service") - case <-time.After(100 * time.Millisecond): + log.Println("Subscribed to the service") + case <-time.After(300 * time.Millisecond): cancel() - log.Fatal("No iq response was received in time") + log.Fatal("No iq response was received in time while subscribing") } } +} - // ========================== - // Publish to that node +func pubToNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Client) { pub, err := stanza.NewPublishItemRq(serviceName, nodeName, "", stanza.Item{ Publisher: "testuser2", Any: &stanza.Node{ @@ -166,59 +253,10 @@ func main() { if pubRespCh != nil { select { case <-pubRespCh: - fmt.Println("Published item to the service") - case <-time.After(100 * time.Millisecond): - cancel() - log.Fatal("No iq response was received in time") - } - } - - // ============================= - // Let's purge the node now : - purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName) - client.SendIQ(ctx, purgeRq) - - // ============================= - // Configure the node : - confRq, _ := stanza.NewConfigureNode(serviceName, nodeName) - confReqCh, err := client.SendIQ(ctx, confRq) - select { - case confForm := <-confReqCh: - fields, err := confForm.GetFormFields() - if err != nil { - log.Fatal("No config fields found !") - } - - // These are some common fields expected to be present. Change processing to your liking - if fields["pubsub#max_payload_size"] != nil { - fields["pubsub#max_payload_size"].ValuesList[0] = "100000" - } - - if fields["pubsub#notification_type"] != nil { - fields["pubsub#notification_type"].ValuesList[0] = "headline" - } - - submitConf, err := stanza.NewFormSubmissionOwner(serviceName, - nodeName, - []*stanza.Field{ - fields["pubsub#max_payload_size"], - fields["pubsub#notification_type"], - }) - - c, _ := client.SendIQ(ctx, submitConf) - select { - case <-c: - fmt.Println("node configuration was successful") + log.Println("Published item to the service") case <-time.After(300 * time.Millisecond): cancel() - log.Fatal("No iq response was received in time") - + log.Fatal("No iq response was received in time while publishing") } - - case <-time.After(300 * time.Millisecond): - cancel() - log.Fatal("No iq response was received in time") } - - cancel() } From e9bda893d6d7b98e07c6c60559647f950aefc3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Tue, 14 Jan 2020 23:47:18 +0100 Subject: [PATCH 36/54] Added tests for new Owner namespace function --- stanza/pubsub_owner_test.go | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go index 665fba3..b7cf6db 100644 --- a/stanza/pubsub_owner_test.go +++ b/stanza/pubsub_owner_test.go @@ -816,6 +816,58 @@ func TestGetFormFieldsCmd(t *testing.T) { } +func TestNewFormSubmissionOwner(t *testing.T) { + expectedReq := "" + + " " + + " " + + "http://jabber.org/protocol/pubsub#node_config " + + "604800 roster " + + " friends servants " + + "courtiers " + + subR, err := stanza.NewFormSubmissionOwner("pubsub.shakespeare.lit", + "princely_musings", + []*stanza.Field{ + {Var: "FORM_TYPE", Type: stanza.FieldTypeHidden, ValuesList: []string{"http://jabber.org/protocol/pubsub#node_config"}}, + {Var: "pubsub#item_expire", ValuesList: []string{"604800"}}, + {Var: "pubsub#access_model", ValuesList: []string{"roster"}}, + {Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}}, + }) + subR.Id = "config2" + if err != nil { + t.Fatalf("Could not create request : %s", err) + } + + if _, e := checkMarshalling(t, subR); e != nil { + t.Fatalf("Failed to check marshalling for generated sub request : %s", e) + } + + pubsub, ok := subR.Payload.(*stanza.PubSubOwner) + if !ok { + t.Fatalf("payload is not a pubsub in namespace owner !") + } + + conf, ok := pubsub.OwnerUseCase.(*stanza.ConfigureOwner) + if !ok { + t.Fatalf("pubsub does not contain a configure node !") + } + + if conf.Form == nil { + t.Fatalf("the form is absent from the configuration submission !") + } + if len(conf.Form.Fields) != 4 { + t.Fatalf("expected 4 fields, found %d", len(conf.Form.Fields)) + } + if len(conf.Form.Fields[3].ValuesList) != 3 { + t.Fatalf("expected 3 values in fourth field, found %d", len(conf.Form.Fields[3].ValuesList)) + } + + data, err := xml.Marshal(subR) + if err := compareMarshal(expectedReq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) { var respIQ stanza.IQ err := xml.Unmarshal([]byte(response), &respIQ) From 3a3a15507e743c4880ce7a7c70078a962e972f82 Mon Sep 17 00:00:00 2001 From: remicorniere Date: Mon, 20 Jan 2020 12:24:01 +0100 Subject: [PATCH 37/54] Update README.md --- _examples/xmpp_pubsub_client/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_examples/xmpp_pubsub_client/README.md b/_examples/xmpp_pubsub_client/README.md index bb44a86..b86cedd 100644 --- a/_examples/xmpp_pubsub_client/README.md +++ b/_examples/xmpp_pubsub_client/README.md @@ -8,10 +8,10 @@ This is a simple example of a client that : * Gets the notification from the publication and prints it on screen ## Requirements -You need to hve running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html). +You need to have a running jabber server, like [ejabberd](https://www.ejabberd.im/) that supports [XEP-0060](https://xmpp.org/extensions/xep-0060.html). ## How to use Just run : ``` go run xmpp_ps_client.go -``` \ No newline at end of file +``` From 8798ff6fc11aff5cf039352540140395a3aaf265 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Fri, 31 Jan 2020 11:48:03 +0100 Subject: [PATCH 38/54] - Changed IQ stanzas to pointer semantics - Fixed commands from v 0.4.0 and tests - Added primitive Result Sets support (XEP-0059) --- _examples/custom_stanza/custom_stanza.go | 7 +- _examples/delegation/delegation.go | 31 +++-- _examples/go.mod | 2 +- _examples/xmpp_chat_client/go.mod | 2 +- .../xmpp_chat_client/xmpp_chat_client.go | 4 +- _examples/xmpp_component/xmpp_component.go | 23 +++- _examples/xmpp_component2/main.go | 8 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 4 +- .../xmpp_pubsub_client/xmpp_ps_client.go | 24 +++- bi_dir_iterator.go | 12 ++ client.go | 2 +- client_test.go | 13 +- cmd/fluuxmpp/send.go | 6 +- component.go | 2 +- component_test.go | 12 +- router.go | 12 +- router_test.go | 51 +++++-- session.go | 2 +- stanza/commands.go | 18 ++- stanza/commands_test.go | 43 +++--- stanza/component.go | 9 +- stanza/component_test.go | 2 +- stanza/form.go | 17 +-- stanza/form_test.go | 5 +- stanza/iot.go | 11 +- stanza/iq.go | 49 +++++-- stanza/iq_disco.go | 24 +++- stanza/iq_disco_test.go | 10 +- stanza/iq_roster.go | 11 ++ stanza/iq_roster_test.go | 7 +- stanza/iq_test.go | 30 ++++- stanza/iq_version.go | 8 +- stanza/iq_version_test.go | 5 +- stanza/msg_chat_markers.go | 8 +- stanza/msg_chat_state.go | 10 +- stanza/msg_html.go | 2 +- stanza/msg_oob.go | 2 +- stanza/msg_pubsub_event.go | 1 - stanza/msg_receipts.go | 4 +- stanza/node_test.go | 5 +- stanza/pres_muc.go | 2 +- stanza/pubsub.go | 125 ++++++++++++------ stanza/pubsub_owner.go | 102 ++++++++++---- stanza/pubsub_owner_test.go | 44 +++++- stanza/pubsub_test.go | 36 ++++- stanza/results_sets.go | 29 ++++ stanza/results_sets_test.go | 79 +++++++++++ stanza/sasl_auth.go | 12 ++ stanza/sasl_auth_test.go | 5 +- stanza/xmpp_test.go | 2 +- stream_manager.go | 4 +- tcp_server_mock.go | 7 +- transport.go | 6 +- xmpp_transport.go | 2 +- 54 files changed, 724 insertions(+), 229 deletions(-) create mode 100644 bi_dir_iterator.go create mode 100644 stanza/results_sets.go create mode 100644 stanza/results_sets_test.go diff --git a/_examples/custom_stanza/custom_stanza.go b/_examples/custom_stanza/custom_stanza.go index 46043f2..5320565 100644 --- a/_examples/custom_stanza/custom_stanza.go +++ b/_examples/custom_stanza/custom_stanza.go @@ -9,7 +9,10 @@ import ( ) func main() { - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "service.localhost", Id: "custom-pl-1"}) + if err != nil { + log.Fatalf("failed to create IQ: %v", err) + } payload := CustomPayload{XMLName: xml.Name{Space: "my:custom:payload", Local: "query"}, Node: "test"} iq.Payload = payload @@ -45,5 +48,5 @@ func (c CustomPayload) Namespace() string { } func init() { - stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{"my:custom:payload", "query"}, CustomPayload{}) + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "my:custom:payload", Local: "query"}, CustomPayload{}) } diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go index d07587a..d022633 100644 --- a/_examples/delegation/delegation.go +++ b/_examples/delegation/delegation.go @@ -35,7 +35,9 @@ func main() { IQNamespaces("urn:xmpp:delegation:1"). HandlerFunc(handleDelegation) - component, err := xmpp.NewComponent(opts, router) + component, err := xmpp.NewComponent(opts, router, func(err error) { + log.Println(err) + }) if err != nil { log.Fatalf("%+v", err) } @@ -78,7 +80,7 @@ const ( // ctx.Opts func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { // Type conversion & sanity checks - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok { return } @@ -87,15 +89,18 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { return } - iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) + iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) + if err != nil { + log.Fatalf("failed to create IQ response: %v", err) + } switch info.Node { case "": - discoInfoRoot(&iqResp, opts) + discoInfoRoot(iqResp, opts) case pubsubNode: - discoInfoPubSub(&iqResp) + discoInfoPubSub(iqResp) case pepNode: - discoInfoPEP(&iqResp) + discoInfoPEP(iqResp) } _ = c.Send(iqResp) @@ -155,7 +160,7 @@ func discoInfoPEP(iqResp *stanza.IQ) { func handleDelegation(s xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok { return } @@ -166,7 +171,7 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { } forwardedPacket := delegation.Forwarded.Stanza fmt.Println(forwardedPacket) - forwardedIQ, ok := forwardedPacket.(stanza.IQ) + forwardedIQ, ok := forwardedPacket.(*stanza.IQ) if !ok { return } @@ -179,7 +184,10 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { if pubsub.Publish.XMLName.Local == "publish" { // Prepare pubsub IQ reply - iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) + iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: forwardedIQ.To, To: forwardedIQ.From, Id: forwardedIQ.Id}) + if err != nil { + log.Fatalf("failed to create iqResp: %v", err) + } payload := stanza.PubSubGeneric{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/pubsub", @@ -188,7 +196,10 @@ func handleDelegation(s xmpp.Sender, p stanza.Packet) { } iqResp.Payload = &payload // Wrap the reply in delegation 'forward' - iqForward := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) + iqForward, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id}) + if err != nil { + log.Fatalf("failed to create iqForward: %v", err) + } delegPayload := stanza.Delegation{ XMLName: xml.Name{ Space: "urn:xmpp:delegation:1", diff --git a/_examples/go.mod b/_examples/go.mod index 779d698..1dd46c2 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -5,7 +5,7 @@ go 1.13 require ( github.com/processone/mpg123 v1.0.0 github.com/processone/soundcloud v1.0.0 - gosrc.io/xmpp v0.1.1 + gosrc.io/xmpp v0.4.0 ) replace gosrc.io/xmpp => ./../ diff --git a/_examples/xmpp_chat_client/go.mod b/_examples/xmpp_chat_client/go.mod index 267b416..60d9744 100644 --- a/_examples/xmpp_chat_client/go.mod +++ b/_examples/xmpp_chat_client/go.mod @@ -6,5 +6,5 @@ require ( github.com/awesome-gocui/gocui v0.6.1-0.20191115151952-a34ffb055986 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.1 - gosrc.io/xmpp v0.3.1-0.20191223080939-f8f820170e08 + gosrc.io/xmpp v0.4.0 ) diff --git a/_examples/xmpp_chat_client/xmpp_chat_client.go b/_examples/xmpp_chat_client/xmpp_chat_client.go index 0d4b94b..51e3bcf 100644 --- a/_examples/xmpp_chat_client/xmpp_chat_client.go +++ b/_examples/xmpp_chat_client/xmpp_chat_client.go @@ -186,7 +186,7 @@ func startClient(g *gocui.Gui, config *config) { // ========================== // Start working - updateRosterFromConfig(g, config) + updateRosterFromConfig(config) // Sending the default contact in a channel. Default value is the first contact in the list from the config. viewState.currentContact = strings.Split(config.Contacts, configContactSep)[0] // Informing user of the default contact @@ -283,7 +283,7 @@ func errorHandler(err error) { // Read the client roster from the config. This does not check with the server that the roster is correct. // If user tries to send a message to someone not registered with the server, the server will return an error. -func updateRosterFromConfig(g *gocui.Gui, config *config) { +func updateRosterFromConfig(config *config) { viewState.contacts = append(strings.Split(config.Contacts, configContactSep), backFromContacts) // Put a "go back" button at the end of the list viewState.contacts = append(viewState.contacts, backFromContacts) diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go index 7f676cb..e3a70ce 100644 --- a/_examples/xmpp_component/xmpp_component.go +++ b/_examples/xmpp_component/xmpp_component.go @@ -61,12 +61,16 @@ func handleMessage(_ xmpp.Sender, p stanza.Packet) { func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { // Type conversion & sanity checks - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok || iq.Type != stanza.IQTypeGet { return } - iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + // TODO: fix this... + if err != nil { + return + } disco := iqResp.DiscoInfo() disco.AddIdentity(opts.Name, opts.Category, opts.Type) disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1") @@ -76,7 +80,7 @@ func discoInfo(c xmpp.Sender, p stanza.Packet, opts xmpp.ComponentOptions) { // TODO: Handle iq error responses func discoItems(c xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok || iq.Type != stanza.IQTypeGet { return } @@ -86,7 +90,11 @@ func discoItems(c xmpp.Sender, p stanza.Packet) { return } - iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + // TODO: fix this... + iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + if err != nil { + return + } items := iqResp.DiscoItems() if discoItems.Node == "" { @@ -97,12 +105,15 @@ func discoItems(c xmpp.Sender, p stanza.Packet) { func handleVersion(c xmpp.Sender, p stanza.Packet) { // Type conversion & sanity checks - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok { return } - iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id, Lang: "en"}) + if err != nil { + return + } iqResp.Version().SetInfo("Fluux XMPP Component", "0.0.1", "") _ = c.Send(iqResp) } diff --git a/_examples/xmpp_component2/main.go b/_examples/xmpp_component2/main.go index 6bde7eb..01415a0 100644 --- a/_examples/xmpp_component2/main.go +++ b/_examples/xmpp_component2/main.go @@ -9,9 +9,10 @@ Connect to an XMPP server using XEP 114 protocol, perform a discovery query on t import ( "context" "fmt" + "log" "time" - xmpp "gosrc.io/xmpp" + "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" ) @@ -53,10 +54,13 @@ func main() { } // make a disco iq - iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, + iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: domain, To: "localhost", Id: "my-iq1"}) + if err != nil { + log.Fatalf("failed to create IQ: %v", err) + } disco := iqReq.DiscoInfo() iqReq.Payload = disco diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index 57137b8..b8075a2 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -81,7 +81,7 @@ func handleMessage(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { } func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok { return } @@ -100,7 +100,7 @@ func handleIQ(s xmpp.Sender, p stanza.Packet, player *mpg123.Player) { setResponse := new(stanza.ControlSetResponse) // FIXME: Broken reply := stanza.IQ{Attrs: stanza.Attrs{To: iq.From, Type: "result", Id: iq.Id}, Payload: setResponse} - _ = s.Send(reply) + _ = s.Send(&reply) // TODO add Soundclound artist / title retrieval sendUserTune(s, "Radiohead", "Spectre") default: diff --git a/_examples/xmpp_pubsub_client/xmpp_ps_client.go b/_examples/xmpp_pubsub_client/xmpp_ps_client.go index 14f0fb0..b2e9cf6 100644 --- a/_examples/xmpp_pubsub_client/xmpp_ps_client.go +++ b/_examples/xmpp_pubsub_client/xmpp_ps_client.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/xml" + "errors" "fmt" "gosrc.io/xmpp" "gosrc.io/xmpp/stanza" @@ -17,6 +18,8 @@ const ( serviceName = "pubsub.localhost" ) +var invalidResp = errors.New("invalid response") + func main() { config := xmpp.Config{ @@ -52,7 +55,7 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) createNode(ctx, cancel, client) - // ============================= + // ================================================================================ // Configure the node. This can also be done in a single message with the creation configureNode(ctx, cancel, client) @@ -68,10 +71,17 @@ func main() { // Let's purge the node : purgeRq, _ := stanza.NewPurgeAllItems(serviceName, nodeName) purgeCh, err := client.SendIQ(ctx, purgeRq) + if err != nil { + log.Fatalf("could not send purge request: %v", err) + } select { case purgeResp := <-purgeCh: - if purgeResp.Error != nil { + + if purgeResp.Type == stanza.IQTypeError { cancel() + if vld, err := purgeResp.IsValid(); !vld { + log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", purgeResp, err) + } log.Fatalf("error while purging node : %s", purgeResp.Error.Text) } log.Println("node successfully purged") @@ -97,7 +107,10 @@ func createNode(ctx context.Context, cancel context.CancelFunc, client *xmpp.Cli select { case respCr := <-createCh: // Got response from server - if respCr.Error != nil { + if respCr.Type == stanza.IQTypeError { + if vld, err := respCr.IsValid(); !vld { + log.Fatalf(invalidResp.Error()+" %+v"+" reason: %s", respCr, err) + } if respCr.Error.Reason != "conflict" { log.Fatalf("%+v", respCr.Error.Text) } @@ -148,8 +161,11 @@ func configureNode(ctx context.Context, cancel context.CancelFunc, client *xmpp. c, _ := client.SendIQ(ctx, submitConf) select { case confResp := <-c: - if confResp.Error != nil { + if confResp.Type == stanza.IQTypeError { cancel() + if vld, err := confResp.IsValid(); !vld { + log.Fatalf(invalidResp.Error()+" %v"+" reason: %v", confResp, err) + } log.Fatalf("node configuration failed : %s", confResp.Error.Text) } log.Println("node configuration was successful") diff --git a/bi_dir_iterator.go b/bi_dir_iterator.go new file mode 100644 index 0000000..5293b1b --- /dev/null +++ b/bi_dir_iterator.go @@ -0,0 +1,12 @@ +package xmpp + +type BiDirIterator interface { + // Next returns the next element of this iterator, if a response is available within t milliseconds + Next(t int) (BiDirIteratorElt, error) + // Previous returns the previous element of this iterator, if a response is available within t milliseconds + Previous(t int) (BiDirIteratorElt, error) +} + +type BiDirIteratorElt interface { + NoOp() +} diff --git a/client.go b/client.go index a795934..bd40c38 100644 --- a/client.go +++ b/client.go @@ -264,7 +264,7 @@ func (c *Client) Send(packet stanza.Packet) error { // ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second) // result := <- client.SendIQ(ctx, iq) // -func (c *Client) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) { +func (c *Client) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) { if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet { return nil, ErrCanOnlySendGetOrSetIq } diff --git a/client_test.go b/client_test.go index 5af099a..d6ebf99 100644 --- a/client_test.go +++ b/client_test.go @@ -171,7 +171,11 @@ func TestClient_SendIQ(t *testing.T) { client, mock := mockClientConnection(t, h, testClientIqPort) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + if err != nil { + t.Fatalf("failed to create the IQ request: %v", err) + } + disco := iqReq.DiscoInfo() iqReq.Payload = disco @@ -219,7 +223,10 @@ func TestClient_SendIQFail(t *testing.T) { //================== // Create an IQ to send ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + if err != nil { + t.Fatalf("failed to create IQ request: %v", err) + } disco := iqReq.DiscoInfo() iqReq.Payload = disco // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified @@ -387,7 +394,7 @@ func handlerClientConnectSuccess(t *testing.T, sc *ServerConn) { checkClientOpenStream(t, sc) sendStreamFeatures(t, sc) // Send initial features readAuth(t, sc.decoder) - fmt.Fprintln(sc.connection, "") + sc.connection.Write([]byte("")) checkClientOpenStream(t, sc) // Reset stream sendBindFeature(t, sc) // Send post auth features diff --git a/cmd/fluuxmpp/send.go b/cmd/fluuxmpp/send.go index 5a10c9e..7e7ed97 100644 --- a/cmd/fluuxmpp/send.go +++ b/cmd/fluuxmpp/send.go @@ -38,7 +38,11 @@ func sendxmpp(cmd *cobra.Command, args []string) { }, Jid: viper.GetString("jid"), Credential: xmpp.Password(viper.GetString("password")), - }, xmpp.NewRouter()) + }, + xmpp.NewRouter(), + func(err error) { + log.Println(err) + }) if err != nil { log.Errorf("error when starting xmpp client: %s", err) diff --git a/component.go b/component.go index bd85aa2..a57b07b 100644 --- a/component.go +++ b/component.go @@ -185,7 +185,7 @@ func (c *Component) sendWithWriter(writer io.Writer, packet []byte) error { // ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second) // result := <- client.SendIQ(ctx, iq) // -func (c *Component) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) { +func (c *Component) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) { if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet { return nil, ErrCanOnlySendGetOrSetIq } diff --git a/component_test.go b/component_test.go index 73d8947..4af4b2c 100644 --- a/component_test.go +++ b/component_test.go @@ -93,7 +93,7 @@ func TestGenerateHandshakeId(t *testing.T) { // Try connecting, and storing the resulting streamID in a map. m := make(map[string]bool) - for _, _ = range uuidsArray { + for range uuidsArray { streamId, _ := c.transport.Connect() m[c.handshake(streamId)] = true } @@ -131,7 +131,10 @@ func TestSendIq(t *testing.T) { c, m := mockComponentConnection(t, testSendIqPort, h) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + if err != nil { + t.Fatalf("failed to create IQ request: %v", err) + } disco := iqReq.DiscoInfo() iqReq.Payload = disco @@ -173,7 +176,10 @@ func TestSendIqFail(t *testing.T) { c, m := mockComponentConnection(t, testSendIqFailPort, h) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - iqReq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + iqReq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "test1@localhost/mremond-mbp", To: defaultServerName, Id: defaultStreamID, Lang: "en"}) + if err != nil { + t.Fatalf("failed to create IQ request: %v", err) + } // Removing the id to make the stanza invalid. The IQ constructor makes a random one if none is specified // so we need to overwrite it. diff --git a/router.go b/router.go index 23a134e..f20af5b 100644 --- a/router.go +++ b/router.go @@ -42,7 +42,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 stanza.Packet) { - iq, isIq := p.(stanza.IQ) + iq, isIq := p.(*stanza.IQ) if isIq { r.IQResultRouteLock.RLock() route, ok := r.IQResultRoutes[iq.Id] @@ -51,7 +51,7 @@ func (r *Router) route(s Sender, p stanza.Packet) { r.IQResultRouteLock.Lock() delete(r.IQResultRoutes, iq.Id) r.IQResultRouteLock.Unlock() - route.result <- iq + route.result <- *iq close(route.result) return } @@ -70,7 +70,7 @@ func (r *Router) route(s Sender, p stanza.Packet) { } } -func iqNotImplemented(s Sender, iq stanza.IQ) { +func iqNotImplemented(s Sender, iq *stanza.IQ) { err := stanza.Err{ XMLName: xml.Name{Local: "error"}, Code: 501, @@ -232,7 +232,7 @@ func (n nameMatcher) Match(p stanza.Packet, match *RouteMatch) bool { switch p.(type) { case stanza.Message: name = "message" - case stanza.IQ: + case *stanza.IQ: name = "iq" case stanza.Presence: name = "presence" @@ -259,7 +259,7 @@ type nsTypeMatcher []string func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool { var stanzaType stanza.StanzaType switch packet := p.(type) { - case stanza.IQ: + case *stanza.IQ: stanzaType = packet.Type case stanza.Presence: stanzaType = packet.Type @@ -291,7 +291,7 @@ func (r *Route) StanzaType(types ...string) *Route { type nsIQMatcher []string func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool { - iq, ok := p.(stanza.IQ) + iq, ok := p.(*stanza.IQ) if !ok { return false } diff --git a/router_test.go b/router_test.go index 2b5cf82..677ad0e 100644 --- a/router_test.go +++ b/router_test.go @@ -25,7 +25,10 @@ func TestIQResultRoutes(t *testing.T) { // Check if the IQ handler was called ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, Id: "1234"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } res := router.NewIQResultRoute(ctx, "1234") go router.route(conn, iq) select { @@ -71,7 +74,10 @@ func TestNameMatcher(t *testing.T) { // Check that an IQ packet is not matched conn = NewSenderMock() - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } iq.Payload = &stanza.DiscoInfo{} router.route(conn, iq) if conn.String() == successFlag { @@ -89,7 +95,10 @@ func TestIQNSMatcher(t *testing.T) { // Check that an IQ with proper namespace does match conn := NewSenderMock() - iqDisco := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + iqDisco, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create iqDisco: %v", err) + } // TODO: Add a function to generate payload with proper namespace initialisation iqDisco.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ @@ -103,7 +112,10 @@ func TestIQNSMatcher(t *testing.T) { // Check that another namespace is not matched conn = NewSenderMock() - iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, To: "localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create iqVersion: %v", err) + } // TODO: Add a function to generate payload with proper namespace initialisation iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ @@ -146,7 +158,10 @@ func TestTypeMatcher(t *testing.T) { // We do not match on other types conn = NewSenderMock() - iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create iqVersion: %v", err) + } iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", @@ -169,22 +184,31 @@ func TestCompositeMatcher(t *testing.T) { }) // Data set - getVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + getVersionIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create getVersionIq: %v", err) + } getVersionIq.Payload = &stanza.Version{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", }} - setVersionIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"}) + setVersionIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, From: "service.localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create setVersionIq: %v", err) + } setVersionIq.Payload = &stanza.Version{ XMLName: xml.Name{ Space: "jabber:iq:version", Local: "query", }} - GetDiscoIq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) - GetDiscoIq.Payload = &stanza.DiscoInfo{ + getDiscoIq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create getDiscoIq: %v", err) + } + getDiscoIq.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "http://jabber.org/protocol/disco#info", Local: "query", @@ -200,7 +224,7 @@ func TestCompositeMatcher(t *testing.T) { }{ {name: "match get version iq", input: getVersionIq, want: true}, {name: "ignore set version iq", input: setVersionIq, want: false}, - {name: "ignore get discoinfo iq", input: GetDiscoIq, want: false}, + {name: "ignore get discoinfo iq", input: getDiscoIq, want: false}, {name: "ignore message", input: message, want: false}, } @@ -238,7 +262,10 @@ func TestCatchallMatcher(t *testing.T) { } conn = NewSenderMock() - iqVersion := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + iqVersion, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "service.localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create iqVersion: %v", err) + } iqVersion.Payload = &stanza.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", @@ -274,7 +301,7 @@ func (s SenderMock) Send(packet stanza.Packet) error { return nil } -func (s SenderMock) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) { +func (s SenderMock) SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) { out, err := xml.Marshal(iq) if err != nil { return nil, err diff --git a/session.go b/session.go index 6b9c75a..182e32b 100644 --- a/session.go +++ b/session.go @@ -97,7 +97,7 @@ func (s *Session) startTlsIfSupported(o Config) { if !s.transport.DoesStartTLS() { if !o.Insecure { - s.err = errors.New("Transport does not support starttls") + s.err = errors.New("transport does not support starttls") } return } diff --git a/stanza/commands.go b/stanza/commands.go index 5a3191f..6f9d3bc 100644 --- a/stanza/commands.go +++ b/stanza/commands.go @@ -23,7 +23,7 @@ const ( type Command struct { XMLName xml.Name `xml:"http://jabber.org/protocol/commands command"` - CommandElement CommandElement `xml:",any"` + CommandElement CommandElement BadAction *struct{} `xml:"bad-action,omitempty"` BadLocale *struct{} `xml:"bad-locale,omitempty"` @@ -38,12 +38,19 @@ type Command struct { SessionId string `xml:"sessionid,attr,omitempty"` Status string `xml:"status,attr,omitempty"` Lang string `xml:"lang,attr,omitempty"` + + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (c *Command) Namespace() string { return c.XMLName.Space } +func (c *Command) GetSet() *ResultSet { + return c.ResultSet +} + type CommandElement interface { Ref() string } @@ -68,6 +75,7 @@ type Note struct { func (n *Note) Ref() string { return "note" } +func (f *Form) Ref() string { return "form" } func (n *Node) Ref() string { return "node" @@ -117,6 +125,10 @@ func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { nt := Note{} d.DecodeElement(&nt, &tt) c.CommandElement = &nt + case "x": + f := Form{} + d.DecodeElement(&f, &tt) + c.CommandElement = &f default: n := Node{} e := d.DecodeElement(&n, &tt) @@ -134,3 +146,7 @@ func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } } } + +func init() { + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "http://jabber.org/protocol/commands", Local: "command"}, Command{}) +} diff --git a/stanza/commands_test.go b/stanza/commands_test.go index 4cdee0f..a72e5aa 100644 --- a/stanza/commands_test.go +++ b/stanza/commands_test.go @@ -7,34 +7,21 @@ import ( ) func TestMarshalCommands(t *testing.T) { - input := "Available Servi" + - "ces<" + - "field xmlns=\"jabber:x:data\" var=\"service\">httpdoffoffononpostgresqloffoffon" + - "onjabberdoffoffonon" - + input := "Available Services" + + "" + + "httpd" + + "offoff" + + "onon" + + "postgresql" + + "offoff" + + "onon" + + "jabberd" + + "offoff" + + "onon" var c stanza.Command err := xml.Unmarshal([]byte(input), &c) diff --git a/stanza/component.go b/stanza/component.go index 32a36b0..ba3b81e 100644 --- a/stanza/component.go +++ b/stanza/component.go @@ -42,11 +42,16 @@ type Delegation struct { 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 + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (d *Delegation) Namespace() string { return d.XMLName.Space } +func (d *Delegation) GetSet() *ResultSet { + return d.ResultSet +} // Forwarded is used to wrapped forwarded stanzas. // TODO: Move it in another file, as it is not limited to components. @@ -86,6 +91,6 @@ type Delegated struct { } func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{}) - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:delegation:1", Local: "delegation"}, Delegation{}) } diff --git a/stanza/component_test.go b/stanza/component_test.go index 648131c..2fb1672 100644 --- a/stanza/component_test.go +++ b/stanza/component_test.go @@ -61,7 +61,7 @@ func TestParsingDelegationIQ(t *testing.T) { if iq.Payload != nil { if delegation, ok := iq.Payload.(*Delegation); ok { packet := delegation.Forwarded.Stanza - forwardedIQ, ok := packet.(IQ) + forwardedIQ, ok := packet.(*IQ) if !ok { t.Errorf("Could not extract packet IQ") return diff --git a/stanza/form.go b/stanza/form.go index f22c6ce..f758f74 100644 --- a/stanza/form.go +++ b/stanza/form.go @@ -14,17 +14,18 @@ const ( // See XEP-0004 and XEP-0068 // Pointer semantics type Form struct { - XMLName xml.Name `xml:"jabber:x:data x"` - Instructions []string `xml:"instructions"` - Title string `xml:"title,omitempty"` - Fields []*Field `xml:"field,omitempty"` - Reported *FormItem `xml:"reported"` - Items []FormItem - Type string `xml:"type,attr"` + XMLName xml.Name `xml:"jabber:x:data x"` + Instructions []string `xml:"instructions"` + Title string `xml:"title,omitempty"` + Fields []*Field `xml:"field,omitempty"` + Reported *FormItem `xml:"reported"` + Items []FormItem `xml:"item,omitempty"` + Type string `xml:"type,attr"` } type FormItem struct { - Fields []Field + XMLName xml.Name + Fields []Field `xml:"field,omitempty"` } type Field struct { diff --git a/stanza/form_test.go b/stanza/form_test.go index 7346db2..ea6c613 100644 --- a/stanza/form_test.go +++ b/stanza/form_test.go @@ -51,7 +51,10 @@ const ( ) func TestMarshalFormSubmit(t *testing.T) { - formIQ := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet}) + formIQ, err := NewIQ(Attrs{From: clientJid, To: serviceJid, Id: iqId, Type: IQTypeSet}) + if err != nil { + t.Fatalf("failed to create formIQ: %v", err) + } formIQ.Payload = &PubSubOwner{ OwnerUseCase: &ConfigureOwner{ Node: serviceNode, diff --git a/stanza/iot.go b/stanza/iot.go index 5e15056..b6952e0 100644 --- a/stanza/iot.go +++ b/stanza/iot.go @@ -7,12 +7,18 @@ import ( type ControlSet struct { XMLName xml.Name `xml:"urn:xmpp:iot:control set"` Fields []ControlField `xml:",any"` + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (c *ControlSet) Namespace() string { return c.XMLName.Space } +func (c *ControlSet) GetSet() *ResultSet { + return c.ResultSet +} + type ControlGetForm struct { XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"` } @@ -30,10 +36,13 @@ type ControlSetResponse struct { func (c *ControlSetResponse) Namespace() string { return c.XMLName.Space } +func (c *ControlSetResponse) GetSet() *ResultSet { + return nil +} // ============================================================================ // Registry init func init() { - TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:iot:control", "set"}, ControlSet{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "urn:xmpp:iot:control", Local: "set"}, ControlSet{}) } diff --git a/stanza/iq.go b/stanza/iq.go index 499c261..a8cd62a 100644 --- a/stanza/iq.go +++ b/stanza/iq.go @@ -2,6 +2,7 @@ package stanza import ( "encoding/xml" + "errors" "strings" "github.com/google/uuid" @@ -31,22 +32,28 @@ type IQ struct { // Info/Query type IQPayload interface { Namespace() string + GetSet() *ResultSet } -func NewIQ(a Attrs) IQ { - // TODO ensure that type is set, as it is required +func NewIQ(a Attrs) (*IQ, error) { if a.Id == "" { if id, err := uuid.NewRandom(); err == nil { a.Id = id.String() } } - return IQ{ + + iq := IQ{ XMLName: xml.Name{Local: "iq"}, Attrs: a, } + + if iq.Type.IsEmpty() { + return nil, IqTypeUnset + } + return &iq, nil } -func (iq IQ) MakeError(xerror Err) IQ { +func (iq *IQ) MakeError(xerror Err) *IQ { from := iq.From to := iq.To @@ -58,18 +65,23 @@ func (iq IQ) MakeError(xerror Err) IQ { return iq } -func (IQ) Name() string { +func (*IQ) Name() string { return "iq" } +// NoOp to implement BiDirIteratorElt +func (*IQ) NoOp() { + +} + type iqDecoder struct{} var iq iqDecoder -func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (IQ, error) { +func (iqDecoder) decode(p *xml.Decoder, se xml.StartElement) (*IQ, error) { var packet IQ err := p.DecodeElement(&packet, &se) - return packet, err + return &packet, err } // UnmarshalXML implements custom parsing for IQs @@ -134,38 +146,47 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } } +var ( + IqTypeUnset = errors.New("iq type is not set but is mandatory") + IqIDUnset = errors.New("iq stanza ID is not set but is mandatory") + IqSGetNoPl = errors.New("iq is of type get or set but has no payload") + IqResNoPl = errors.New("iq is of type result but has no payload") + IqErrNoErrPl = errors.New("iq is of type error but has no error payload") +) + +// IsValid checks if the IQ is valid. If not, return an error with the reason as a message // Following RFC-3920 for IQs -func (iq *IQ) IsValid() bool { +func (iq *IQ) IsValid() (bool, error) { // ID is required if len(strings.TrimSpace(iq.Id)) == 0 { - return false + return false, IqIDUnset } // Type is required if iq.Type.IsEmpty() { - return false + return false, IqTypeUnset } // Type get and set must contain one and only one child element that specifies the semantics if iq.Type == IQTypeGet || iq.Type == IQTypeSet { if iq.Payload == nil && iq.Any == nil { - return false + return false, IqSGetNoPl } } // A result must include zero or one child element if iq.Type == IQTypeResult { if iq.Payload != nil && iq.Any != nil { - return false + return false, IqResNoPl } } //Error type must contain an "error" child element if iq.Type == IQTypeError { if iq.Error == nil { - return false + return false, IqErrNoErrPl } } - return true + return true, nil } diff --git a/stanza/iq_disco.go b/stanza/iq_disco.go index 7b9acac..8e50f90 100644 --- a/stanza/iq_disco.go +++ b/stanza/iq_disco.go @@ -16,10 +16,11 @@ const ( // 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"` + ResultSet *ResultSet `xml:"set,omitempty"` } // Namespace lets DiscoInfo implement the IQPayload interface @@ -27,6 +28,10 @@ func (d *DiscoInfo) Namespace() string { return d.XMLName.Space } +func (d *DiscoInfo) GetSet() *ResultSet { + return d.ResultSet +} + // --------------- // Builder helpers @@ -102,12 +107,19 @@ type DiscoItems struct { XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"` Node string `xml:"node,attr,omitempty"` Items []DiscoItem `xml:"item"` + + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (d *DiscoItems) Namespace() string { return d.XMLName.Space } +func (d *DiscoItems) GetSet() *ResultSet { + return d.ResultSet +} + // --------------- // Builder helpers @@ -146,6 +158,6 @@ type DiscoItem struct { // 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{Space: NSDiscoInfo, Local: "query"}, DiscoInfo{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: NSDiscoItems, Local: "query"}, DiscoItems{}) } diff --git a/stanza/iq_disco_test.go b/stanza/iq_disco_test.go index 1f8ab8b..87b7001 100644 --- a/stanza/iq_disco_test.go +++ b/stanza/iq_disco_test.go @@ -9,7 +9,10 @@ import ( // Test DiscoInfo Builder with several features func TestDiscoInfo_Builder(t *testing.T) { - iq := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: "get", To: "service.localhost", Id: "disco-get-1"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } disco := iq.DiscoInfo() disco.AddIdentity("Test Component", "gateway", "service") disco.AddFeatures(stanza.NSDiscoInfo, stanza.NSDiscoItems, "jabber:iq:version", "urn:xmpp:delegation:1") @@ -50,8 +53,11 @@ func TestDiscoInfo_Builder(t *testing.T) { // Implements XEP-0030 example 17 // https://xmpp.org/extensions/xep-0030.html#example-17 func TestDiscoItems_Builder(t *testing.T) { - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit", + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "catalog.shakespeare.lit", To: "romeo@montague.net/orchard", Id: "items-2"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } iq.DiscoItems(). AddItem("catalog.shakespeare.lit", "books", "Books by and about Shakespeare"). AddItem("catalog.shakespeare.lit", "clothing", "Wear your literary taste with pride"). diff --git a/stanza/iq_roster.go b/stanza/iq_roster.go index 1923013..34ab5f0 100644 --- a/stanza/iq_roster.go +++ b/stanza/iq_roster.go @@ -35,12 +35,17 @@ const ( // Roster struct represents Roster IQs type Roster struct { XMLName xml.Name `xml:"jabber:iq:roster query"` + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } // Namespace defines the namespace for the RosterIQ func (r *Roster) Namespace() string { return r.XMLName.Space } +func (r *Roster) GetSet() *ResultSet { + return r.ResultSet +} // --------------- // Builder helpers @@ -64,6 +69,8 @@ func (iq *IQ) RosterIQ() *Roster { type RosterItems struct { XMLName xml.Name `xml:"jabber:iq:roster query"` Items []RosterItem `xml:"item"` + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } // Namespace lets RosterItems implement the IQPayload interface @@ -71,6 +78,10 @@ func (r *RosterItems) Namespace() string { return r.XMLName.Space } +func (r *RosterItems) GetSet() *ResultSet { + return r.ResultSet +} + // RosterItem represents an item in the roster iq type RosterItem struct { XMLName xml.Name `xml:"jabber:iq:roster item"` diff --git a/stanza/iq_roster_test.go b/stanza/iq_roster_test.go index ca891df..8eee77f 100644 --- a/stanza/iq_roster_test.go +++ b/stanza/iq_roster_test.go @@ -7,7 +7,10 @@ import ( ) func TestRosterBuilder(t *testing.T) { - iq := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"}) + iq, err := NewIQ(Attrs{Type: IQTypeResult, From: "romeo@montague.net/orchard"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } var noGroup []string iq.RosterItems().AddItem("xl8ceawrfu8zdneomw1h6h28d@crypho.com", @@ -91,7 +94,7 @@ func TestRosterBuilder(t *testing.T) { } } -func checkMarshalling(t *testing.T, iq IQ) (*IQ, error) { +func checkMarshalling(t *testing.T, iq *IQ) (*IQ, error) { // Marshall data, err := xml.Marshal(iq) if err != nil { diff --git a/stanza/iq_test.go b/stanza/iq_test.go index 3223566..e5af6de 100644 --- a/stanza/iq_test.go +++ b/stanza/iq_test.go @@ -36,24 +36,36 @@ func TestUnmarshalIqs(t *testing.T) { func TestGenerateIqId(t *testing.T) { t.Parallel() - iq := stanza.NewIQ(stanza.Attrs{Id: "1"}) + iq, err := stanza.NewIQ(stanza.Attrs{Id: "1", Type: "dummy type"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } if iq.Id != "1" { t.Errorf("NewIQ replaced id with %s", iq.Id) } - iq = stanza.NewIQ(stanza.Attrs{}) + iq, err = stanza.NewIQ(stanza.Attrs{Type: "dummy type"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } if iq.Id == "" { t.Error("NewIQ did not generate an Id") } - otherIq := stanza.NewIQ(stanza.Attrs{}) + otherIq, err := stanza.NewIQ(stanza.Attrs{Type: "dummy type"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } if iq.Id == otherIq.Id { t.Errorf("NewIQ generated two identical ids: %s", iq.Id) } } func TestGenerateIq(t *testing.T) { - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: "admin@localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } payload := stanza.DiscoInfo{ Identity: []stanza.Identity{ {Name: "Test Gateway", @@ -111,7 +123,10 @@ func TestErrorTag(t *testing.T) { } func TestDiscoItems(t *testing.T) { - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeGet, From: "romeo@montague.net/orchard", To: "catalog.shakespeare.lit", Id: "items3"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } payload := stanza.DiscoItems{ Node: "music", } @@ -215,8 +230,9 @@ func TestIsValid(t *testing.T) { t.Errorf("Unmarshal error: %#v (%s)", err, tcase.iq) return } - if !parsedIQ.IsValid() && !tcase.shouldErr { - t.Errorf("failed iq validation for : %s", tcase.iq) + isValid, err := parsedIQ.IsValid() + if !isValid && !tcase.shouldErr { + t.Errorf("failed validation for iq because: %s\nin test case : %s", err, tcase.iq) } }) } diff --git a/stanza/iq_version.go b/stanza/iq_version.go index 4cfbfce..4661dba 100644 --- a/stanza/iq_version.go +++ b/stanza/iq_version.go @@ -11,12 +11,18 @@ type Version struct { Name string `xml:"name,omitempty"` Version string `xml:"version,omitempty"` OS string `xml:"os,omitempty"` + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (v *Version) Namespace() string { return v.XMLName.Space } +func (v *Version) GetSet() *ResultSet { + return v.ResultSet +} + // --------------- // Builder helpers @@ -41,5 +47,5 @@ func (v *Version) SetInfo(name, version, os string) *Version { // Registry init func init() { - TypeRegistry.MapExtension(PKTIQ, xml.Name{"jabber:iq:version", "query"}, Version{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{Space: "jabber:iq:version", Local: "query"}, Version{}) } diff --git a/stanza/iq_version_test.go b/stanza/iq_version_test.go index 45d68f7..bdd621e 100644 --- a/stanza/iq_version_test.go +++ b/stanza/iq_version_test.go @@ -12,8 +12,11 @@ func TestVersion_Builder(t *testing.T) { name := "Exodus" version := "0.7.0.4" os := "Windows-XP 5.01.2600" - iq := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard", + iq, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: "romeo@montague.net/orchard", To: "juliet@capulet.com/balcony", Id: "version_1"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } iq.Version().SetInfo(name, version, os) parsedIQ, err := checkMarshalling(t, iq) diff --git a/stanza/msg_chat_markers.go b/stanza/msg_chat_markers.go index f226379..66276f9 100644 --- a/stanza/msg_chat_markers.go +++ b/stanza/msg_chat_markers.go @@ -35,8 +35,8 @@ type MarkAcknowledged struct { } func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "markable"}, Markable{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "received"}, MarkReceived{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "displayed"}, MarkDisplayed{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatMarkers, "acknowledged"}, MarkAcknowledged{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "markable"}, Markable{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "received"}, MarkReceived{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "displayed"}, MarkDisplayed{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatMarkers, Local: "acknowledged"}, MarkAcknowledged{}) } diff --git a/stanza/msg_chat_state.go b/stanza/msg_chat_state.go index 728a52e..553a314 100644 --- a/stanza/msg_chat_state.go +++ b/stanza/msg_chat_state.go @@ -37,9 +37,9 @@ type StatePaused struct { } func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "active"}, StateActive{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "composing"}, StateComposing{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "gone"}, StateGone{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "inactive"}, StateInactive{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgChatStateNotifications, "paused"}, StatePaused{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "active"}, StateActive{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "composing"}, StateComposing{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "gone"}, StateGone{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "inactive"}, StateInactive{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgChatStateNotifications, Local: "paused"}, StatePaused{}) } diff --git a/stanza/msg_html.go b/stanza/msg_html.go index 1b4016c..f8fbba6 100644 --- a/stanza/msg_html.go +++ b/stanza/msg_html.go @@ -18,5 +18,5 @@ type HTMLBody struct { } func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{"http://jabber.org/protocol/xhtml-im", "html"}, HTML{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "http://jabber.org/protocol/xhtml-im", Local: "html"}, HTML{}) } diff --git a/stanza/msg_oob.go b/stanza/msg_oob.go index 039fac1..92ccadf 100644 --- a/stanza/msg_oob.go +++ b/stanza/msg_oob.go @@ -17,5 +17,5 @@ type OOB struct { } func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{"jabber:x:oob", "x"}, OOB{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "jabber:x:oob", Local: "x"}, OOB{}) } diff --git a/stanza/msg_pubsub_event.go b/stanza/msg_pubsub_event.go index 6ee3dfb..70db228 100644 --- a/stanza/msg_pubsub_event.go +++ b/stanza/msg_pubsub_event.go @@ -210,5 +210,4 @@ func (pse *PubSubEvent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err } } - return nil } diff --git a/stanza/msg_receipts.go b/stanza/msg_receipts.go index 85c2783..71fdd52 100644 --- a/stanza/msg_receipts.go +++ b/stanza/msg_receipts.go @@ -24,6 +24,6 @@ type ReceiptReceived struct { } func init() { - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "request"}, ReceiptRequest{}) - TypeRegistry.MapExtension(PKTMessage, xml.Name{NSMsgReceipts, "received"}, ReceiptReceived{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "request"}, ReceiptRequest{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: NSMsgReceipts, Local: "received"}, ReceiptReceived{}) } diff --git a/stanza/node_test.go b/stanza/node_test.go index aae699d..33649d9 100644 --- a/stanza/node_test.go +++ b/stanza/node_test.go @@ -8,7 +8,10 @@ import ( func TestNode_Marshal(t *testing.T) { jsonData := []byte("{\"key\":\"value\"}") - iqResp := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"}) + iqResp, err := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } iqResp.Any = &Node{ XMLName: xml.Name{Space: "myNS", Local: "space"}, Content: string(jsonData), diff --git a/stanza/pres_muc.go b/stanza/pres_muc.go index bc0e75e..e103a48 100644 --- a/stanza/pres_muc.go +++ b/stanza/pres_muc.go @@ -144,5 +144,5 @@ func (h History) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) } func init() { - TypeRegistry.MapExtension(PKTPresence, xml.Name{"http://jabber.org/protocol/muc", "x"}, MucPresence{}) + TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc", Local: "x"}, MucPresence{}) } diff --git a/stanza/pubsub.go b/stanza/pubsub.go index 1cbdf16..f0aa1ce 100644 --- a/stanza/pubsub.go +++ b/stanza/pubsub.go @@ -29,12 +29,19 @@ type PubSubGeneric struct { // To use in responses to sub/unsub for instance // Subscription options Unsubscribe *SubInfo `xml:"unsubscribe,omitempty"` + + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (p *PubSubGeneric) Namespace() string { return p.XMLName.Space } +func (p *PubSubGeneric) GetSet() *ResultSet { + return p.ResultSet +} + type Affiliations struct { List []Affiliation `xml:"affiliation"` Node string `xml:"node,attr,omitempty"` @@ -156,12 +163,15 @@ type PubSubOption struct { // It's a Set type IQ. // Information about the subscription and the requester are separated. subInfo contains information about the subscription. // 6.1 Subscribe to a Node -func NewSubRq(serviceId string, subInfo SubInfo) (IQ, error) { +func NewSubRq(serviceId string, subInfo SubInfo) (*IQ, error) { if e := subInfo.validate(); e != nil { - return IQ{}, e + return nil, e } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Subscribe: &subInfo, } @@ -172,12 +182,15 @@ func NewSubRq(serviceId string, subInfo SubInfo) (IQ, error) { // It's a Set type IQ // Information about the subscription and the requester are separated. subInfo contains information about the subscription. // 6.2 Unsubscribe from a Node -func NewUnsubRq(serviceId string, subInfo SubInfo) (IQ, error) { +func NewUnsubRq(serviceId string, subInfo SubInfo) (*IQ, error) { if e := subInfo.validate(); e != nil { - return IQ{}, e + return nil, e } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Unsubscribe: &subInfo, } @@ -188,12 +201,15 @@ func NewUnsubRq(serviceId string, subInfo SubInfo) (IQ, error) { // It's a Get type IQ // Information about the subscription and the requester are separated. subInfo contains information about the subscription. // 6.3 Configure Subscription Options -func NewSubOptsRq(serviceId string, subInfo SubInfo) (IQ, error) { +func NewSubOptsRq(serviceId string, subInfo SubInfo) (*IQ, error) { if e := subInfo.validate(); e != nil { - return IQ{}, e + return nil, e } - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ SubOptions: &SubOptions{ SubInfo: subInfo, @@ -205,15 +221,18 @@ func NewSubOptsRq(serviceId string, subInfo SubInfo) (IQ, error) { // NewFormSubmission builds a form submission pubsub IQ // Information about the subscription and the requester are separated. subInfo contains information about the subscription. // 6.3.5 Form Submission -func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (IQ, error) { +func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) { if e := subInfo.validate(); e != nil { - return IQ{}, e + return nil, e } if form.Type != FormTypeSubmit { - return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type) + return nil, errors.New("form type was expected to be submit but was : " + form.Type) } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ SubOptions: &SubOptions{ SubInfo: subInfo, @@ -229,14 +248,17 @@ func NewFormSubmission(serviceId string, subInfo SubInfo, form *Form) (IQ, error // since the value of the element's 'node' attribute specifies the desired NodeID and // the value of the element's 'jid' attribute specifies the subscriber's JID // 6.3.7 Subscribe and Configure -func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error) { +func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (*IQ, error) { if e := subInfo.validate(); e != nil { - return IQ{}, e + return nil, e } if form.Type != FormTypeSubmit { - return IQ{}, errors.New("form type was expected to be submit but was : " + form.Type) + return nil, errors.New("form type was expected to be submit but was : " + form.Type) + } + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) iq.Payload = &PubSubGeneric{ Subscribe: &subInfo, SubOptions: &SubOptions{ @@ -251,8 +273,11 @@ func NewSubAndConfig(serviceId string, subInfo SubInfo, form *Form) (IQ, error) // NewItemsRequest creates a request to query existing items from a node. // Specify a "maxItems" value to request only the last maxItems items. If 0, requests all items. // 6.5.2 Requesting All List AND 6.5.7 Requesting the Most Recent List -func NewItemsRequest(serviceId string, node string, maxItems int) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewItemsRequest(serviceId string, node string, maxItems int) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Items: &Items{Node: node}, } @@ -266,8 +291,11 @@ func NewItemsRequest(serviceId string, node string, maxItems int) (IQ, error) { // NewItemsRequest creates a request to get a specific item from a node. // 6.5.8 Requesting a Particular Item -func NewSpecificItemRequest(serviceId, node, itemId string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewSpecificItemRequest(serviceId, node, itemId string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Items: &Items{Node: node, List: []Item{ @@ -281,13 +309,16 @@ func NewSpecificItemRequest(serviceId, node, itemId string) (IQ, error) { } // NewPublishItemRq creates a request to publish a single item to a node identified by its provided ID -func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (IQ, error) { +func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (*IQ, error) { // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." if strings.TrimSpace(nodeID) == "" { - return IQ{}, errors.New("cannot publish without a target node ID") + return nil, errors.New("cannot publish without a target node ID") } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Publish: &Publish{Node: nodeID, Items: []Item{item}}, } @@ -306,13 +337,16 @@ func NewPublishItemRq(serviceId, nodeID, pubItemID string, item Item) (IQ, error // NewPublishItemOptsRq creates a request to publish items to a node identified by its provided ID, along with configuration options // A pubsub service MAY support the ability to specify options along with a publish request //(if so, it MUST advertise support for the "http://jabber.org/protocol/pubsub#publish-options" feature). -func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (IQ, error) { +func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *PublishOptions) (*IQ, error) { // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." if strings.TrimSpace(nodeID) == "" { - return IQ{}, errors.New("cannot publish without a target node ID") + return nil, errors.New("cannot publish without a target node ID") } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Publish: &Publish{Node: nodeID, Items: items}, PublishOptions: options, @@ -324,13 +358,16 @@ func NewPublishItemOptsRq(serviceId, nodeID string, items []Item, options *Publi // NewDelItemFromNode creates a request to delete and item from a node, given its id. // To delete an item, the publisher sends a retract request. // This helper function follows 7.2 Delete an Item from a Node -func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (IQ, error) { +func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (*IQ, error) { // "The element MUST possess a 'node' attribute, specifying the NodeID of the node." if strings.TrimSpace(nodeID) == "" { - return IQ{}, errors.New("cannot delete item without a target node ID") + return nil, errors.New("cannot delete item without a target node ID") } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Retract: &Retract{Node: nodeID, Items: []Item{{Id: itemId}}, Notify: notify}, } @@ -339,8 +376,11 @@ func NewDelItemFromNode(serviceId, nodeID, itemId string, notify *bool) (IQ, err // NewCreateAndConfigNode makes a request for node creation that has the desired node configuration. // See 8.1.3 Create and Configure a Node -func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) +func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Create: &Create{Node: nodeID}, Configure: &Configure{Form: confForm}, @@ -350,8 +390,11 @@ func NewCreateAndConfigNode(serviceId, nodeID string, confForm *Form) (IQ, error // NewCreateNode builds a request to create a node on the service referenced by "serviceId" // See 8.1 Create a Node -func NewCreateNode(serviceId, nodeName string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) +func NewCreateNode(serviceId, nodeName string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Create: &Create{Node: nodeName}, } @@ -361,8 +404,11 @@ func NewCreateNode(serviceId, nodeName string) (IQ, error) { // NewRetrieveAllSubsRequest builds a request to retrieve all subscriptions from all nodes // In order to make the request, the requesting entity MUST send an IQ-get whose // child contains an empty element with no attributes. -func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewRetrieveAllSubsRequest(serviceId string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Subscriptions: &Subscriptions{}, } @@ -371,8 +417,11 @@ func NewRetrieveAllSubsRequest(serviceId string) (IQ, error) { // NewRetrieveAllAffilsRequest builds a request to retrieve all affiliations from all nodes // In order to make the request of the service, the requesting entity includes an empty element with no attributes. -func NewRetrieveAllAffilsRequest(serviceId string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewRetrieveAllAffilsRequest(serviceId string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubGeneric{ Affiliations: &Affiliations{}, } diff --git a/stanza/pubsub_owner.go b/stanza/pubsub_owner.go index 9adfbef..2f63bbb 100644 --- a/stanza/pubsub_owner.go +++ b/stanza/pubsub_owner.go @@ -9,12 +9,18 @@ import ( type PubSubOwner struct { XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub#owner pubsub"` OwnerUseCase OwnerUseCase + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (pso *PubSubOwner) Namespace() string { return pso.XMLName.Space } +func (pso *PubSubOwner) GetSet() *ResultSet { + return pso.ResultSet +} + type OwnerUseCase interface { UseCase() string } @@ -112,8 +118,11 @@ const ( // NewConfigureNode creates a request to configure a node on the given service. // A form will be returned by the service, to which the user must respond using for instance the NewFormSubmission function. // See 8.2 Configure a Node -func NewConfigureNode(serviceId, nodeName string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewConfigureNode(serviceId, nodeName string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &ConfigureOwner{Node: nodeName}, } @@ -122,11 +131,14 @@ func NewConfigureNode(serviceId, nodeName string) (IQ, error) { // NewDelNode creates a request to delete node "nodeID" from the "serviceId" service // See 8.4 Delete a Node -func NewDelNode(serviceId, nodeID string) (IQ, error) { +func NewDelNode(serviceId, nodeID string) (*IQ, error) { if strings.TrimSpace(nodeID) == "" { - return IQ{}, errors.New("cannot delete a node without a target node ID") + return nil, errors.New("cannot delete a node without a target node ID") + } + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err } - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) iq.Payload = &PubSubOwner{ OwnerUseCase: &DeleteOwner{Node: nodeID}, } @@ -135,8 +147,11 @@ func NewDelNode(serviceId, nodeID string) (IQ, error) { // NewPurgeAllItems creates a new purge request for the "nodeId" node, at "serviceId" service // See 8.5 Purge All Node Items -func NewPurgeAllItems(serviceId, nodeId string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) +func NewPurgeAllItems(serviceId, nodeId string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &PurgeOwner{Node: nodeId}, } @@ -145,8 +160,11 @@ func NewPurgeAllItems(serviceId, nodeId string) (IQ, error) { // NewRequestDefaultConfig build a request to ask the service for the default config of its nodes // See 8.3 Request Default Node Configuration Options -func NewRequestDefaultConfig(serviceId string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewRequestDefaultConfig(serviceId string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &DefaultOwner{}, } @@ -177,8 +195,11 @@ func NewApproveSubRequest(serviceId, reqID string, apprForm *Form) (Message, err // NewGetPendingSubRequests creates a new request for all pending subscriptions to all their nodes at a service // This feature MUST be implemented using the Ad-Hoc Commands (XEP-0050) protocol // 8.7 Process Pending Subscription Requests -func NewGetPendingSubRequests(serviceId string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) +func NewGetPendingSubRequests(serviceId string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &Command{ // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" Node: "http://jabber.org/protocol/pubsub#get-pending", @@ -191,9 +212,9 @@ func NewGetPendingSubRequests(serviceId string) (IQ, error) { // Upon receiving the data form for managing subscription requests, the owner then MAY request pending subscription // approval requests for a given node. // See 8.7.4 Per-Node Request -func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error) { +func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (*IQ, error) { if sessionId == "" { - return IQ{}, errors.New("the sessionId must be maintained for the command") + return nil, errors.New("the sessionId must be maintained for the command") } form := &Form{ @@ -202,12 +223,15 @@ func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error } data, err := xml.Marshal(form) if err != nil { - return IQ{}, err + return nil, err } var n Node xml.Unmarshal(data, &n) - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &Command{ // the command name ('node' attribute of the command element) MUST have a value of "http://jabber.org/protocol/pubsub#get-pending" Node: "http://jabber.org/protocol/pubsub#get-pending", @@ -221,16 +245,22 @@ func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (IQ, error // NewSubListRequest creates a request to list subscriptions of the client, for all nodes at the service. // It's a Get type IQ // 8.8.1 Retrieve Subscriptions -func NewSubListRqPl(serviceId, nodeID string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewSubListRqPl(serviceId, nodeID string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &SubscriptionsOwner{Node: nodeID}, } return iq, nil } -func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) +func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwner) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &SubscriptionsOwner{Node: nodeID, Subscriptions: subs}, } @@ -239,8 +269,11 @@ func NewSubsForEntitiesRequest(serviceId, nodeID string, subs []SubscriptionOwne // NewModifAffiliationRequest creates a request to either modify one or more affiliations, or delete one or more affiliations // 8.9.2 Modify Affiliation & 8.9.2.4 Multiple Simultaneous Modifications & 8.9.3 Delete an Entity (just set the status to "none") -func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) +func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []AffiliationOwner) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &AffiliationsOwner{ Node: nodeID, @@ -252,8 +285,11 @@ func NewModifAffiliationRequest(serviceId, nodeID string, newAffils []Affiliatio // NewAffiliationListRequest creates a request to list all affiliated entities // See 8.9.1 Retrieve List List -func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) { - iq := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) +func NewAffiliationListRequest(serviceId, nodeID string) (*IQ, error) { + iq, err := NewIQ(Attrs{Type: IQTypeGet, To: serviceId}) + if err != nil { + return nil, err + } iq.Payload = &PubSubOwner{ OwnerUseCase: &AffiliationsOwner{ Node: nodeID, @@ -265,12 +301,15 @@ func NewAffiliationListRequest(serviceId, nodeID string) (IQ, error) { // NewFormSubmission builds a form submission pubsub IQ, in the Owner namespace // This is typically used to respond to a form issued by the server when configuring a node. // See 8.2.4 Form Submission -func NewFormSubmissionOwner(serviceId, nodeName string, fields []*Field) (IQ, error) { +func NewFormSubmissionOwner(serviceId, nodeName string, fields []*Field) (*IQ, error) { if serviceId == "" || nodeName == "" { - return IQ{}, errors.New("serviceId and nodeName must be filled for this request to be valid") + return nil, errors.New("serviceId and nodeName must be filled for this request to be valid") } - submitConf := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + submitConf, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) + if err != nil { + return nil, err + } submitConf.Payload = &PubSubOwner{ OwnerUseCase: &ConfigureOwner{ Node: nodeName, @@ -308,6 +347,17 @@ func (iq *IQ) GetFormFields() (map[string]*Field, error) { fieldMap[elt.Var] = elt } return fieldMap, nil + + case *Command: + fieldMap := make(map[string]*Field) + co, ok := payload.CommandElement.(*Form) + if !ok { + return nil, errors.New("this IQ does not contain a command payload with a form") + } + for _, elt := range co.Fields { + fieldMap[elt.Var] = elt + } + return fieldMap, nil default: if iq.Any != nil { fieldMap := make(map[string]*Field) diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go index b7cf6db..6ff1c1b 100644 --- a/stanza/pubsub_owner_test.go +++ b/stanza/pubsub_owner_test.go @@ -16,6 +16,9 @@ func TestNewConfigureNode(t *testing.T) { " " subR, err := stanza.NewConfigureNode("pubsub.shakespeare.lit", "princely_musings") + if err != nil { + t.Fatalf("failed to create a configure node request: %v", err) + } subR.Id = "config1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -129,6 +132,9 @@ func TestNewRequestDefaultConfig(t *testing.T) { " " subR, err := stanza.NewRequestDefaultConfig("pubsub.shakespeare.lit") + if err != nil { + t.Fatalf("failed to create a default config request: %v", err) + } subR.Id = "def1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -239,6 +245,9 @@ func TestNewDelNode(t *testing.T) { " " subR, err := stanza.NewDelNode("pubsub.shakespeare.lit", "princely_musings") + if err != nil { + t.Fatalf("failed to create a node delete request: %v", err) + } subR.Id = "delete1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -311,6 +320,9 @@ func TestNewPurgeAllItems(t *testing.T) { " " subR, err := stanza.NewPurgeAllItems("pubsub.shakespeare.lit", "princely_musings") + if err != nil { + t.Fatalf("failed to create a purge all items request: %v", err) + } subR.Id = "purge1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -367,6 +379,9 @@ func TestNewApproveSubRequest(t *testing.T) { } subR, err := stanza.NewApproveSubRequest("pubsub.shakespeare.lit", "approve1", apprForm) + if err != nil { + t.Fatalf("failed to create a sub approval request: %v", err) + } subR.Id = "approve1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -404,6 +419,9 @@ func TestNewGetPendingSubRequests(t *testing.T) { " " subR, err := stanza.NewGetPendingSubRequests("pubsub.shakespeare.lit") + if err != nil { + t.Fatalf("failed to create a get pending subs request: %v", err) + } subR.Id = "pending1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -461,12 +479,12 @@ func TestNewGetPendingSubRequestsResp(t *testing.T) { _, ok := respIQ.Payload.(*stanza.Command) if !ok { - errors.New("this iq payload is not a command") + t.Fatal("this iq payload is not a command") } fMap, err := respIQ.GetFormFields() if err != nil || len(fMap) != 2 { - errors.New("could not parse command form fields") + t.Fatal("could not parse command form fields") } } @@ -485,6 +503,9 @@ func TestNewApprovePendingSubRequest(t *testing.T) { subR, err := stanza.NewApprovePendingSubRequest("pubsub.shakespeare.lit", "pubsub-get-pending:20031021T150901Z-600", "princely_musings") + if err != nil { + t.Fatalf("failed to create a approve pending sub request: %v", err) + } subR.Id = "pending2" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -524,6 +545,9 @@ func TestNewSubListRqPl(t *testing.T) { " " subR, err := stanza.NewSubListRqPl("pubsub.shakespeare.lit", "princely_musings") + if err != nil { + t.Fatalf("failed to create a sub list request: %v", err) + } subR.Id = "subman1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -575,7 +599,7 @@ func TestNewSubListRqPlResp(t *testing.T) { pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) if !ok { - errors.New("this iq payload is not a command") + t.Fatal("this iq payload is not a command") } subs, ok := pubsub.OwnerUseCase.(*stanza.SubscriptionsOwner) @@ -599,6 +623,9 @@ func TestNewAffiliationListRequest(t *testing.T) { " " subR, err := stanza.NewAffiliationListRequest("pubsub.shakespeare.lit", "princely_musings") + if err != nil { + t.Fatalf("failed to create an affiliations list request: %v", err) + } subR.Id = "ent1" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -648,7 +675,7 @@ func TestNewAffiliationListRequestResp(t *testing.T) { pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) if !ok { - errors.New("this iq payload is not a command") + t.Fatal("this iq payload is not a command") } affils, ok := pubsub.OwnerUseCase.(*stanza.AffiliationsOwner) @@ -690,6 +717,9 @@ func TestNewModifAffiliationRequest(t *testing.T) { } subR, err := stanza.NewModifAffiliationRequest("pubsub.shakespeare.lit", "princely_musings", affils) + if err != nil { + t.Fatalf("failed to create a modif affiliation request: %v", err) + } subR.Id = "ent3" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -833,6 +863,10 @@ func TestNewFormSubmissionOwner(t *testing.T) { {Var: "pubsub#access_model", ValuesList: []string{"roster"}}, {Var: "pubsub#roster_groups_allowed", ValuesList: []string{"friends", "servants", "courtiers"}}, }) + if err != nil { + t.Fatalf("failed to create a form submission request: %v", err) + } + subR.Id = "config2" if err != nil { t.Fatalf("Could not create request : %s", err) @@ -878,7 +912,7 @@ func getPubSubOwnerPayload(response string) (*stanza.PubSubOwner, error) { pubsub, ok := respIQ.Payload.(*stanza.PubSubOwner) if !ok { - errors.New("this iq payload is not a pubsub of the owner namespace") + return nil, errors.New("this iq payload is not a pubsub of the owner namespace") } return pubsub, nil diff --git a/stanza/pubsub_test.go b/stanza/pubsub_test.go index 6b413c4..ac227d2 100644 --- a/stanza/pubsub_test.go +++ b/stanza/pubsub_test.go @@ -40,6 +40,9 @@ func TestNewSubRequest(t *testing.T) { Node: "princely_musings", Jid: "francisco@denmark.lit", } subR, err := stanza.NewSubRq("pubsub.shakespeare.lit", subInfo) + if err != nil { + t.Fatalf("failed to create a sub request: %v", err) + } subR.Id = "sub1" if err != nil { t.Fatalf("Could not create a sub request : %s", err) @@ -96,6 +99,9 @@ func TestNewUnSubRequest(t *testing.T) { Node: "princely_musings", Jid: "francisco@denmark.lit", } subR, err := stanza.NewUnsubRq("pubsub.shakespeare.lit", subInfo) + if err != nil { + t.Fatalf("failed to create an unsub request: %v", err) + } subR.Id = "unsub1" if err != nil { t.Fatalf("Could not create a sub request : %s", err) @@ -157,6 +163,9 @@ func TestNewSubOptsRq(t *testing.T) { Node: "princely_musings", Jid: "francisco@denmark.lit", } subR, err := stanza.NewSubOptsRq("pubsub.shakespeare.lit", subInfo) + if err != nil { + t.Fatalf("failed to create a sub options request: %v", err) + } subR.Id = "options1" if err != nil { t.Fatalf("Could not create a sub request : %s", err) @@ -264,6 +273,9 @@ func TestNewFormSubmission(t *testing.T) { } subR, err := stanza.NewFormSubmission("pubsub.shakespeare.lit", subInfo, submitFormExample) + if err != nil { + t.Fatalf("failed to create a form submission request: %v", err) + } subR.Id = "options2" if err != nil { t.Fatalf("Could not create a sub request : %s", err) @@ -313,6 +325,9 @@ func TestNewSubAndConfig(t *testing.T) { } subR, err := stanza.NewSubAndConfig("pubsub.shakespeare.lit", subInfo, submitFormExample) + if err != nil { + t.Fatalf("failed to create a sub and config request: %v", err) + } subR.Id = "sub1" if err != nil { t.Fatalf("Could not create a sub request : %s", err) @@ -482,6 +497,9 @@ func TestNewSpecificItemRequest(t *testing.T) { " " subR, err := stanza.NewSpecificItemRequest("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901") + if err != nil { + t.Fatalf("failed to create a specific item request: %v", err) + } subR.Id = "items3" if err != nil { t.Fatalf("Could not create an items request : %s", err) @@ -638,6 +656,9 @@ func TestNewDelItemFromNode(t *testing.T) { " " subR, err := stanza.NewDelItemFromNode("pubsub.shakespeare.lit", "princely_musings", "ae890ac52d0df67ed7cfdf51b644e901", nil) + if err != nil { + t.Fatalf("failed to create a delete item from node request: %v", err) + } subR.Id = "retract1" if err != nil { t.Fatalf("Could not create a del item request : %s", err) @@ -677,6 +698,9 @@ func TestNewCreateNode(t *testing.T) { " " subR, err := stanza.NewCreateNode("pubsub.shakespeare.lit", "princely_musings") + if err != nil { + t.Fatalf("failed to create a create node request: %v", err) + } subR.Id = "create1" if err != nil { t.Fatalf("Could not create a create node request : %s", err) @@ -748,6 +772,10 @@ func TestNewCreateAndConfigNode(t *testing.T) { {Var: "pubsub#max_payload_size", ValuesList: []string{"1028"}}, }, }) + + if err != nil { + t.Fatalf("failed to create a create and config node request: %v", err) + } subR.Id = "create1" if err != nil { t.Fatalf("Could not create a create node request : %s", err) @@ -796,6 +824,9 @@ func TestNewRetrieveAllSubsRequest(t *testing.T) { " " subR, err := stanza.NewRetrieveAllSubsRequest("pubsub.shakespeare.lit") + if err != nil { + t.Fatalf("failed to create a get all subs request: %v", err) + } subR.Id = "subscriptions1" if err != nil { t.Fatalf("Could not create a create node request : %s", err) @@ -856,6 +887,9 @@ func TestNewRetrieveAllAffilsRequest(t *testing.T) { " " subR, err := stanza.NewRetrieveAllAffilsRequest("pubsub.shakespeare.lit") + if err != nil { + t.Fatalf("failed to create a get all affiliations request: %v", err) + } subR.Id = "affil1" if err != nil { t.Fatalf("Could not create retreive all affiliations request : %s", err) @@ -914,7 +948,7 @@ func getPubSubGenericPayload(response string) (*stanza.PubSubGeneric, error) { pubsub, ok := respIQ.Payload.(*stanza.PubSubGeneric) if !ok { - errors.New("this iq payload is not a pubsub") + return nil, errors.New("this iq payload is not a pubsub") } return pubsub, nil diff --git a/stanza/results_sets.go b/stanza/results_sets.go new file mode 100644 index 0000000..6cbab0f --- /dev/null +++ b/stanza/results_sets.go @@ -0,0 +1,29 @@ +package stanza + +import ( + "encoding/xml" +) + +// Support for XEP-0059 +// See https://xmpp.org/extensions/xep-0059 +const ( + // Common but not only possible namespace for query blocks in a result set context + NSQuerySet = "jabber:iq:search" +) + +type ResultSet struct { + XMLName xml.Name `xml:"http://jabber.org/protocol/rsm set"` + After *string `xml:"after,omitempty"` + Before *string `xml:"before,omitempty"` + Count *int `xml:"count,omitempty"` + First *First `xml:"first,omitempty"` + Index *int `xml:"index,omitempty"` + Last *string `xml:"last,omitempty"` + Max *int `xml:"max,omitempty"` +} + +type First struct { + XMLName xml.Name `xml:"first"` + Content string + Index *int `xml:"index,attr,omitempty"` +} diff --git a/stanza/results_sets_test.go b/stanza/results_sets_test.go new file mode 100644 index 0000000..25d085e --- /dev/null +++ b/stanza/results_sets_test.go @@ -0,0 +1,79 @@ +package stanza_test + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "testing" +) + +// Limiting the number of items +func TestNewResultSetReq(t *testing.T) { + expectedRq := " " + + "Pete 10 " + + items := []stanza.Node{ + { + XMLName: xml.Name{Local: "nick"}, + Content: "Pete", + }, + } + + maxVal := 10 + rs := &stanza.ResultSet{ + Max: &maxVal, + } + + rq, err := stanza.NewResultSetReq("users.jabber.org", "jabber:iq:search", items, rs) + if err != nil { + t.Fatalf("failed to build the result set request : %v", err) + } + rq.Id = "limit1" + + data, err := xml.Marshal(rq) + if err := compareMarshal(expectedRq, string(data)); err != nil { + t.Fatalf(err.Error()) + } +} + +func TestUnmarshalResultSeqReq(t *testing.T) { + //expectedRq := " " + + // "Pete 10 " + //var uReq stanza.IQ + //err := xml.Unmarshal([]byte(expectedRq), &uReq) + //if err != nil { + // t.Fatalf(err.Error()) + //} + //items := []stanza.Node{ + // { + // XMLName: xml.Name{Local: "nick"}, + // Content: "Pete", + // }, + //} + // + //maxVal := 10 + //rs := &stanza.ResultSet{ + // XMLName: xml.Name{Local: "set", Space: "http://jabber.org/protocol/rsm"}, + // Max: &maxVal, + //} + // + //rq, err := stanza.NewResultSetReq("users.jabber.org", "jabber:iq:search", items, rs) + //if err != nil { + // t.Fatalf("failed to build the result set request : %v", err) + //} + //rq.Id = "limit1" + // + //// Namespace is unmarshalled as of parent for nodes in the payload. To DeepEqual, we need to set the namespace in + //// the "expectedRq" + //n, ok := rq.Payload.(*stanza.QuerySet) + //if !ok { + // t.Fatalf("payload is not a query set: %v", ok) + //} + //n.Nodes[0].XMLName.Space = stanza.NSQuerySet + // + //data, err := xml.Marshal(rq) + //fmt.Println(string(data)) + //if !reflect.DeepEqual(rq, uReq) { + // t.Fatalf("nope") + //} + +} diff --git a/stanza/sasl_auth.go b/stanza/sasl_auth.go index 29648ee..9dfe557 100644 --- a/stanza/sasl_auth.go +++ b/stanza/sasl_auth.go @@ -69,12 +69,18 @@ type Bind struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"` Resource string `xml:"resource,omitempty"` Jid string `xml:"jid,omitempty"` + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (b *Bind) Namespace() string { return b.XMLName.Space } +func (b *Bind) GetSet() *ResultSet { + return b.ResultSet +} + // ============================================================================ // Session (Obsolete) @@ -89,12 +95,18 @@ func (b *Bind) Namespace() string { type StreamSession struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` Optional bool // If element does exist, it mean we are not required to open session + // Result sets + ResultSet *ResultSet `xml:"set,omitempty"` } func (s *StreamSession) Namespace() string { return s.XMLName.Space } +func (s *StreamSession) GetSet() *ResultSet { + return s.ResultSet +} + func (s *StreamSession) IsOptional() bool { if s.XMLName.Local == "session" { return s.Optional diff --git a/stanza/sasl_auth_test.go b/stanza/sasl_auth_test.go index d9ba1dc..3e37453 100644 --- a/stanza/sasl_auth_test.go +++ b/stanza/sasl_auth_test.go @@ -28,7 +28,10 @@ func TestSessionFeatures(t *testing.T) { // Check that the Session tag can be used in IQ decoding func TestSessionIQ(t *testing.T) { - iq := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"}) + iq, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeSet, Id: "session"}) + if err != nil { + t.Fatalf("failed to create IQ: %v", err) + } iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true} data, err := xml.Marshal(iq) diff --git a/stanza/xmpp_test.go b/stanza/xmpp_test.go index 473616a..b39613b 100644 --- a/stanza/xmpp_test.go +++ b/stanza/xmpp_test.go @@ -16,7 +16,7 @@ var reInsideWhtsp = regexp.MustCompile(`[\s\p{Zs}]`) // ============================================================================ // Marshaller / unmarshaller test -func checkMarshalling(t *testing.T, iq stanza.IQ) (*stanza.IQ, error) { +func checkMarshalling(t *testing.T, iq *stanza.IQ) (*stanza.IQ, error) { // Marshall data, err := xml.Marshal(iq) if err != nil { diff --git a/stream_manager.go b/stream_manager.go index 18e1434..ebef1fa 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -27,7 +27,7 @@ type StreamClient interface { Connect() error Resume(state SMState) error Send(packet stanza.Packet) error - SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) + SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) SendRaw(packet string) error Disconnect() error SetHandler(handler EventHandler) @@ -37,7 +37,7 @@ type StreamClient interface { // It is mostly use in callback to pass a limited subset of the stream client interface type Sender interface { Send(packet stanza.Packet) error - SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) + SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) SendRaw(packet string) error } diff --git a/tcp_server_mock.go b/tcp_server_mock.go index 1a4f92e..117513a 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -130,13 +130,16 @@ func respondToIQ(t *testing.T, sc *ServerConn) { t.Fatalf("failed to receive IQ : %s", err.Error()) } - if !iqReq.IsValid() { + if vld, _ := iqReq.IsValid(); !vld { mockIQError(sc.connection) return } // Crafting response - iqResp := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) + iqResp, err := stanza.NewIQ(stanza.Attrs{Type: stanza.IQTypeResult, From: iqReq.To, To: iqReq.From, Id: iqReq.Id, Lang: "en"}) + if err != nil { + t.Fatalf("failed to create iqResp: %v", err) + } disco := iqResp.DiscoInfo() disco.AddFeatures("vcard-temp", `http://jabber.org/protocol/address`) diff --git a/transport.go b/transport.go index abf7f4a..fa124ea 100644 --- a/transport.go +++ b/transport.go @@ -9,8 +9,8 @@ import ( "strings" ) -var ErrTransportProtocolNotSupported = errors.New("Transport protocol not supported") -var ErrTLSNotSupported = errors.New("Transport does not support StartTLS") +var ErrTransportProtocolNotSupported = errors.New("transport protocol not supported") +var ErrTLSNotSupported = errors.New("transport does not support StartTLS") // TODO: rename to transport config? type TransportConfiguration struct { @@ -67,7 +67,7 @@ func NewClientTransport(config TransportConfiguration) Transport { // will be returned. func NewComponentTransport(config TransportConfiguration) (Transport, error) { if strings.HasPrefix(config.Address, "ws:") || strings.HasPrefix(config.Address, "wss:") { - return nil, fmt.Errorf("Components only support XMPP transport: %w", ErrTransportProtocolNotSupported) + return nil, fmt.Errorf("components only support XMPP transport: %w", ErrTransportProtocolNotSupported) } config.Address = ensurePort(config.Address, 5222) diff --git a/xmpp_transport.go b/xmpp_transport.go index 6e1209f..092b95d 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -113,7 +113,7 @@ func (t *XMPPTransport) Ping() error { return err } if n != 1 { - return errors.New("Could not write ping") + return errors.New("could not write ping") } return nil } From 70ef1d575f020e7ba247af484df78dac5870ee83 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Fri, 31 Jan 2020 12:06:53 +0100 Subject: [PATCH 39/54] Reset Tests Will come with MaM (XEP-313) implementation --- stanza/results_sets_test.go | 67 +++++-------------------------------- 1 file changed, 8 insertions(+), 59 deletions(-) diff --git a/stanza/results_sets_test.go b/stanza/results_sets_test.go index 25d085e..2ddd123 100644 --- a/stanza/results_sets_test.go +++ b/stanza/results_sets_test.go @@ -1,79 +1,28 @@ package stanza_test import ( - "encoding/xml" "gosrc.io/xmpp/stanza" "testing" ) // Limiting the number of items func TestNewResultSetReq(t *testing.T) { - expectedRq := " " + - "Pete 10 " - - items := []stanza.Node{ - { - XMLName: xml.Name{Local: "nick"}, - Content: "Pete", - }, - } + expectedRq := " " + + " " + + "urn:xmpp:mam:2 2010-08-07T00:00:00Z " + + " 10 " maxVal := 10 rs := &stanza.ResultSet{ Max: &maxVal, } - rq, err := stanza.NewResultSetReq("users.jabber.org", "jabber:iq:search", items, rs) - if err != nil { - t.Fatalf("failed to build the result set request : %v", err) - } - rq.Id = "limit1" - - data, err := xml.Marshal(rq) - if err := compareMarshal(expectedRq, string(data)); err != nil { - t.Fatalf(err.Error()) - } + // TODO when Mam is implemented + _ = expectedRq + _ = rs } func TestUnmarshalResultSeqReq(t *testing.T) { - //expectedRq := " " + - // "Pete 10 " - //var uReq stanza.IQ - //err := xml.Unmarshal([]byte(expectedRq), &uReq) - //if err != nil { - // t.Fatalf(err.Error()) - //} - //items := []stanza.Node{ - // { - // XMLName: xml.Name{Local: "nick"}, - // Content: "Pete", - // }, - //} - // - //maxVal := 10 - //rs := &stanza.ResultSet{ - // XMLName: xml.Name{Local: "set", Space: "http://jabber.org/protocol/rsm"}, - // Max: &maxVal, - //} - // - //rq, err := stanza.NewResultSetReq("users.jabber.org", "jabber:iq:search", items, rs) - //if err != nil { - // t.Fatalf("failed to build the result set request : %v", err) - //} - //rq.Id = "limit1" - // - //// Namespace is unmarshalled as of parent for nodes in the payload. To DeepEqual, we need to set the namespace in - //// the "expectedRq" - //n, ok := rq.Payload.(*stanza.QuerySet) - //if !ok { - // t.Fatalf("payload is not a query set: %v", ok) - //} - //n.Nodes[0].XMLName.Space = stanza.NSQuerySet - // - //data, err := xml.Marshal(rq) - //fmt.Println(string(data)) - //if !reflect.DeepEqual(rq, uReq) { - // t.Fatalf("nope") - //} + // TODO when Mam is implemented } From 2083cbf29caeabac7671e6403bfd3e56466cac9a Mon Sep 17 00:00:00 2001 From: rcorniere Date: Fri, 31 Jan 2020 15:17:59 +0100 Subject: [PATCH 40/54] Various fixes --- _examples/custom_stanza/custom_stanza.go | 3 +++ client_test.go | 9 ++++--- component_test.go | 14 +++++++--- stanza/commands.go | 13 ++++----- stanza/pubsub_owner.go | 21 +++++++++------ stanza/pubsub_owner_test.go | 34 ------------------------ stanza/pubsub_test.go | 33 ----------------------- tcp_server_mock.go | 16 +++++++---- 8 files changed, 50 insertions(+), 93 deletions(-) diff --git a/_examples/custom_stanza/custom_stanza.go b/_examples/custom_stanza/custom_stanza.go index 5320565..b4bfb52 100644 --- a/_examples/custom_stanza/custom_stanza.go +++ b/_examples/custom_stanza/custom_stanza.go @@ -47,6 +47,9 @@ func (c CustomPayload) Namespace() string { return c.XMLName.Space } +func (c CustomPayload) GetSet() *stanza.ResultSet { + return nil +} func init() { stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "my:custom:payload", Local: "query"}, CustomPayload{}) } diff --git a/client_test.go b/client_test.go index d6ebf99..00c956a 100644 --- a/client_test.go +++ b/client_test.go @@ -411,7 +411,7 @@ func closeConn(t *testing.T, sc *ServerConn) { } switch cls.(type) { case stanza.StreamClosePacket: - fmt.Fprintf(sc.connection, stanza.StreamClose) + sc.connection.Write([]byte(stanza.StreamClose)) return } } @@ -430,7 +430,7 @@ func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) { sendStreamFeatures(t, sc) // Send initial features readAuth(t, sc.decoder) - fmt.Fprintln(sc.connection, "") + sc.connection.Write([]byte("")) checkClientOpenStream(t, sc) // Reset stream sendRFC3921Feature(t, sc) // Send post auth features @@ -439,7 +439,10 @@ func handlerClientConnectWithSession(t *testing.T, sc *ServerConn) { } func checkClientOpenStream(t *testing.T, sc *ServerConn) { - sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + if err != nil { + t.Fatalf("failed to set deadline: %v", err) + } defer sc.connection.SetDeadline(time.Time{}) for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. diff --git a/component_test.go b/component_test.go index 4af4b2c..f2b5a2f 100644 --- a/component_test.go +++ b/component_test.go @@ -60,7 +60,7 @@ func TestGenerateHandshakeId(t *testing.T) { checkOpenStreamHandshakeID(t, sc, <-uchan) readHandshakeComponent(t, sc.decoder) - fmt.Fprintln(sc.connection, "") // That's all the server needs to return (see xep-0114) + sc.connection.Write([]byte("")) // That's all the server needs to return (see xep-0114) return } @@ -401,7 +401,10 @@ func handlerForComponentIQSend(t *testing.T, sc *ServerConn) { // Used for ID and handshake related tests func checkOpenStreamHandshakeID(t *testing.T, sc *ServerConn, streamID string) { - sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + if err != nil { + t.Fatalf("failed to set deadline: %v", err) + } defer sc.connection.SetDeadline(time.Time{}) for { // TODO clean up. That for loop is not elegant and I prefer bounded recursion. @@ -441,7 +444,10 @@ func handlerComponentFailedHandshakeDefaultID(t *testing.T, sc *ServerConn) { Body: "Fail my handshake.", } s, _ := xml.Marshal(me) - fmt.Fprintln(sc.connection, string(s)) + _, err := sc.connection.Write(s) + if err != nil { + t.Fatalf("could not write message: %v", err) + } return } @@ -469,6 +475,6 @@ func readHandshakeComponent(t *testing.T, decoder *xml.Decoder) { func handlerForComponentHandshakeDefaultID(t *testing.T, sc *ServerConn) { checkOpenStreamHandshakeDefaultID(t, sc) readHandshakeComponent(t, sc.decoder) - fmt.Fprintln(sc.connection, "") // That's all the server needs to return (see xep-0114) + sc.connection.Write([]byte("")) // That's all the server needs to return (see xep-0114) return } diff --git a/stanza/commands.go b/stanza/commands.go index 6f9d3bc..3d9d4ea 100644 --- a/stanza/commands.go +++ b/stanza/commands.go @@ -119,26 +119,27 @@ func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { case "affiliations": a := Actions{} - d.DecodeElement(&a, &tt) + err = d.DecodeElement(&a, &tt) c.CommandElement = &a case "configure": nt := Note{} - d.DecodeElement(&nt, &tt) + err = d.DecodeElement(&nt, &tt) c.CommandElement = &nt case "x": f := Form{} - d.DecodeElement(&f, &tt) + err = d.DecodeElement(&f, &tt) c.CommandElement = &f default: n := Node{} - e := d.DecodeElement(&n, &tt) - _ = e + err = d.DecodeElement(&n, &tt) c.CommandElement = &n if err != nil { return err } } - + if err != nil { + return err + } case xml.EndElement: if tt == start.End() { return nil diff --git a/stanza/pubsub_owner.go b/stanza/pubsub_owner.go index 2f63bbb..32d2773 100644 --- a/stanza/pubsub_owner.go +++ b/stanza/pubsub_owner.go @@ -226,7 +226,10 @@ func NewApprovePendingSubRequest(serviceId, sessionId, nodeId string) (*IQ, erro return nil, err } var n Node - xml.Unmarshal(data, &n) + err = xml.Unmarshal(data, &n) + if err != nil { + return nil, err + } iq, err := NewIQ(Attrs{Type: IQTypeSet, To: serviceId}) if err != nil { @@ -406,33 +409,35 @@ func (pso *PubSubOwner) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err case "affiliations": aff := AffiliationsOwner{} - d.DecodeElement(&aff, &tt) + err = d.DecodeElement(&aff, &tt) pso.OwnerUseCase = &aff case "configure": co := ConfigureOwner{} - d.DecodeElement(&co, &tt) + err = d.DecodeElement(&co, &tt) pso.OwnerUseCase = &co case "default": def := DefaultOwner{} - d.DecodeElement(&def, &tt) + err = d.DecodeElement(&def, &tt) pso.OwnerUseCase = &def case "delete": del := DeleteOwner{} - d.DecodeElement(&del, &tt) + err = d.DecodeElement(&del, &tt) pso.OwnerUseCase = &del case "purge": pu := PurgeOwner{} - d.DecodeElement(&pu, &tt) + err = d.DecodeElement(&pu, &tt) pso.OwnerUseCase = &pu case "subscriptions": subs := SubscriptionsOwner{} - d.DecodeElement(&subs, &tt) + err = d.DecodeElement(&subs, &tt) pso.OwnerUseCase = &subs if err != nil { return err } } - + if err != nil { + return err + } case xml.EndElement: if tt == start.End() { return nil diff --git a/stanza/pubsub_owner_test.go b/stanza/pubsub_owner_test.go index 6ff1c1b..151688b 100644 --- a/stanza/pubsub_owner_test.go +++ b/stanza/pubsub_owner_test.go @@ -20,9 +20,6 @@ func TestNewConfigureNode(t *testing.T) { t.Fatalf("failed to create a configure node request: %v", err) } subR.Id = "config1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -136,9 +133,6 @@ func TestNewRequestDefaultConfig(t *testing.T) { t.Fatalf("failed to create a default config request: %v", err) } subR.Id = "def1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -249,9 +243,6 @@ func TestNewDelNode(t *testing.T) { t.Fatalf("failed to create a node delete request: %v", err) } subR.Id = "delete1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -324,9 +315,6 @@ func TestNewPurgeAllItems(t *testing.T) { t.Fatalf("failed to create a purge all items request: %v", err) } subR.Id = "purge1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -383,9 +371,6 @@ func TestNewApproveSubRequest(t *testing.T) { t.Fatalf("failed to create a sub approval request: %v", err) } subR.Id = "approve1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } frm, ok := subR.Extensions[0].(*stanza.Form) if !ok { @@ -423,9 +408,6 @@ func TestNewGetPendingSubRequests(t *testing.T) { t.Fatalf("failed to create a get pending subs request: %v", err) } subR.Id = "pending1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -507,9 +489,6 @@ func TestNewApprovePendingSubRequest(t *testing.T) { t.Fatalf("failed to create a approve pending sub request: %v", err) } subR.Id = "pending2" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -549,9 +528,6 @@ func TestNewSubListRqPl(t *testing.T) { t.Fatalf("failed to create a sub list request: %v", err) } subR.Id = "subman1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -627,9 +603,6 @@ func TestNewAffiliationListRequest(t *testing.T) { t.Fatalf("failed to create an affiliations list request: %v", err) } subR.Id = "ent1" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -721,9 +694,6 @@ func TestNewModifAffiliationRequest(t *testing.T) { t.Fatalf("failed to create a modif affiliation request: %v", err) } subR.Id = "ent3" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -866,11 +836,7 @@ func TestNewFormSubmissionOwner(t *testing.T) { if err != nil { t.Fatalf("failed to create a form submission request: %v", err) } - subR.Id = "config2" - if err != nil { - t.Fatalf("Could not create request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) diff --git a/stanza/pubsub_test.go b/stanza/pubsub_test.go index ac227d2..88d19d9 100644 --- a/stanza/pubsub_test.go +++ b/stanza/pubsub_test.go @@ -44,9 +44,6 @@ func TestNewSubRequest(t *testing.T) { t.Fatalf("failed to create a sub request: %v", err) } subR.Id = "sub1" - if err != nil { - t.Fatalf("Could not create a sub request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -103,9 +100,6 @@ func TestNewUnSubRequest(t *testing.T) { t.Fatalf("failed to create an unsub request: %v", err) } subR.Id = "unsub1" - if err != nil { - t.Fatalf("Could not create a sub request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -167,9 +161,6 @@ func TestNewSubOptsRq(t *testing.T) { t.Fatalf("failed to create a sub options request: %v", err) } subR.Id = "options1" - if err != nil { - t.Fatalf("Could not create a sub request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -277,9 +268,6 @@ func TestNewFormSubmission(t *testing.T) { t.Fatalf("failed to create a form submission request: %v", err) } subR.Id = "options2" - if err != nil { - t.Fatalf("Could not create a sub request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -329,9 +317,6 @@ func TestNewSubAndConfig(t *testing.T) { t.Fatalf("failed to create a sub and config request: %v", err) } subR.Id = "sub1" - if err != nil { - t.Fatalf("Could not create a sub request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -501,9 +486,6 @@ func TestNewSpecificItemRequest(t *testing.T) { t.Fatalf("failed to create a specific item request: %v", err) } subR.Id = "items3" - if err != nil { - t.Fatalf("Could not create an items request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated sub request : %s", e) @@ -660,9 +642,6 @@ func TestNewDelItemFromNode(t *testing.T) { t.Fatalf("failed to create a delete item from node request: %v", err) } subR.Id = "retract1" - if err != nil { - t.Fatalf("Could not create a del item request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated del item request : %s", e) @@ -702,9 +681,6 @@ func TestNewCreateNode(t *testing.T) { t.Fatalf("failed to create a create node request: %v", err) } subR.Id = "create1" - if err != nil { - t.Fatalf("Could not create a create node request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated del item request : %s", e) @@ -777,9 +753,6 @@ func TestNewCreateAndConfigNode(t *testing.T) { t.Fatalf("failed to create a create and config node request: %v", err) } subR.Id = "create1" - if err != nil { - t.Fatalf("Could not create a create node request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated del item request : %s", e) @@ -828,9 +801,6 @@ func TestNewRetrieveAllSubsRequest(t *testing.T) { t.Fatalf("failed to create a get all subs request: %v", err) } subR.Id = "subscriptions1" - if err != nil { - t.Fatalf("Could not create a create node request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated del item request : %s", e) @@ -891,9 +861,6 @@ func TestNewRetrieveAllAffilsRequest(t *testing.T) { t.Fatalf("failed to create a get all affiliations request: %v", err) } subR.Id = "affil1" - if err != nil { - t.Fatalf("Could not create retreive all affiliations request : %s", err) - } if _, e := checkMarshalling(t, subR); e != nil { t.Fatalf("Failed to check marshalling for generated retreive all affiliations request : %s", e) diff --git a/tcp_server_mock.go b/tcp_server_mock.go index 117513a..55740fa 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -159,12 +159,15 @@ func respondToIQ(t *testing.T, sc *ServerConn) { // When a presence stanza is automatically sent (right now it's the case in the client), we may want to discard it // and test further stanzas. func discardPresence(t *testing.T, sc *ServerConn) { - sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + if err != nil { + t.Fatalf("failed to set deadline: %v", err) + } defer sc.connection.SetDeadline(time.Time{}) var presenceStz stanza.Presence recvBuf := make([]byte, len(InitialPresence)) - _, err := sc.connection.Read(recvBuf[:]) // recv data + _, err = sc.connection.Read(recvBuf[:]) // recv data if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { @@ -173,7 +176,7 @@ func discardPresence(t *testing.T, sc *ServerConn) { t.Errorf("read error: %s", err) } } - xml.Unmarshal(recvBuf, &presenceStz) + err = xml.Unmarshal(recvBuf, &presenceStz) if err != nil { t.Errorf("Expected presence but this happened : %s", err.Error()) @@ -182,10 +185,13 @@ func discardPresence(t *testing.T, sc *ServerConn) { // Reads next request coming from the Component. Expecting it to be an IQ request func receiveIq(sc *ServerConn) (*stanza.IQ, error) { - sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + err := sc.connection.SetDeadline(time.Now().Add(defaultTimeout)) + if err != nil { + return nil, err + } defer sc.connection.SetDeadline(time.Time{}) var iqStz stanza.IQ - err := sc.decoder.Decode(&iqStz) + err = sc.decoder.Decode(&iqStz) if err != nil { return nil, err } From 35e3defc625aee34a21f9a7ac4a08609e5b46ba6 Mon Sep 17 00:00:00 2001 From: Jack Henschel Date: Tue, 11 Feb 2020 16:29:52 +0100 Subject: [PATCH 41/54] Remove "no depdencies" statement from README As is apparent from the current go.mod file, this library definitely depends on various other libraries. This in turn makes it depend on Go 1.13 (currently). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62c6518..3487b01 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The goal is to make simple to write simple XMPP clients and components: - For writing simple chatbot to control a service or a thing, - For writing XMPP servers components. -The library is designed to have minimal dependencies. For now, the library does not depend on any other library. +The library is designed to have minimal dependencies. Currently it requires at least Go 1.13. ## Configuration and connection From 086ceb40476a18d3766d2726d7a6ccb46643d10b Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 18 Feb 2020 10:29:22 +0100 Subject: [PATCH 42/54] Removed unnecessary dependencies from the core lib go.mod --- cmd/go.sum | 14 ++++++++++++++ go.mod | 3 --- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/go.sum b/cmd/go.sum index 258caa5..c7e00fa 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -6,6 +6,8 @@ github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2e github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= github.com/bdlm/log v0.1.19 h1:GqVFZC+khJCEbtTmkaDL/araNDwxTeLBmdMK8pbRoBE= github.com/bdlm/log v0.1.19/go.mod h1:30V5Zwc5Vt5ePq5rd9KJ6JQ/A5aFUcKzq5fYtO7c9qc= github.com/bdlm/std v0.0.0-20180922040903-fd3b596111c7 h1:ggZyn+N8eoBh/qLla2kUtqm/ysjnkbzUxTQY+6LMshY= @@ -38,6 +40,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -63,6 +66,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -73,6 +77,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -87,6 +92,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -97,6 +103,7 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -126,6 +133,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= @@ -143,12 +152,14 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -203,6 +214,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -219,12 +231,14 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go.mod b/go.mod index d3b3273..f31fe40 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,8 @@ module gosrc.io/xmpp go 1.13 require ( - github.com/awesome-gocui/gocui v0.6.0 // indirect github.com/google/go-cmp v0.3.1 github.com/google/uuid v1.1.1 - github.com/spf13/viper v1.6.1 // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 nhooyr.io/websocket v1.6.5 - ) From 045914451266caf4f0345a551e3a6dcf17df303d Mon Sep 17 00:00:00 2001 From: rcorniere Date: Fri, 6 Mar 2020 16:44:01 +0100 Subject: [PATCH 43/54] First commit with dirty tests --- _examples/go.sum | 3 + _examples/xmpp_echo/xmpp_echo.go | 2 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 2 +- _examples/xmpp_oauth2/xmpp_oauth2.go | 2 +- .../xmpp_pubsub_client/xmpp_ps_client.go | 2 +- _examples/xmpp_websocket/xmpp_websocket.go | 2 +- auth.go | 13 +- client.go | 161 +++-- client_internal_test.go | 597 ++++++++++++++++++ client_test.go | 182 +++++- cmd/fluuxmpp/send.go | 2 +- cmd/go.sum | 9 + component.go | 8 +- config.go | 11 + router.go | 37 ++ session.go | 181 ++++-- stanza/fifo_queue.go | 32 + stanza/sasl_auth.go | 6 +- stanza/sasl_auth_test.go | 4 +- stanza/stanza_errors.go | 171 +++++ stanza/stream_features.go | 4 + stanza/stream_management.go | 261 +++++++- stanza/stream_management_test.go | 187 ++++++ stream_manager.go | 17 +- tcp_server_mock.go | 1 + xmpp_transport.go | 3 +- 26 files changed, 1771 insertions(+), 129 deletions(-) create mode 100644 stanza/fifo_queue.go create mode 100644 stanza/stanza_errors.go create mode 100644 stanza/stream_management_test.go diff --git a/_examples/go.sum b/_examples/go.sum index 286bc95..467aab6 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -99,7 +99,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA= github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go= +github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE= github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -155,6 +157,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go index b6c6766..d7a35c8 100644 --- a/_examples/xmpp_echo/xmpp_echo.go +++ b/_examples/xmpp_echo/xmpp_echo.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index b8075a2..31f000a 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -54,7 +54,7 @@ func main() { handleIQ(s, p, player) }) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_oauth2/xmpp_oauth2.go b/_examples/xmpp_oauth2/xmpp_oauth2.go index 89b2639..a993049 100644 --- a/_examples/xmpp_oauth2/xmpp_oauth2.go +++ b/_examples/xmpp_oauth2/xmpp_oauth2.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_pubsub_client/xmpp_ps_client.go b/_examples/xmpp_pubsub_client/xmpp_ps_client.go index b2e9cf6..2308071 100644 --- a/_examples/xmpp_pubsub_client/xmpp_ps_client.go +++ b/_examples/xmpp_pubsub_client/xmpp_ps_client.go @@ -38,7 +38,7 @@ func main() { log.Println("Received a message ! => \n" + string(data)) }) - client, err := xmpp.NewClient(config, router, func(err error) { log.Println(err) }) + client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) }) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_websocket/xmpp_websocket.go b/_examples/xmpp_websocket/xmpp_websocket.go index c8c0620..3a0c1ba 100644 --- a/_examples/xmpp_websocket/xmpp_websocket.go +++ b/_examples/xmpp_websocket/xmpp_websocket.go @@ -26,7 +26,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/auth.go b/auth.go index b8d20b9..902371b 100644 --- a/auth.go +++ b/auth.go @@ -60,10 +60,21 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str raw := "\x00" + user + "\x00" + secret enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) base64.StdEncoding.Encode(enc, []byte(raw)) - _, err := fmt.Fprintf(socket, "%s", stanza.NSSASL, mech, enc) + + a := stanza.SASLAuth{ + Mechanism: mech, + Value: string(enc), + } + data, err := xml.Marshal(a) if err != nil { return err } + n, err := socket.Write(data) + if err != nil { + return err + } else if n == 0 { + return errors.New("failed to write authSASL nonza to socket : wrote 0 bytes") + } // Next message should be either success or failure. val, err := stanza.NextPacket(decoder) diff --git a/client.go b/client.go index bd40c38..1be1f4d 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net" + "sync" "time" "gosrc.io/xmpp/stanza" @@ -14,15 +15,36 @@ import ( //============================================================================= // EventManager -// ConnState represents the current connection state. +// SyncConnState represents the current connection state. +type SyncConnState struct { + sync.RWMutex + // Current state of the client. Please use the dedicated getter and setter for this field as they are thread safe. + state ConnState +} type ConnState = uint8 +// getState is a thread-safe getter for the current state +func (scs *SyncConnState) getState() ConnState { + var res ConnState + scs.RLock() + res = scs.state + scs.RUnlock() + return res +} + +// setState is a thread-safe setter for the current +func (scs *SyncConnState) setState(cs ConnState) { + scs.Lock() + scs.state = cs + scs.Unlock() +} + // This is a the list of events happening on the connection that the // client can be notified about. const ( InitialPresence = "" StateDisconnected ConnState = iota - StateConnected + StateResuming StateSessionEstablished StateStreamError StatePermanentError @@ -31,7 +53,7 @@ const ( // Event is a structure use to convey event changes related to client state. This // is for example used to notify the client when the client get disconnected. type Event struct { - State ConnState + State SyncConnState Description string StreamError string SMState SMState @@ -44,7 +66,16 @@ type SMState struct { Id string // Inbound stanza count Inbound uint - // TODO Store location for IP affinity + + // IP affinity + preferredReconAddr string + + // Error + StreamErrorGroup stanza.StanzaErrorGroup + + // Track sent stanzas + *stanza.UnAckQueue + // TODO Store max and timestamp, to check if we should retry resumption or not } @@ -53,29 +84,35 @@ type SMState struct { type EventHandler func(Event) error type EventManager struct { - // Store current state - CurrentState ConnState + // Store current state. Please use "getState" and "setState" to access and/or modify this. + CurrentState SyncConnState // Callback used to propagate connection state changes Handler EventHandler } +// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee +// regarding the triggered callback function. func (em *EventManager) updateState(state ConnState) { - em.CurrentState = state + em.CurrentState.setState(state) if em.Handler != nil { em.Handler(Event{State: em.CurrentState}) } } +// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee +// regarding the triggered callback function. func (em *EventManager) disconnected(state SMState) { - em.CurrentState = StateDisconnected + em.CurrentState.setState(StateDisconnected) if em.Handler != nil { em.Handler(Event{State: em.CurrentState, SMState: state}) } } +// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee +// regarding the triggered callback function. func (em *EventManager) streamError(error, desc string) { - em.CurrentState = StateStreamError + em.CurrentState.setState(StateStreamError) if em.Handler != nil { em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc}) } @@ -90,7 +127,7 @@ var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ s // server. type Client struct { // Store user defined options and states - config Config + config *Config // Session gather data that can be accessed by users of this library Session *Session transport Transport @@ -100,6 +137,12 @@ type Client struct { EventManager // Handle errors from client execution ErrorHandler func(error) + + // Post connection hook. This will be executed on first connection + PostFirstConnHook func() error + + // Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198) + PostReconnectHook func() error } /* @@ -107,9 +150,9 @@ Setting up the client / Checking the parameters */ // NewClient generates a new XMPP client, based on Config passed as parameters. -// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the Jid. +// If host is not specified, the DNS SRV should be used to find the host from the domain part of the Jid. // Default the port to 5222. -func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { +func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) { if config.KeepaliveInterval == 0 { config.KeepaliveInterval = time.Second * 30 } @@ -169,26 +212,45 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e return } -// Connect triggers actual TCP connection, based on previously defined parameters. -// Connect simply triggers resumption, with an empty session state. +// Connect establishes a first time connection to a XMPP server. +// It calls the PostFirstConnHook func (c *Client) Connect() error { - var state SMState - return c.Resume(state) + err := c.connect() + if err != nil { + return err + } + // TODO: Do we always want to send initial presence automatically ? + // Do we need an option to avoid that or do we rely on client to send the presence itself ? + err = c.sendWithWriter(c.transport, []byte(InitialPresence)) + // Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions. + if c.PostFirstConnHook != nil { + err = c.PostFirstConnHook() + if err != nil { + return err + } + } + + // Start the keepalive go routine + keepaliveQuit := make(chan struct{}) + go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) + // Start the receiver go routine + go c.recv(keepaliveQuit) + return err } -// Resume attempts resuming a Stream Managed session, based on the provided stream management -// state. -func (c *Client) Resume(state SMState) error { +// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session +func (c *Client) connect() error { + var state SMState var err error - + // This is the TCP connection streamId, err := c.transport.Connect() if err != nil { return err } - c.updateState(StateConnected) - // Client is ok, we now open XMPP session - if c.Session, err = NewSession(c.transport, c.config, state); err != nil { + // Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding + // depending on state. + if c.Session, err = NewSession(c, state); err != nil { // Try to get the stream close tag from the server. go func() { for { @@ -212,22 +274,26 @@ func (c *Client) Resume(state SMState) error { c.Session.StreamId = streamId c.updateState(StateSessionEstablished) - // Start the keepalive go routine - keepaliveQuit := make(chan struct{}) - go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) - // Start the receiver go routine - state = c.Session.SMState - go c.recv(state, keepaliveQuit) - - // We're connected and can now receive and send messages. - //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") - // TODO: Do we always want to send initial presence automatically ? - // Do we need an option to avoid that or do we rely on client to send the presence itself ? - err = c.sendWithWriter(c.transport, []byte(InitialPresence)) - return err } +// Resume attempts resuming a Stream Managed session, based on the provided stream management +// state. See XEP-0198 +func (c *Client) Resume() error { + c.EventManager.updateState(StateResuming) + err := c.connect() + if err != nil { + return err + } + // Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrival + // for example. + if c.PostReconnectHook != nil { + err = c.PostReconnectHook() + } + return err +} + +// Disconnect disconnects the client from the server, sending a stream close nonza and closing the TCP connection. func (c *Client) Disconnect() error { if c.transport != nil { return c.transport.Close() @@ -252,6 +318,15 @@ func (c *Client) Send(packet stanza.Packet) error { return errors.New("cannot marshal packet " + err.Error()) } + // Store stanza as non-acked as part of stream management + // See https://xmpp.org/extensions/xep-0198.html#scenarios + if c.config.StreamManagementEnable { + if _, ok := packet.(stanza.SMRequest); !ok { + toStore := stanza.UnAckedStz{Stz: string(data)} + c.Session.SMState.UnAckQueue.Push(&toStore) + } + } + return c.sendWithWriter(c.transport, data) } @@ -284,6 +359,12 @@ func (c *Client) SendRaw(packet string) error { return errors.New("client is not connected") } + // Store stanza as non-acked as part of stream management + // See https://xmpp.org/extensions/xep-0198.html#scenarios + if c.config.StreamManagementEnable { + toStore := stanza.UnAckedStz{Stz: packet} + c.Session.SMState.UnAckQueue.Push(&toStore) + } return c.sendWithWriter(c.transport, []byte(packet)) } @@ -297,13 +378,13 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Go routines // Loop: Receive data from server -func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { +func (c *Client) recv(keepaliveQuit chan<- struct{}) { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.ErrorHandler(err) close(keepaliveQuit) - c.disconnected(state) + c.disconnected(c.Session.SMState) return } @@ -321,7 +402,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { answer := stanza.SMAnswer{XMLName: xml.Name{ Space: stanza.NSStreamManagement, Local: "a", - }, H: state.Inbound} + }, H: c.Session.SMState.Inbound} err = c.Send(answer) if err != nil { c.ErrorHandler(err) @@ -332,7 +413,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { c.transport.ReceivedStreamClose() return default: - state.Inbound++ + c.Session.SMState.Inbound++ } // Do normal route processing in a go-routine so we can immediately // start receiving other stanzas. This also allows route handlers to diff --git a/client_internal_test.go b/client_internal_test.go index 6daef09..9eeac7f 100644 --- a/client_internal_test.go +++ b/client_internal_test.go @@ -2,7 +2,16 @@ package xmpp import ( "bytes" + "encoding/xml" + "fmt" + "gosrc.io/xmpp/stanza" + "strconv" "testing" + "time" +) + +const ( + streamManagementID = "test-stream_management-id" ) func TestClient_Send(t *testing.T) { @@ -17,3 +26,591 @@ func TestClient_Send(t *testing.T) { t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String()) } } + +// Stream management test. +// Connection is established, then the server sends supported features and so on. +// After the bind, client attempts a stream management enablement, and server replies in kind. +func Test_StreamManagement(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var state SMState + + _, err = client.transport.Connect() + if err != nil { + return + } + + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + mock.Stop() +} + +// Absence of stream management test. +// Connection is established, then the server sends supported features and so on. +// Client has stream management disabled in its config, and should not ask for it. Server is not set up to reply. +func Test_NoStreamManagement(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: false} // Don't enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var state SMState + + _, err = client.transport.Connect() + if err != nil { + return + } + + done := make(chan struct{}) + // Client is ok, we now open XMPP session + go func() { + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + done <- struct{}{} + }() + + select { + case <-done: + case <-time.After(3 * time.Second): + t.Fatalf("timed out waiting for no stream management session. This probably means session process is broken") + } + + mock.Stop() +} + +func Test_StreamManagementNotSupported(t *testing.T) { + + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesNoStreamManagment(t, sc) // Send post auth features + bind(t, sc) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var state SMState + + _, err = client.transport.Connect() + if err != nil { + return + } + + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + if client.Session.Features.DoesStreamManagement() { + t.Fatalf("server does not provide stream management") + } + + mock.Stop() +} + +func Test_StreamManagementNoResume(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, false) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management. This should be overridden by the response from the server + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var state SMState + + _, err = client.transport.Connect() + if err != nil { + return + } + + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + + if IsStreamResumable(client) { + t.Fatalf("server does not support resumption but client says stream is resumable") + } + mock.Stop() +} + +func Test_StreamManagementResume(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + discardPresence(t, sc) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + err = client.Connect() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + statusCorrectChan := make(chan struct{}) + kill := make(chan struct{}) + + transp, ok := client.transport.(*XMPPTransport) + if !ok { + t.Fatalf("problem with client transport ") + } + + transp.conn.Close() + mock.Stop() + + // Check if status is correctly updated because of the disconnect + go checkClientResumeStatus(client, statusCorrectChan, kill) + select { + case <-statusCorrectChan: + // Test passed + case <-time.After(5 * time.Second): + kill <- struct{}{} + t.Fatalf("Client is not in disconnected state while it should be. Timed out") + } + + // Check if the client can have its connection resumed using its state but also its configuration + if !IsStreamResumable(client) { + t.Fatalf("should support resumption") + } + + // Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler + // and they should be different between a first connection and a stream resume since exchanged messages + // are different (See XEP-0198) + mock2 := ServerMock{} + mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + // Reconnect + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + resumeStream(t, sc) + }) + + // Reconnect + err = client.Resume() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + mock2.Stop() + +} + +//func Test_SendStanzaQueueNoSM(t *testing.T) { +// +// panic("not implemented") +//} + +func Test_StreamManagementFail(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, true, true) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var state SMState + _, err = client.transport.Connect() + if err != nil { + return + } + + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err == nil { + t.Fatalf("test is supposed to err") + } + if client.Session.SMState.StreamErrorGroup == nil { + t.Fatalf("error was not stored correctly in session state") + } + + mock.Stop() +} + +func Test_SendStanzaQueueWithSM(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + serverDone := make(chan struct{}) + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + + // Ignore the initial presence sent to the server by the client so we can move on to the next packet. + discardPresence(t, sc) + + // Used here to silently discard the IQ sent by the client, in order to later trigger a resend + skipPacket(t, sc) + // Respond to the client ACK request with a number of processed stanzas of 0. This should trigger a resend + // of previously ignored stanza to the server, which this handler element will be expecting. + respondWithAck(t, sc, 0, serverDone) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + err = client.Connect() + + client.SendRaw(` + + +`) + + // Last stanza was discarded silently by the server. Let's ask an ack for it. This should trigger resend as the server + // will respond with an acknowledged number of stanzas of 0. + r := stanza.SMRequest{} + client.Send(r) + + select { + case <-time.After(defaultChannelTimeout): + t.Fatalf("server failed to complete the test in time") + case <-serverDone: + // Test completed successfully + } + + mock.Stop() +} + +//======================================================================== +// Helper functions for tests + +func skipPacket(t *testing.T, sc *ServerConn) { + var p stanza.IQ + se, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read packet: %s", err) + return + } + if err := sc.decoder.DecodeElement(&p, &se); err != nil { + t.Fatalf("cannot decode packet: %s", err) + return + } +} + +func respondWithAck(t *testing.T, sc *ServerConn, h int, serverDone chan struct{}) { + + // Mock server reads the ack request + var p stanza.SMRequest + se, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read packet: %s", err) + return + } + if err := sc.decoder.DecodeElement(&p, &se); err != nil { + t.Fatalf("cannot decode packet: %s", err) + return + } + + // Mock server sends the ack response + a := stanza.SMAnswer{ + H: uint(h), + } + data, err := xml.Marshal(a) + _, err = sc.connection.Write(data) + if err != nil { + t.Fatalf("failed to send response ack") + } + + // Mock server reads the re-sent stanza that was previously discarded intentionally + var p2 stanza.IQ + nse, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read packet: %s", err) + return + } + if err := sc.decoder.DecodeElement(&p2, &nse); err != nil { + t.Fatalf("cannot decode packet: %s", err) + return + } + serverDone <- struct{}{} +} + +func sendFeaturesStreamManagment(t *testing.T, sc *ServerConn) { + // This is a basic server, supporting only 2 features after auth: stream management & session binding + features := ` + + +` + if _, err := fmt.Fprintln(sc.connection, features); err != nil { + t.Fatalf("cannot send stream feature: %s", err) + } +} + +func sendFeaturesNoStreamManagment(t *testing.T, sc *ServerConn) { + // This is a basic server, supporting only 2 features after auth: stream management & session binding + features := ` + +` + if _, err := fmt.Fprintln(sc.connection, features); err != nil { + t.Fatalf("cannot send stream feature: %s", err) + } +} + +// enableStreamManagement is a function for the mock server that can either mock a successful session, or fail depending on +// the value of the "fail" boolean. True means the session should fail. +func enableStreamManagement(t *testing.T, sc *ServerConn, fail bool, resume bool) { + // Decode element into pointer storage + var ed stanza.SMEnable + se, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read stream management enable: %s", err) + return + } + if err := sc.decoder.DecodeElement(&ed, &se); err != nil { + t.Fatalf("cannot decode stream management enable: %s", err) + return + } + + if fail { + f := stanza.SMFailed{ + H: nil, + StreamErrorGroup: &stanza.UnexpectedRequest{}, + } + data, err := xml.Marshal(f) + if err != nil { + t.Fatalf("failed to marshall error response: %s", err) + } + sc.connection.Write(data) + } else { + e := &stanza.SMEnabled{ + Resume: strconv.FormatBool(resume), + Id: streamManagementID, + } + data, err := xml.Marshal(e) + if err != nil { + t.Fatalf("failed to marshall error response: %s", err) + } + sc.connection.Write(data) + } +} + +func resumeStream(t *testing.T, sc *ServerConn) { + h := uint(0) + response := stanza.SMResumed{ + PrevId: streamManagementID, + H: &h, + } + + data, err := xml.Marshal(response) + if err != nil { + t.Fatalf("failed to marshall stream management enabled response : %s", err) + } + + writtenChan := make(chan struct{}) + + go func() { + sc.connection.Write(data) + writtenChan <- struct{}{} + }() + select { + case <-writtenChan: + // We're done here + return + case <-time.After(defaultTimeout): + t.Fatalf("failed to write enabled nonza to client") + } +} + +func checkClientResumeStatus(client *Client, statusCorrectChan chan struct{}, killChan chan struct{}) { + for { + if client.CurrentState.getState() == StateDisconnected { + statusCorrectChan <- struct{}{} + } + select { + case <-killChan: + return + case <-time.After(time.Millisecond * 10): + // Keep checking status value + } + } +} diff --git a/client_test.go b/client_test.go index 00c956a..3cdd77a 100644 --- a/client_test.go +++ b/client_test.go @@ -20,18 +20,20 @@ const ( func TestEventManager(t *testing.T) { mgr := EventManager{} - mgr.updateState(StateConnected) - if mgr.CurrentState != StateConnected { + mgr.updateState(StateResuming) + if mgr.CurrentState.getState() != StateResuming { t.Fatal("CurrentState not updated by updateState()") } mgr.disconnected(SMState{}) - if mgr.CurrentState != StateDisconnected { + + if mgr.CurrentState.getState() != StateDisconnected { t.Fatalf("CurrentState not reset by disconnected()") } mgr.streamError(ErrTLSNotSupported.Error(), "") - if mgr.CurrentState != StateStreamError { + + if mgr.CurrentState.getState() != StateStreamError { t.Fatalf("CurrentState not set by streamError()") } } @@ -53,7 +55,7 @@ func TestClient_Connect(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -84,7 +86,7 @@ func TestClient_NoInsecure(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -117,7 +119,7 @@ func TestClient_FeaturesTracking(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -147,7 +149,7 @@ func TestClient_RFC3921Session(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -366,7 +368,7 @@ func TestClient_DisconnectStreamManager(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -386,6 +388,162 @@ func TestClient_DisconnectStreamManager(t *testing.T) { mock.Stop() } +func Test_ClientPostConnectHook(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, sc *ServerConn) { + handlerClientConnectSuccess(t, sc) + done <- struct{}{} + } + + hookChan := make(chan struct{}) + mock := &ServerMock{} + testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, testClientPostConnectHook) + + mock.Start(t, testServerAddress, h) + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testServerAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true} + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + // The post connection client hook should just write to a channel that we will read later. + client.PostFirstConnHook = func() error { + go func() { + hookChan <- struct{}{} + }() + return nil + } + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + client.ErrorHandler = errorHandler + if err = client.Connect(); err != nil { + t.Errorf("XMPP connection failed: %s", err) + } + + // Check if the post connection client hook was correctly called + select { + case err := <-errChan: // If the server sends an error, or there is a connection error + t.Fatal(err.Error()) + case <-time.After(defaultChannelTimeout): // If we timeout + t.Fatal("Failed to call post connection client hook") + case <-hookChan: + // Test succeeded, channel was written to. + } + + select { + case <-done: + mock.Stop() + case <-time.After(defaultChannelTimeout): + t.Fatal("The mock server failed to finish its job !") + } +} + +func Test_ClientPostReconnectHook(t *testing.T) { + hookChan := make(chan struct{}) + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + client.PostReconnectHook = func() error { + go func() { + hookChan <- struct{}{} + }() + return nil + } + + err = client.Connect() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + transp, ok := client.transport.(*XMPPTransport) + if !ok { + t.Fatalf("problem with client transport ") + } + + transp.conn.Close() + mock.Stop() + + // Check if the client can have its connection resumed using its state but also its configuration + if !IsStreamResumable(client) { + t.Fatalf("should support resumption") + } + + // Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler + // and they should be different between a first connection and a stream resume since exchanged messages + // are different (See XEP-0198) + mock2 := ServerMock{} + mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + // Reconnect + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + resumeStream(t, sc) + }) + + // Reconnect + err = client.Resume() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + select { + case <-time.After(defaultChannelTimeout): // If we timeout + t.Fatal("Failed to call post connection client hook") + case <-hookChan: + // Test succeeded, channel was written to. + } + + mock2.Stop() +} + //============================================================================= // Basic XMPP Server Mock Handlers. @@ -449,7 +607,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) { var token xml.Token token, err := sc.decoder.Token() if err != nil { - t.Errorf("cannot read next token: %s", err) + t.Fatalf("cannot read next token: %s", err) } switch elem := token.(type) { @@ -464,6 +622,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) { } return } + } } @@ -472,7 +631,6 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) mock.Start(t, testServerAddress, serverHandler) - config := Config{ TransportConfiguration: TransportConfiguration{ Address: testServerAddress, @@ -484,7 +642,7 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } diff --git a/cmd/fluuxmpp/send.go b/cmd/fluuxmpp/send.go index 7e7ed97..27c1a67 100644 --- a/cmd/fluuxmpp/send.go +++ b/cmd/fluuxmpp/send.go @@ -32,7 +32,7 @@ func sendxmpp(cmd *cobra.Command, args []string) { msgText := args[1] var err error - client, err := xmpp.NewClient(xmpp.Config{ + client, err := xmpp.NewClient(&xmpp.Config{ TransportConfiguration: xmpp.TransportConfiguration{ Address: viper.GetString("addr"), }, diff --git a/cmd/go.sum b/cmd/go.sum index c7e00fa..8398605 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -65,6 +65,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -92,6 +93,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -148,10 +150,12 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -159,6 +163,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= @@ -207,6 +212,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -231,6 +237,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -238,9 +245,11 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= diff --git a/component.go b/component.go index a57b07b..ec0e8df 100644 --- a/component.go +++ b/component.go @@ -60,11 +60,10 @@ func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (* // Connect triggers component connection to XMPP server component port. // TODO: Failed handshake should be a permanent error func (c *Component) Connect() error { - var state SMState - return c.Resume(state) + return c.Resume() } -func (c *Component) Resume(sm SMState) error { +func (c *Component) Resume() error { var err error var streamId string if c.ComponentOptions.TransportConfiguration.Domain == "" { @@ -73,16 +72,13 @@ func (c *Component) Resume(sm SMState) error { c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) if err != nil { c.updateState(StatePermanentError) - return NewConnError(err, true) } if streamId, err = c.transport.Connect(); err != nil { c.updateState(StatePermanentError) - return NewConnError(err, true) } - c.updateState(StateConnected) // Authentication if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("%s", c.handshake(streamId)))); err != nil { diff --git a/config.go b/config.go index 178da2e..4609a0a 100644 --- a/config.go +++ b/config.go @@ -21,4 +21,15 @@ type Config struct { // Insecure can be set to true to allow to open a session without TLS. If TLS // is supported on the server, we will still try to use it. Insecure bool + + // Activate stream management process during session + StreamManagementEnable bool + // Enable stream management resume capability + streamManagementResume bool +} + +// IsStreamResumable tells if a stream session is resumable by reading the "config" part of a client. +// It checks if stream management is enabled, and if stream resumption was set and accepted by the server. +func IsStreamResumable(c *Client) bool { + return c.config.StreamManagementEnable && c.config.streamManagementResume } diff --git a/router.go b/router.go index f20af5b..d334d36 100644 --- a/router.go +++ b/router.go @@ -42,6 +42,17 @@ 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 stanza.Packet) { + a, isA := p.(stanza.SMAnswer) + if isA { + switch tt := s.(type) { + case *Client: + lastAcked := a.H + SendMissingStz(int(lastAcked), s, tt.Session.SMState.UnAckQueue) + case *Component: + // TODO + default: + } + } iq, isIq := p.(*stanza.IQ) if isIq { r.IQResultRouteLock.RLock() @@ -70,6 +81,32 @@ func (r *Router) route(s Sender, p stanza.Packet) { } } +func SendMissingStz(lastSent int, s Sender, uaq *stanza.UnAckQueue) error { + uaq.RWMutex.Lock() + if len(uaq.Uslice) <= 0 { + uaq.RWMutex.Unlock() + return nil + } + last := uaq.Uslice[len(uaq.Uslice)-1] + if last.Id > lastSent { + // Remove sent stanzas from the queue + uaq.PopN(lastSent - last.Id) + // Re-send non acknowledged stanzas + for _, elt := range uaq.PopN(len(uaq.Uslice)) { + eltStz := elt.(*stanza.UnAckedStz) + err := s.SendRaw(eltStz.Stz) + if err != nil { + return err + } + + } + // Ask for updates on stanzas we just sent to the entity + s.Send(stanza.SMRequest{}) + } + uaq.RWMutex.Unlock() + return nil +} + func iqNotImplemented(s Sender, iq *stanza.IQ) { err := stanza.Err{ XMLName: xml.Name{Local: "error"}, diff --git a/session.go b/session.go index 182e32b..05bdce3 100644 --- a/session.go +++ b/session.go @@ -1,10 +1,11 @@ package xmpp import ( + "encoding/xml" "errors" "fmt" - "gosrc.io/xmpp/stanza" + "strconv" ) type Session struct { @@ -23,44 +24,67 @@ type Session struct { err error } -func NewSession(transport Transport, o Config, state SMState) (*Session, error) { - s := new(Session) - s.transport = transport - s.SMState = state - s.init(o) +func NewSession(c *Client, state SMState) (*Session, error) { + var s *Session + if c.Session == nil { + s = new(Session) + s.transport = c.transport + s.SMState = state + s.init() + } else { + s = c.Session + // We keep information about the previously set session, like the session ID, but we read server provided + // info again in case it changed between session break and resume, such as features. + s.init() + } if s.err != nil { return nil, NewConnError(s.err, true) } - if !transport.IsSecure() { - s.startTlsIfSupported(o) + if !c.transport.IsSecure() { + s.startTlsIfSupported(c.config) } - if !transport.IsSecure() && !o.Insecure { + if !c.transport.IsSecure() && !c.config.Insecure { err := fmt.Errorf("failed to negotiate TLS session : %s", s.err) return nil, NewConnError(err, true) } if s.TlsEnabled { - s.reset(o) + s.reset() } // auth - s.auth(o) - s.reset(o) + s.auth(c.config) + if s.err != nil { + return s, s.err + } + s.reset() + if s.err != nil { + return s, s.err + } // attempt resumption - if s.resume(o) { + if s.resume(c.config) { return s, s.err } // otherwise, bind resource and 'start' XMPP session - s.bind(o) - s.rfc3921Session(o) + s.bind(c.config) + if s.err != nil { + return s, s.err + } + s.rfc3921Session() + if s.err != nil { + return s, s.err + } // Enable stream management if supported - s.EnableStreamManagement(o) + s.EnableStreamManagement(c.config) + if s.err != nil { + return s, s.err + } return s, s.err } @@ -70,19 +94,20 @@ func (s *Session) PacketId() string { return fmt.Sprintf("%x", s.lastPacketId) } -func (s *Session) init(o Config) { - s.Features = s.open(o.parsedJid.Domain) +// init gathers information on the session such as stream features from the server. +func (s *Session) init() { + s.Features = s.extractStreamFeatures() } -func (s *Session) reset(o Config) { +func (s *Session) reset() { if s.StreamId, s.err = s.transport.StartStream(); s.err != nil { return } - s.Features = s.open(o.parsedJid.Domain) + s.Features = s.extractStreamFeatures() } -func (s *Session) open(domain string) (f stanza.StreamFeatures) { +func (s *Session) extractStreamFeatures() (f stanza.StreamFeatures) { // extract stream features if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil { s.err = errors.New("stream open decode features: " + s.err.Error()) @@ -90,7 +115,7 @@ func (s *Session) open(domain string) (f stanza.StreamFeatures) { return } -func (s *Session) startTlsIfSupported(o Config) { +func (s *Session) startTlsIfSupported(o *Config) { if s.err != nil { return } @@ -125,7 +150,7 @@ func (s *Session) startTlsIfSupported(o Config) { } } -func (s *Session) auth(o Config) { +func (s *Session) auth(o *Config) { if s.err != nil { return } @@ -134,7 +159,7 @@ func (s *Session) auth(o Config) { } // Attempt to resume session using stream management -func (s *Session) resume(o Config) bool { +func (s *Session) resume(o *Config) bool { if !s.Features.DoesStreamManagement() { return false } @@ -142,9 +167,16 @@ func (s *Session) resume(o Config) bool { return false } - fmt.Fprintf(s.transport, "", - stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id) + rsm := stanza.SMResume{ + PrevId: s.SMState.Id, + H: &s.SMState.Inbound, + } + data, err := xml.Marshal(rsm) + _, err = s.transport.Write(data) + if err != nil { + return false + } var packet stanza.Packet packet, s.err = stanza.NextPacket(s.transport.GetDecoder()) if s.err == nil { @@ -165,20 +197,48 @@ func (s *Session) resume(o Config) bool { return false } -func (s *Session) bind(o Config) { +func (s *Session) bind(o *Config) { if s.err != nil { return } // Send IQ message asking to bind to the local user name. var resource = o.parsedJid.Resource - if resource != "" { - fmt.Fprintf(s.transport, "%s", - s.PacketId(), stanza.NSBind, resource) - } else { - fmt.Fprintf(s.transport, "", s.PacketId(), stanza.NSBind) + iqB, err := stanza.NewIQ(stanza.Attrs{ + Type: stanza.IQTypeSet, + Id: s.PacketId(), + }) + if err != nil { + s.err = err + return } + // Check if we already have a resource name, and include it in the request if so + if resource != "" { + iqB.Payload = &stanza.Bind{ + Resource: resource, + } + } else { + iqB.Payload = &stanza.Bind{} + + } + + // Send the bind request IQ + data, err := xml.Marshal(iqB) + if err != nil { + s.err = err + return + } + n, err := s.transport.Write(data) + if err != nil { + s.err = err + return + } else if n == 0 { + s.err = errors.New("failed to write bind iq stanza to the server : wrote 0 bytes") + return + } + + // Check the server response var iq stanza.IQ if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil { s.err = errors.New("error decoding iq bind result: " + s.err.Error()) @@ -197,7 +257,7 @@ func (s *Session) bind(o Config) { } // After the bind, if the session is not optional (as per old RFC 3921), we send the session open iq. -func (s *Session) rfc3921Session(o Config) { +func (s *Session) rfc3921Session() { if s.err != nil { return } @@ -205,7 +265,29 @@ func (s *Session) rfc3921Session(o Config) { var iq stanza.IQ // We only negotiate session binding if it is mandatory, we skip it when optional. if !s.Features.Session.IsOptional() { - fmt.Fprintf(s.transport, "", s.PacketId(), stanza.NSSession) + se, err := stanza.NewIQ(stanza.Attrs{ + Type: stanza.IQTypeSet, + Id: s.PacketId(), + }) + if err != nil { + s.err = err + return + } + se.Payload = &stanza.StreamSession{} + data, err := xml.Marshal(se) + if err != nil { + s.err = err + return + } + n, err := s.transport.Write(data) + if err != nil { + s.err = err + return + } else if n == 0 { + s.err = errors.New("there was a problem marshaling the session IQ : wrote 0 bytes to server") + return + } + if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil { s.err = errors.New("expecting iq result after session open: " + s.err.Error()) return @@ -214,28 +296,47 @@ func (s *Session) rfc3921Session(o Config) { } // Enable stream management, with session resumption, if supported. -func (s *Session) EnableStreamManagement(o Config) { +func (s *Session) EnableStreamManagement(o *Config) { if s.err != nil { return } - if !s.Features.DoesStreamManagement() { + if !s.Features.DoesStreamManagement() || !o.StreamManagementEnable { + return + } + q := stanza.NewUnAckQueue() + ebleNonza := stanza.SMEnable{Resume: &o.streamManagementResume} + pktStr, err := xml.Marshal(ebleNonza) + if err != nil { + s.err = err + return + } + _, err = s.transport.Write(pktStr) + if err != nil { + s.err = err return } - - fmt.Fprintf(s.transport, "", stanza.NSStreamManagement) var packet stanza.Packet packet, s.err = stanza.NextPacket(s.transport.GetDecoder()) if s.err == nil { switch p := packet.(type) { case stanza.SMEnabled: - s.SMState = SMState{Id: p.Id} + // Server allows resumption or not using SMEnabled attribute "resume". We must read the server response + // and update config accordingly + b, err := strconv.ParseBool(p.Resume) + if err != nil || !b { + o.StreamManagementEnable = false + } + s.SMState = SMState{Id: p.Id, preferredReconAddr: p.Location} + s.SMState.UnAckQueue = q case stanza.SMFailed: // TODO: Store error in SMState, for later inspection + s.SMState = SMState{StreamErrorGroup: p.StreamErrorGroup} + s.SMState.UnAckQueue = q + s.err = errors.New("failed to establish session : " + s.SMState.StreamErrorGroup.GroupErrorName()) default: s.err = errors.New("unexpected reply to SM enable") } } - return } diff --git a/stanza/fifo_queue.go b/stanza/fifo_queue.go new file mode 100644 index 0000000..dcdab02 --- /dev/null +++ b/stanza/fifo_queue.go @@ -0,0 +1,32 @@ +package stanza + +// FIFO queue for string contents +// Implementations have no guarantee regarding thread safety ! +type FifoQueue interface { + // Pop returns the first inserted element still in queue and delete it from queue + // No guarantee regarding thread safety ! + Pop() Queueable + + // PopN returns the N first inserted elements still in queue and delete them from queue + // No guarantee regarding thread safety ! + PopN(i int) []Queueable + + // Peek returns a copy of the first inserted element in queue without deleting it + // No guarantee regarding thread safety ! + Peek() Queueable + + // Peek returns a copy of the first inserted element in queue without deleting it + // No guarantee regarding thread safety ! + PeekN() []Queueable + // Push adds an element to the queue + // No guarantee regarding thread safety ! + Push(s Queueable) error + + // Empty returns true if queue is empty + // No guarantee regarding thread safety ! + Empty() bool +} + +type Queueable interface { + QueueableName() string +} diff --git a/stanza/sasl_auth.go b/stanza/sasl_auth.go index 9dfe557..2fb660e 100644 --- a/stanza/sasl_auth.go +++ b/stanza/sasl_auth.go @@ -93,8 +93,8 @@ func (b *Bind) GetSet() *ResultSet { // This is the draft defining how to handle the transition: // https://tools.ietf.org/html/draft-cridland-xmpp-session-01 type StreamSession struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` - Optional bool // If element does exist, it mean we are not required to open session + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` + Optional *struct{} // If element does exist, it mean we are not required to open session // Result sets ResultSet *ResultSet `xml:"set,omitempty"` } @@ -109,7 +109,7 @@ func (s *StreamSession) GetSet() *ResultSet { func (s *StreamSession) IsOptional() bool { if s.XMLName.Local == "session" { - return s.Optional + return s.Optional != nil } // If session element is missing, then we should not use session return true diff --git a/stanza/sasl_auth_test.go b/stanza/sasl_auth_test.go index 3e37453..600035c 100644 --- a/stanza/sasl_auth_test.go +++ b/stanza/sasl_auth_test.go @@ -9,7 +9,7 @@ import ( // Check that we can detect optional session from advertised stream features func TestSessionFeatures(t *testing.T) { - streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: true}} + streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: &struct{}{}}} data, err := xml.Marshal(streamFeatures) if err != nil { @@ -32,7 +32,7 @@ func TestSessionIQ(t *testing.T) { if err != nil { t.Fatalf("failed to create IQ: %v", err) } - iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true} + iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: &struct{}{}} data, err := xml.Marshal(iq) if err != nil { diff --git a/stanza/stanza_errors.go b/stanza/stanza_errors.go new file mode 100644 index 0000000..c66ea33 --- /dev/null +++ b/stanza/stanza_errors.go @@ -0,0 +1,171 @@ +package stanza + +import ( + "encoding/xml" +) + +type StanzaErrorGroup interface { + GroupErrorName() string +} + +type BadFormat struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-format"` +} + +func (e *BadFormat) GroupErrorName() string { return "bad-format" } + +type BadNamespacePrefix struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-namespace-prefix"` +} + +func (e *BadNamespacePrefix) GroupErrorName() string { return "bad-namespace-prefix" } + +type Conflict struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas conflict"` +} + +func (e *Conflict) GroupErrorName() string { return "conflict" } + +type ConnectionTimeout struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas connection-timeout"` +} + +func (e *ConnectionTimeout) GroupErrorName() string { return "connection-timeout" } + +type HostGone struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-gone"` +} + +func (e *HostGone) GroupErrorName() string { return "host-gone" } + +type HostUnknown struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-unknown"` +} + +func (e *HostUnknown) GroupErrorName() string { return "host-unknown" } + +type ImproperAddressing struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas improper-addressing"` +} + +func (e *ImproperAddressing) GroupErrorName() string { return "improper-addressing" } + +type InternalServerError struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error"` +} + +func (e *InternalServerError) GroupErrorName() string { return "internal-server-error" } + +type InvalidForm struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-from"` +} + +func (e *InvalidForm) GroupErrorName() string { return "invalid-from" } + +type InvalidId struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-id"` +} + +func (e *InvalidId) GroupErrorName() string { return "invalid-id" } + +type InvalidNamespace struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-namespace"` +} + +func (e *InvalidNamespace) GroupErrorName() string { return "invalid-namespace" } + +type InvalidXML struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-xml"` +} + +func (e *InvalidXML) GroupErrorName() string { return "invalid-xml" } + +type NotAuthorized struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-authorized"` +} + +func (e *NotAuthorized) GroupErrorName() string { return "not-authorized" } + +type NotWellFormed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-well-formed"` +} + +func (e *NotWellFormed) GroupErrorName() string { return "not-well-formed" } + +type PolicyViolation struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas policy-violation"` +} + +func (e *PolicyViolation) GroupErrorName() string { return "policy-violation" } + +type RemoteConnectionFailed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas remote-connection-failed"` +} + +func (e *RemoteConnectionFailed) GroupErrorName() string { return "remote-connection-failed" } + +type Reset struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas reset"` +} + +func (e *Reset) GroupErrorName() string { return "reset" } + +type ResourceConstraint struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint"` +} + +func (e *ResourceConstraint) GroupErrorName() string { return "resource-constraint" } + +type RestrictedXML struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas restricted-xml"` +} + +func (e *RestrictedXML) GroupErrorName() string { return "restricted-xml" } + +type SeeOtherHost struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas see-other-host"` +} + +func (e *SeeOtherHost) GroupErrorName() string { return "see-other-host" } + +type SystemShutdown struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas system-shutdown"` +} + +func (e *SystemShutdown) GroupErrorName() string { return "system-shutdown" } + +type UndefinedCondition struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition"` +} + +func (e *UndefinedCondition) GroupErrorName() string { return "undefined-condition" } + +type UnsupportedEncoding struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-encoding"` +} + +type UnexpectedRequest struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request"` +} + +func (e *UnexpectedRequest) GroupErrorName() string { return "unexpected-request" } + +func (e *UnsupportedEncoding) GroupErrorName() string { return "unsupported-encoding" } + +type UnsupportedStanzaType struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-stanza-type"` +} + +func (e *UnsupportedStanzaType) GroupErrorName() string { return "unsupported-stanza-type" } + +type UnsupportedVersion struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-version"` +} + +func (e *UnsupportedVersion) GroupErrorName() string { return "unsupported-version" } + +type XMLNotWellFormed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas xml-not-well-formed"` +} + +func (e *XMLNotWellFormed) GroupErrorName() string { return "xml-not-well-formed" } diff --git a/stanza/stream_features.go b/stanza/stream_features.go index d5bed5c..d1b6274 100644 --- a/stanza/stream_features.go +++ b/stanza/stream_features.go @@ -118,6 +118,10 @@ type streamManagement struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"` } +func (streamManagement) Name() string { + return "streamManagement" +} + func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) { if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" { return true diff --git a/stanza/stream_management.go b/stanza/stream_management.go index ddbe9cd..f48d9ca 100644 --- a/stanza/stream_management.go +++ b/stanza/stream_management.go @@ -3,12 +3,19 @@ package stanza import ( "encoding/xml" "errors" + "sync" ) const ( NSStreamManagement = "urn:xmpp:sm:3" ) +type SMEnable struct { + XMLName xml.Name `xml:"urn:xmpp:sm:3 enable"` + Max *uint `xml:"max,attr,omitempty"` + Resume *bool `xml:"resume,attr,omitempty"` +} + // Enabled as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#enable type SMEnabled struct { @@ -23,6 +30,94 @@ func (SMEnabled) Name() string { return "Stream Management: enabled" } +type UnAckQueue struct { + Uslice []*UnAckedStz + sync.RWMutex +} +type UnAckedStz struct { + Id int + Stz string +} + +func NewUnAckQueue() *UnAckQueue { + return &UnAckQueue{ + Uslice: make([]*UnAckedStz, 0, 10), // Capacity is 0 to comply with "Push" implementation (so that no reachable element is nil) + RWMutex: sync.RWMutex{}, + } +} + +func (u *UnAckedStz) QueueableName() string { + return "Un-acknowledged stanza" +} + +func (uaq *UnAckQueue) PeekN(n int) []Queueable { + if n <= 0 { + return []Queueable{} + } + if len(uaq.Uslice) < n { + n = len(uaq.Uslice) + } + + if len(uaq.Uslice) == 0 { + return []Queueable{} + } + var r []Queueable + for i := 0; i < n; i++ { + r = append(r, uaq.Uslice[i]) + } + return r +} + +// No guarantee regarding thread safety ! +func (uaq *UnAckQueue) Pop() Queueable { + r := uaq.Peek() + if r != nil { + uaq.Uslice = uaq.Uslice[1:] + } + return r +} + +// No guarantee regarding thread safety ! +func (uaq *UnAckQueue) PopN(n int) []Queueable { + r := uaq.PeekN(n) + uaq.Uslice = uaq.Uslice[len(r):] + return r +} + +func (uaq *UnAckQueue) Peek() Queueable { + if len(uaq.Uslice) == 0 { + return nil + } + r := uaq.Uslice[0] + return r +} + +func (uaq *UnAckQueue) Push(s Queueable) error { + pushIdx := 1 + if len(uaq.Uslice) != 0 { + pushIdx = uaq.Uslice[len(uaq.Uslice)-1].Id + 1 + } + + sStz, ok := s.(*UnAckedStz) + if !ok { + return errors.New("element in not compatible with this queue. expected an UnAckedStz") + } + + e := UnAckedStz{ + Id: pushIdx, + Stz: sStz.Stz, + } + + uaq.Uslice = append(uaq.Uslice, &e) + + return nil +} + +func (uaq *UnAckQueue) Empty() bool { + r := len(uaq.Uslice) + return r == 0 +} + // Request as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMRequest struct { @@ -37,7 +132,7 @@ func (SMRequest) Name() string { // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMAnswer struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 a"` - H uint `xml:"h,attr,omitempty"` + H uint `xml:"h,attr"` } func (SMAnswer) Name() string { @@ -49,24 +144,175 @@ func (SMAnswer) Name() string { type SMResumed struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"` PrevId string `xml:"previd,attr,omitempty"` - H uint `xml:"h,attr,omitempty"` + H *uint `xml:"h,attr,omitempty"` } func (SMResumed) Name() string { return "Stream Management: resumed" } +// Resumed as defined in Stream Management spec +// Reference: https://xmpp.org/extensions/xep-0198.html#acking +type SMResume struct { + XMLName xml.Name `xml:"urn:xmpp:sm:3 resume"` + PrevId string `xml:"previd,attr,omitempty"` + H *uint `xml:"h,attr,omitempty"` +} + +func (SMResume) Name() string { + return "Stream Management: resume" +} + // Failed as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMFailed struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"` - // TODO: Handle decoding error cause (need custom parsing). + H *uint `xml:"h,attr,omitempty"` + + StreamErrorGroup StanzaErrorGroup } func (SMFailed) Name() string { return "Stream Management: failed" } +func (smf *SMFailed) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + smf.XMLName = start.Name + + // According to https://xmpp.org/rfcs/rfc3920.html#def we should have no attributes aside from the namespace + // which we don't use internally + + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + // Decode sub-elements + var err error + switch tt.Name.Local { + case "bad-format": + bf := BadFormat{} + err = d.DecodeElement(&bf, &tt) + smf.StreamErrorGroup = &bf + case "bad-namespace-prefix": + bnp := BadNamespacePrefix{} + err = d.DecodeElement(&bnp, &tt) + smf.StreamErrorGroup = &bnp + case "conflict": + c := Conflict{} + err = d.DecodeElement(&c, &tt) + smf.StreamErrorGroup = &c + case "connection-timeout": + ct := ConnectionTimeout{} + err = d.DecodeElement(&ct, &tt) + smf.StreamErrorGroup = &ct + case "host-gone": + hg := HostGone{} + err = d.DecodeElement(&hg, &tt) + smf.StreamErrorGroup = &hg + case "host-unknown": + hu := HostUnknown{} + err = d.DecodeElement(&hu, &tt) + smf.StreamErrorGroup = &hu + case "improper-addressing": + ia := ImproperAddressing{} + err = d.DecodeElement(&ia, &tt) + smf.StreamErrorGroup = &ia + case "internal-server-error": + ise := InternalServerError{} + err = d.DecodeElement(&ise, &tt) + smf.StreamErrorGroup = &ise + case "invalid-from": + ifrm := InvalidForm{} + err = d.DecodeElement(&ifrm, &tt) + smf.StreamErrorGroup = &ifrm + case "invalid-id": + id := InvalidId{} + err = d.DecodeElement(&id, &tt) + smf.StreamErrorGroup = &id + case "invalid-namespace": + ins := InvalidNamespace{} + err = d.DecodeElement(&ins, &tt) + smf.StreamErrorGroup = &ins + case "invalid-xml": + ix := InvalidXML{} + err = d.DecodeElement(&ix, &tt) + smf.StreamErrorGroup = &ix + case "not-authorized": + na := NotAuthorized{} + err = d.DecodeElement(&na, &tt) + smf.StreamErrorGroup = &na + case "not-well-formed": + nwf := NotWellFormed{} + err = d.DecodeElement(&nwf, &tt) + smf.StreamErrorGroup = &nwf + case "policy-violation": + pv := PolicyViolation{} + err = d.DecodeElement(&pv, &tt) + smf.StreamErrorGroup = &pv + case "remote-connection-failed": + rcf := RemoteConnectionFailed{} + err = d.DecodeElement(&rcf, &tt) + smf.StreamErrorGroup = &rcf + case "resource-constraint": + rc := ResourceConstraint{} + err = d.DecodeElement(&rc, &tt) + smf.StreamErrorGroup = &rc + case "restricted-xml": + rx := RestrictedXML{} + err = d.DecodeElement(&rx, &tt) + smf.StreamErrorGroup = &rx + case "see-other-host": + soh := SeeOtherHost{} + err = d.DecodeElement(&soh, &tt) + smf.StreamErrorGroup = &soh + case "system-shutdown": + ss := SystemShutdown{} + err = d.DecodeElement(&ss, &tt) + smf.StreamErrorGroup = &ss + case "undefined-condition": + uc := UndefinedCondition{} + err = d.DecodeElement(&uc, &tt) + smf.StreamErrorGroup = &uc + case "unexpected-request": + ur := UnexpectedRequest{} + err = d.DecodeElement(&ur, &tt) + smf.StreamErrorGroup = &ur + case "unsupported-encoding": + ue := UnsupportedEncoding{} + err = d.DecodeElement(&ue, &tt) + smf.StreamErrorGroup = &ue + case "unsupported-stanza-type": + ust := UnsupportedStanzaType{} + err = d.DecodeElement(&ust, &tt) + smf.StreamErrorGroup = &ust + case "unsupported-version": + uv := UnsupportedVersion{} + err = d.DecodeElement(&uv, &tt) + smf.StreamErrorGroup = &uv + case "xml-not-well-formed": + xnwf := XMLNotWellFormed{} + err = d.DecodeElement(&xnwf, &tt) + smf.StreamErrorGroup = &xnwf + default: + return errors.New("error is unknown") + } + if err != nil { + return err + } + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + type smDecoder struct{} var sm smDecoder @@ -78,9 +324,11 @@ func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) { return s.decodeEnabled(p, se) case "resumed": return s.decodeResumed(p, se) + case "resume": + return s.decodeResume(p, se) case "r": return s.decodeRequest(p, se) - case "h": + case "a": return s.decodeAnswer(p, se) case "failed": return s.decodeFailed(p, se) @@ -102,6 +350,11 @@ func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed, return packet, err } +func (smDecoder) decodeResume(p *xml.Decoder, se xml.StartElement) (SMResume, error) { + var packet SMResume + err := p.DecodeElement(&packet, &se) + return packet, err +} func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) { var packet SMRequest err := p.DecodeElement(&packet, &se) diff --git a/stanza/stream_management_test.go b/stanza/stream_management_test.go new file mode 100644 index 0000000..8f51ba0 --- /dev/null +++ b/stanza/stream_management_test.go @@ -0,0 +1,187 @@ +package stanza_test + +import ( + "gosrc.io/xmpp/stanza" + "math/rand" + "reflect" + "testing" + "time" +) + +// TODO : tests to add +// - Pop on nil or empty slice +// - PeekN (normal and too long) + +func TestPushUnack(t *testing.T) { + uaq := initUnAckQueue() + toPush := stanza.UnAckedStz{ + Id: 3, + Stz: ` + + confucius + Qui + Kong + +`, + } + + err := uaq.Push(&toPush) + if err != nil { + t.Fatalf("could not push element to the queue : %v", err) + } + + if len(uaq.Uslice) != 4 { + t.Fatalf("push to the non-acked queue failed") + } + for i := 0; i < 4; i++ { + if uaq.Uslice[i].Id != i+1 { + t.Fatalf("indexes were not updated correctly. Expected %d got %d", i, uaq.Uslice[i].Id) + } + } + + // Check that the queue is a fifo : popped element should not be the one we just pushed. + popped := uaq.Pop() + poppedElt, ok := popped.(*stanza.UnAckedStz) + if !ok { + t.Fatalf("popped element is not a *stanza.UnAckedStz") + } + + if reflect.DeepEqual(*poppedElt, toPush) { + t.Fatalf("pushed element is at the top of the fifo queue when it should be at the bottom") + } + +} + +func TestPeekUnack(t *testing.T) { + uaq := initUnAckQueue() + + expectedPeek := stanza.UnAckedStz{ + Id: 1, + Stz: ` + + Capulet + +`, + } + + if !reflect.DeepEqual(expectedPeek, *uaq.Uslice[0]) { + t.Fatalf("peek failed to return the correct stanza") + } + +} + +func TestPopNUnack(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + randPop := rand.Int31n(int32(initLen)) + + popped := uaq.PopN(int(randPop)) + + if len(uaq.Uslice)+len(popped) != initLen { + t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped)) + } + + for _, elt := range popped { + for _, oldElt := range uaq.Uslice { + if reflect.DeepEqual(elt, oldElt) { + t.Fatalf("pop n operation duplicated some elements") + } + } + } +} + +func TestPopNUnackTooLong(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + + // Have a random number of elements to pop that's greater than the queue size + randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen) + + popped := uaq.PopN(int(randPop)) + + if len(uaq.Uslice)+len(popped) != initLen { + t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped)) + } + + for _, elt := range popped { + for _, oldElt := range uaq.Uslice { + if reflect.DeepEqual(elt, oldElt) { + t.Fatalf("pop n operation duplicated some elements") + } + } + } +} + +func TestPopUnack(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + + popped := uaq.Pop() + + if len(uaq.Uslice)+1 != initLen { + t.Fatalf("total length changed whith pop operation : had %d found %d after pop", initLen, len(uaq.Uslice)+1) + } + for _, oldElt := range uaq.Uslice { + if reflect.DeepEqual(popped, oldElt) { + t.Fatalf("pop n operation duplicated some elements") + } + } + +} + +func initUnAckQueue() stanza.UnAckQueue { + q := []*stanza.UnAckedStz{ + { + Id: 1, + Stz: ` + + Capulet + +`, + }, + {Id: 2, + Stz: ` + +`}, + {Id: 3, + Stz: ` + + + + jabber:iq:search + + + male + + + +`}, + } + + return stanza.UnAckQueue{Uslice: q} + +} + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} diff --git a/stream_manager.go b/stream_manager.go index ebef1fa..da23df1 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -25,7 +25,7 @@ import ( // set callback and trigger reconnection. type StreamClient interface { Connect() error - Resume(state SMState) error + Resume() error Send(packet stanza.Packet) error SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) SendRaw(packet string) error @@ -75,9 +75,7 @@ func (sm *StreamManager) Run() error { } handler := func(e Event) error { - switch e.State { - case StateConnected: - sm.Metrics.setConnectTime() + switch e.State.state { case StateSessionEstablished: sm.Metrics.setLoginTime() case StateDisconnected: @@ -128,7 +126,7 @@ func (sm *StreamManager) resume(state SMState) error { // TODO: Make it possible to define logger to log disconnect and reconnection attempts sm.Metrics = initMetrics() - if err = sm.client.Resume(state); err != nil { + if err = sm.client.Resume(); err != nil { var actualErr ConnError if xerrors.As(err, &actualErr) { if actualErr.Permanent { @@ -152,11 +150,6 @@ func (sm *StreamManager) resume(state SMState) error { type Metrics struct { startTime time.Time - // ConnectTime returns the duration between client initiation of the TCP/IP - // connection to the server and actual TCP/IP session establishment. - // This time includes DNS resolution and can be slightly higher if the DNS - // resolution result was not in cache. - ConnectTime time.Duration // LoginTime returns the between client initiation of the TCP/IP // connection to the server and the return of the login result. // This includes ConnectTime, but also XMPP level protocol negotiation @@ -172,10 +165,6 @@ func initMetrics() *Metrics { } } -func (m *Metrics) setConnectTime() { - m.ConnectTime = time.Since(m.startTime) -} - func (m *Metrics) setLoginTime() { m.LoginTime = time.Since(m.startTime) } diff --git a/tcp_server_mock.go b/tcp_server_mock.go index 55740fa..0410a01 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -36,6 +36,7 @@ const ( testClientRawPort testClientIqPort testClientIqFailPort + testClientPostConnectHook ) // ClientHandler is passed by the test client to provide custom behaviour to diff --git a/xmpp_transport.go b/xmpp_transport.go index 092b95d..800f1b1 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -24,7 +24,8 @@ type XMPPTransport struct { readWriter io.ReadWriter logFile io.Writer isSecure bool - closeChan chan stanza.StreamClosePacket + // Used to close TCP connection when a stream close message is received from the server + closeChan chan stanza.StreamClosePacket } var componentStreamOpen = fmt.Sprintf("", stanza.NSComponent, stanza.NSStream) From e59a86c3807dcb55ef3f77b31f1c672ff52cce4a Mon Sep 17 00:00:00 2001 From: rcorniere Date: Fri, 6 Mar 2020 16:44:01 +0100 Subject: [PATCH 44/54] Refactor tests --- _examples/go.sum | 3 + _examples/xmpp_echo/xmpp_echo.go | 2 +- _examples/xmpp_jukebox/xmpp_jukebox.go | 2 +- _examples/xmpp_oauth2/xmpp_oauth2.go | 2 +- .../xmpp_pubsub_client/xmpp_ps_client.go | 2 +- _examples/xmpp_websocket/xmpp_websocket.go | 2 +- auth.go | 13 +- client.go | 161 +++-- client_internal_test.go | 560 ++++++++++++++++++ client_test.go | 182 +++++- cmd/fluuxmpp/send.go | 2 +- cmd/go.sum | 9 + component.go | 8 +- config.go | 11 + router.go | 37 ++ session.go | 181 ++++-- stanza/fifo_queue.go | 32 + stanza/sasl_auth.go | 6 +- stanza/sasl_auth_test.go | 4 +- stanza/stanza_errors.go | 171 ++++++ stanza/stream_features.go | 4 + stanza/stream_management.go | 261 +++++++- stanza/stream_management_test.go | 187 ++++++ stream_manager.go | 17 +- tcp_server_mock.go | 4 + xmpp_transport.go | 3 +- 26 files changed, 1737 insertions(+), 129 deletions(-) create mode 100644 stanza/fifo_queue.go create mode 100644 stanza/stanza_errors.go create mode 100644 stanza/stream_management_test.go diff --git a/_examples/go.sum b/_examples/go.sum index 286bc95..467aab6 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -99,7 +99,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA= github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go= +github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE= github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -155,6 +157,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go index b6c6766..d7a35c8 100644 --- a/_examples/xmpp_echo/xmpp_echo.go +++ b/_examples/xmpp_echo/xmpp_echo.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go index b8075a2..31f000a 100644 --- a/_examples/xmpp_jukebox/xmpp_jukebox.go +++ b/_examples/xmpp_jukebox/xmpp_jukebox.go @@ -54,7 +54,7 @@ func main() { handleIQ(s, p, player) }) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_oauth2/xmpp_oauth2.go b/_examples/xmpp_oauth2/xmpp_oauth2.go index 89b2639..a993049 100644 --- a/_examples/xmpp_oauth2/xmpp_oauth2.go +++ b/_examples/xmpp_oauth2/xmpp_oauth2.go @@ -28,7 +28,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_pubsub_client/xmpp_ps_client.go b/_examples/xmpp_pubsub_client/xmpp_ps_client.go index b2e9cf6..2308071 100644 --- a/_examples/xmpp_pubsub_client/xmpp_ps_client.go +++ b/_examples/xmpp_pubsub_client/xmpp_ps_client.go @@ -38,7 +38,7 @@ func main() { log.Println("Received a message ! => \n" + string(data)) }) - client, err := xmpp.NewClient(config, router, func(err error) { log.Println(err) }) + client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) }) if err != nil { log.Fatalf("%+v", err) } diff --git a/_examples/xmpp_websocket/xmpp_websocket.go b/_examples/xmpp_websocket/xmpp_websocket.go index c8c0620..3a0c1ba 100644 --- a/_examples/xmpp_websocket/xmpp_websocket.go +++ b/_examples/xmpp_websocket/xmpp_websocket.go @@ -26,7 +26,7 @@ func main() { router := xmpp.NewRouter() router.HandleFunc("message", handleMessage) - client, err := xmpp.NewClient(config, router, errorHandler) + client, err := xmpp.NewClient(&config, router, errorHandler) if err != nil { log.Fatalf("%+v", err) } diff --git a/auth.go b/auth.go index b8d20b9..902371b 100644 --- a/auth.go +++ b/auth.go @@ -60,10 +60,21 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str raw := "\x00" + user + "\x00" + secret enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw))) base64.StdEncoding.Encode(enc, []byte(raw)) - _, err := fmt.Fprintf(socket, "%s", stanza.NSSASL, mech, enc) + + a := stanza.SASLAuth{ + Mechanism: mech, + Value: string(enc), + } + data, err := xml.Marshal(a) if err != nil { return err } + n, err := socket.Write(data) + if err != nil { + return err + } else if n == 0 { + return errors.New("failed to write authSASL nonza to socket : wrote 0 bytes") + } // Next message should be either success or failure. val, err := stanza.NextPacket(decoder) diff --git a/client.go b/client.go index bd40c38..1be1f4d 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net" + "sync" "time" "gosrc.io/xmpp/stanza" @@ -14,15 +15,36 @@ import ( //============================================================================= // EventManager -// ConnState represents the current connection state. +// SyncConnState represents the current connection state. +type SyncConnState struct { + sync.RWMutex + // Current state of the client. Please use the dedicated getter and setter for this field as they are thread safe. + state ConnState +} type ConnState = uint8 +// getState is a thread-safe getter for the current state +func (scs *SyncConnState) getState() ConnState { + var res ConnState + scs.RLock() + res = scs.state + scs.RUnlock() + return res +} + +// setState is a thread-safe setter for the current +func (scs *SyncConnState) setState(cs ConnState) { + scs.Lock() + scs.state = cs + scs.Unlock() +} + // This is a the list of events happening on the connection that the // client can be notified about. const ( InitialPresence = "" StateDisconnected ConnState = iota - StateConnected + StateResuming StateSessionEstablished StateStreamError StatePermanentError @@ -31,7 +53,7 @@ const ( // Event is a structure use to convey event changes related to client state. This // is for example used to notify the client when the client get disconnected. type Event struct { - State ConnState + State SyncConnState Description string StreamError string SMState SMState @@ -44,7 +66,16 @@ type SMState struct { Id string // Inbound stanza count Inbound uint - // TODO Store location for IP affinity + + // IP affinity + preferredReconAddr string + + // Error + StreamErrorGroup stanza.StanzaErrorGroup + + // Track sent stanzas + *stanza.UnAckQueue + // TODO Store max and timestamp, to check if we should retry resumption or not } @@ -53,29 +84,35 @@ type SMState struct { type EventHandler func(Event) error type EventManager struct { - // Store current state - CurrentState ConnState + // Store current state. Please use "getState" and "setState" to access and/or modify this. + CurrentState SyncConnState // Callback used to propagate connection state changes Handler EventHandler } +// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee +// regarding the triggered callback function. func (em *EventManager) updateState(state ConnState) { - em.CurrentState = state + em.CurrentState.setState(state) if em.Handler != nil { em.Handler(Event{State: em.CurrentState}) } } +// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee +// regarding the triggered callback function. func (em *EventManager) disconnected(state SMState) { - em.CurrentState = StateDisconnected + em.CurrentState.setState(StateDisconnected) if em.Handler != nil { em.Handler(Event{State: em.CurrentState, SMState: state}) } } +// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee +// regarding the triggered callback function. func (em *EventManager) streamError(error, desc string) { - em.CurrentState = StateStreamError + em.CurrentState.setState(StateStreamError) if em.Handler != nil { em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc}) } @@ -90,7 +127,7 @@ var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ s // server. type Client struct { // Store user defined options and states - config Config + config *Config // Session gather data that can be accessed by users of this library Session *Session transport Transport @@ -100,6 +137,12 @@ type Client struct { EventManager // Handle errors from client execution ErrorHandler func(error) + + // Post connection hook. This will be executed on first connection + PostFirstConnHook func() error + + // Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198) + PostReconnectHook func() error } /* @@ -107,9 +150,9 @@ Setting up the client / Checking the parameters */ // NewClient generates a new XMPP client, based on Config passed as parameters. -// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the Jid. +// If host is not specified, the DNS SRV should be used to find the host from the domain part of the Jid. // Default the port to 5222. -func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) { +func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) { if config.KeepaliveInterval == 0 { config.KeepaliveInterval = time.Second * 30 } @@ -169,26 +212,45 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e return } -// Connect triggers actual TCP connection, based on previously defined parameters. -// Connect simply triggers resumption, with an empty session state. +// Connect establishes a first time connection to a XMPP server. +// It calls the PostFirstConnHook func (c *Client) Connect() error { - var state SMState - return c.Resume(state) + err := c.connect() + if err != nil { + return err + } + // TODO: Do we always want to send initial presence automatically ? + // Do we need an option to avoid that or do we rely on client to send the presence itself ? + err = c.sendWithWriter(c.transport, []byte(InitialPresence)) + // Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions. + if c.PostFirstConnHook != nil { + err = c.PostFirstConnHook() + if err != nil { + return err + } + } + + // Start the keepalive go routine + keepaliveQuit := make(chan struct{}) + go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) + // Start the receiver go routine + go c.recv(keepaliveQuit) + return err } -// Resume attempts resuming a Stream Managed session, based on the provided stream management -// state. -func (c *Client) Resume(state SMState) error { +// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session +func (c *Client) connect() error { + var state SMState var err error - + // This is the TCP connection streamId, err := c.transport.Connect() if err != nil { return err } - c.updateState(StateConnected) - // Client is ok, we now open XMPP session - if c.Session, err = NewSession(c.transport, c.config, state); err != nil { + // Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding + // depending on state. + if c.Session, err = NewSession(c, state); err != nil { // Try to get the stream close tag from the server. go func() { for { @@ -212,22 +274,26 @@ func (c *Client) Resume(state SMState) error { c.Session.StreamId = streamId c.updateState(StateSessionEstablished) - // Start the keepalive go routine - keepaliveQuit := make(chan struct{}) - go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit) - // Start the receiver go routine - state = c.Session.SMState - go c.recv(state, keepaliveQuit) - - // We're connected and can now receive and send messages. - //fmt.Fprintf(client.conn, "%s%s", "chat", "Online") - // TODO: Do we always want to send initial presence automatically ? - // Do we need an option to avoid that or do we rely on client to send the presence itself ? - err = c.sendWithWriter(c.transport, []byte(InitialPresence)) - return err } +// Resume attempts resuming a Stream Managed session, based on the provided stream management +// state. See XEP-0198 +func (c *Client) Resume() error { + c.EventManager.updateState(StateResuming) + err := c.connect() + if err != nil { + return err + } + // Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrival + // for example. + if c.PostReconnectHook != nil { + err = c.PostReconnectHook() + } + return err +} + +// Disconnect disconnects the client from the server, sending a stream close nonza and closing the TCP connection. func (c *Client) Disconnect() error { if c.transport != nil { return c.transport.Close() @@ -252,6 +318,15 @@ func (c *Client) Send(packet stanza.Packet) error { return errors.New("cannot marshal packet " + err.Error()) } + // Store stanza as non-acked as part of stream management + // See https://xmpp.org/extensions/xep-0198.html#scenarios + if c.config.StreamManagementEnable { + if _, ok := packet.(stanza.SMRequest); !ok { + toStore := stanza.UnAckedStz{Stz: string(data)} + c.Session.SMState.UnAckQueue.Push(&toStore) + } + } + return c.sendWithWriter(c.transport, data) } @@ -284,6 +359,12 @@ func (c *Client) SendRaw(packet string) error { return errors.New("client is not connected") } + // Store stanza as non-acked as part of stream management + // See https://xmpp.org/extensions/xep-0198.html#scenarios + if c.config.StreamManagementEnable { + toStore := stanza.UnAckedStz{Stz: packet} + c.Session.SMState.UnAckQueue.Push(&toStore) + } return c.sendWithWriter(c.transport, []byte(packet)) } @@ -297,13 +378,13 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Go routines // Loop: Receive data from server -func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { +func (c *Client) recv(keepaliveQuit chan<- struct{}) { for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.ErrorHandler(err) close(keepaliveQuit) - c.disconnected(state) + c.disconnected(c.Session.SMState) return } @@ -321,7 +402,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { answer := stanza.SMAnswer{XMLName: xml.Name{ Space: stanza.NSStreamManagement, Local: "a", - }, H: state.Inbound} + }, H: c.Session.SMState.Inbound} err = c.Send(answer) if err != nil { c.ErrorHandler(err) @@ -332,7 +413,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) { c.transport.ReceivedStreamClose() return default: - state.Inbound++ + c.Session.SMState.Inbound++ } // Do normal route processing in a go-routine so we can immediately // start receiving other stanzas. This also allows route handlers to diff --git a/client_internal_test.go b/client_internal_test.go index 6daef09..e517a42 100644 --- a/client_internal_test.go +++ b/client_internal_test.go @@ -2,7 +2,16 @@ package xmpp import ( "bytes" + "encoding/xml" + "fmt" + "gosrc.io/xmpp/stanza" + "strconv" "testing" + "time" +) + +const ( + streamManagementID = "test-stream_management-id" ) func TestClient_Send(t *testing.T) { @@ -17,3 +26,554 @@ func TestClient_Send(t *testing.T) { t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String()) } } + +// Stream management test. +// Connection is established, then the server sends supported features and so on. +// After the bind, client attempts a stream management enablement, and server replies in kind. +func Test_StreamManagement(t *testing.T) { + serverDone := make(chan struct{}) + clientDone := make(chan struct{}) + + client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + serverDone <- struct{}{} + }, testClientStreamManagement, true, true) + go func() { + var state SMState + var err error + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + clientDone <- struct{}{} + }() + + waitForEntity(t, clientDone) + waitForEntity(t, serverDone) + mock.Stop() +} + +// Absence of stream management test. +// Connection is established, then the server sends supported features and so on. +// Client has stream management disabled in its config, and should not ask for it. Server is not set up to reply. +func Test_NoStreamManagement(t *testing.T) { + serverDone := make(chan struct{}) + clientDone := make(chan struct{}) + + // Setup Mock server + client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesNoStreamManagment(t, sc) // Send post auth features + bind(t, sc) + serverDone <- struct{}{} + }, testClientStreamManagement, true, false) + + go func() { + var state SMState + + // Client is ok, we now open XMPP session + var err error + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + clientDone <- struct{}{} + }() + + waitForEntity(t, clientDone) + waitForEntity(t, serverDone) + + mock.Stop() +} + +func Test_StreamManagementNotSupported(t *testing.T) { + serverDone := make(chan struct{}) + clientDone := make(chan struct{}) + + client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesNoStreamManagment(t, sc) // Send post auth features + bind(t, sc) + serverDone <- struct{}{} + }, testClientStreamManagement, true, true) + + go func() { + var state SMState + var err error + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + clientDone <- struct{}{} + }() + + // Wait for client + waitForEntity(t, clientDone) + + // Check if client got a positive stream management response from the server + if client.Session.Features.DoesStreamManagement() { + t.Fatalf("server does not provide stream management") + } + + // Wait for server + waitForEntity(t, serverDone) + mock.Stop() +} + +func Test_StreamManagementNoResume(t *testing.T) { + serverDone := make(chan struct{}) + clientDone := make(chan struct{}) + + client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, false) + serverDone <- struct{}{} + }, testClientStreamManagement, true, true) + + go func() { + var state SMState + var err error + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err != nil { + t.Fatalf("failed to open XMPP session: %s", err) + } + clientDone <- struct{}{} + }() + waitForEntity(t, clientDone) + if IsStreamResumable(client) { + t.Fatalf("server does not support resumption but client says stream is resumable") + } + waitForEntity(t, serverDone) + mock.Stop() +} + +func Test_StreamManagementResume(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + discardPresence(t, sc) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + err = client.Connect() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + statusCorrectChan := make(chan struct{}) + kill := make(chan struct{}) + + transp, ok := client.transport.(*XMPPTransport) + if !ok { + t.Fatalf("problem with client transport ") + } + + transp.conn.Close() + mock.Stop() + + // Check if status is correctly updated because of the disconnect + go checkClientResumeStatus(client, statusCorrectChan, kill) + select { + case <-statusCorrectChan: + // Test passed + case <-time.After(5 * time.Second): + kill <- struct{}{} + t.Fatalf("Client is not in disconnected state while it should be. Timed out") + } + + // Check if the client can have its connection resumed using its state but also its configuration + if !IsStreamResumable(client) { + t.Fatalf("should support resumption") + } + + // Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler + // and they should be different between a first connection and a stream resume since exchanged messages + // are different (See XEP-0198) + mock2 := ServerMock{} + mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + // Reconnect + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + resumeStream(t, sc) + }) + + // Reconnect + err = client.Resume() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + mock2.Stop() +} + +func Test_StreamManagementFail(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, true, true) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var state SMState + _, err = client.transport.Connect() + if err != nil { + return + } + + // Client is ok, we now open XMPP session + if client.Session, err = NewSession(client, state); err == nil { + t.Fatalf("test is supposed to err") + } + if client.Session.SMState.StreamErrorGroup == nil { + t.Fatalf("error was not stored correctly in session state") + } + + mock.Stop() +} + +func Test_SendStanzaQueueWithSM(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + serverDone := make(chan struct{}) + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + + // Ignore the initial presence sent to the server by the client so we can move on to the next packet. + discardPresence(t, sc) + + // Used here to silently discard the IQ sent by the client, in order to later trigger a resend + skipPacket(t, sc) + // Respond to the client ACK request with a number of processed stanzas of 0. This should trigger a resend + // of previously ignored stanza to the server, which this handler element will be expecting. + respondWithAck(t, sc, 0, serverDone) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + err = client.Connect() + + client.SendRaw(` + + +`) + + // Last stanza was discarded silently by the server. Let's ask an ack for it. This should trigger resend as the server + // will respond with an acknowledged number of stanzas of 0. + r := stanza.SMRequest{} + client.Send(r) + + select { + case <-time.After(defaultChannelTimeout): + t.Fatalf("server failed to complete the test in time") + case <-serverDone: + // Test completed successfully + } + + mock.Stop() +} + +//======================================================================== +// Helper functions for tests + +func skipPacket(t *testing.T, sc *ServerConn) { + var p stanza.IQ + se, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read packet: %s", err) + return + } + if err := sc.decoder.DecodeElement(&p, &se); err != nil { + t.Fatalf("cannot decode packet: %s", err) + return + } +} + +func respondWithAck(t *testing.T, sc *ServerConn, h int, serverDone chan struct{}) { + + // Mock server reads the ack request + var p stanza.SMRequest + se, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read packet: %s", err) + return + } + if err := sc.decoder.DecodeElement(&p, &se); err != nil { + t.Fatalf("cannot decode packet: %s", err) + return + } + + // Mock server sends the ack response + a := stanza.SMAnswer{ + H: uint(h), + } + data, err := xml.Marshal(a) + _, err = sc.connection.Write(data) + if err != nil { + t.Fatalf("failed to send response ack") + } + + // Mock server reads the re-sent stanza that was previously discarded intentionally + var p2 stanza.IQ + nse, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read packet: %s", err) + return + } + if err := sc.decoder.DecodeElement(&p2, &nse); err != nil { + t.Fatalf("cannot decode packet: %s", err) + return + } + serverDone <- struct{}{} +} + +func sendFeaturesStreamManagment(t *testing.T, sc *ServerConn) { + // This is a basic server, supporting only 2 features after auth: stream management & session binding + features := ` + + +` + if _, err := fmt.Fprintln(sc.connection, features); err != nil { + t.Fatalf("cannot send stream feature: %s", err) + } +} + +func sendFeaturesNoStreamManagment(t *testing.T, sc *ServerConn) { + // This is a basic server, supporting only 2 features after auth: stream management & session binding + features := ` + +` + if _, err := fmt.Fprintln(sc.connection, features); err != nil { + t.Fatalf("cannot send stream feature: %s", err) + } +} + +// enableStreamManagement is a function for the mock server that can either mock a successful session, or fail depending on +// the value of the "fail" boolean. True means the session should fail. +func enableStreamManagement(t *testing.T, sc *ServerConn, fail bool, resume bool) { + // Decode element into pointer storage + var ed stanza.SMEnable + se, err := stanza.NextStart(sc.decoder) + + if err != nil { + t.Fatalf("cannot read stream management enable: %s", err) + return + } + if err := sc.decoder.DecodeElement(&ed, &se); err != nil { + t.Fatalf("cannot decode stream management enable: %s", err) + return + } + + if fail { + f := stanza.SMFailed{ + H: nil, + StreamErrorGroup: &stanza.UnexpectedRequest{}, + } + data, err := xml.Marshal(f) + if err != nil { + t.Fatalf("failed to marshall error response: %s", err) + } + sc.connection.Write(data) + } else { + e := &stanza.SMEnabled{ + Resume: strconv.FormatBool(resume), + Id: streamManagementID, + } + data, err := xml.Marshal(e) + if err != nil { + t.Fatalf("failed to marshall error response: %s", err) + } + sc.connection.Write(data) + } +} + +func resumeStream(t *testing.T, sc *ServerConn) { + h := uint(0) + response := stanza.SMResumed{ + PrevId: streamManagementID, + H: &h, + } + + data, err := xml.Marshal(response) + if err != nil { + t.Fatalf("failed to marshall stream management enabled response : %s", err) + } + + writtenChan := make(chan struct{}) + + go func() { + sc.connection.Write(data) + writtenChan <- struct{}{} + }() + select { + case <-writtenChan: + // We're done here + return + case <-time.After(defaultTimeout): + t.Fatalf("failed to write enabled nonza to client") + } +} + +func checkClientResumeStatus(client *Client, statusCorrectChan chan struct{}, killChan chan struct{}) { + for { + if client.CurrentState.getState() == StateDisconnected { + statusCorrectChan <- struct{}{} + } + select { + case <-killChan: + return + case <-time.After(time.Millisecond * 10): + // Keep checking status value + } + } +} + +func initSrvCliForResumeTests(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int, StreamManagementEnable, StreamManagementResume bool) (*Client, *ServerMock) { + mock := &ServerMock{} + testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) + + mock.Start(t, testServerAddress, serverHandler) + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testServerAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: StreamManagementEnable, + streamManagementResume: StreamManagementResume} + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { + t.Fatalf("connect create XMPP client: %s", err) + } + + if _, err = client.transport.Connect(); err != nil { + t.Fatalf("XMPP connection failed: %s", err) + } + + return client, mock +} + +func waitForEntity(t *testing.T, entityDone chan struct{}) { + select { + case <-entityDone: + case <-time.After(defaultTimeout): + t.Fatalf("test timed out") + } +} diff --git a/client_test.go b/client_test.go index 00c956a..3cdd77a 100644 --- a/client_test.go +++ b/client_test.go @@ -20,18 +20,20 @@ const ( func TestEventManager(t *testing.T) { mgr := EventManager{} - mgr.updateState(StateConnected) - if mgr.CurrentState != StateConnected { + mgr.updateState(StateResuming) + if mgr.CurrentState.getState() != StateResuming { t.Fatal("CurrentState not updated by updateState()") } mgr.disconnected(SMState{}) - if mgr.CurrentState != StateDisconnected { + + if mgr.CurrentState.getState() != StateDisconnected { t.Fatalf("CurrentState not reset by disconnected()") } mgr.streamError(ErrTLSNotSupported.Error(), "") - if mgr.CurrentState != StateStreamError { + + if mgr.CurrentState.getState() != StateStreamError { t.Fatalf("CurrentState not set by streamError()") } } @@ -53,7 +55,7 @@ func TestClient_Connect(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -84,7 +86,7 @@ func TestClient_NoInsecure(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -117,7 +119,7 @@ func TestClient_FeaturesTracking(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -147,7 +149,7 @@ func TestClient_RFC3921Session(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } @@ -366,7 +368,7 @@ func TestClient_DisconnectStreamManager(t *testing.T) { var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("cannot create XMPP client: %s", err) } @@ -386,6 +388,162 @@ func TestClient_DisconnectStreamManager(t *testing.T) { mock.Stop() } +func Test_ClientPostConnectHook(t *testing.T) { + done := make(chan struct{}) + // Handler for Mock server + h := func(t *testing.T, sc *ServerConn) { + handlerClientConnectSuccess(t, sc) + done <- struct{}{} + } + + hookChan := make(chan struct{}) + mock := &ServerMock{} + testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, testClientPostConnectHook) + + mock.Start(t, testServerAddress, h) + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testServerAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true} + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + // The post connection client hook should just write to a channel that we will read later. + client.PostFirstConnHook = func() error { + go func() { + hookChan <- struct{}{} + }() + return nil + } + // Handle a possible error + errChan := make(chan error) + errorHandler := func(err error) { + errChan <- err + } + client.ErrorHandler = errorHandler + if err = client.Connect(); err != nil { + t.Errorf("XMPP connection failed: %s", err) + } + + // Check if the post connection client hook was correctly called + select { + case err := <-errChan: // If the server sends an error, or there is a connection error + t.Fatal(err.Error()) + case <-time.After(defaultChannelTimeout): // If we timeout + t.Fatal("Failed to call post connection client hook") + case <-hookChan: + // Test succeeded, channel was written to. + } + + select { + case <-done: + mock.Stop() + case <-time.After(defaultChannelTimeout): + t.Fatal("The mock server failed to finish its job !") + } +} + +func Test_ClientPostReconnectHook(t *testing.T) { + hookChan := make(chan struct{}) + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + bind(t, sc) + enableStreamManagement(t, sc, false, true) + }) + + // Test / Check result + config := Config{ + TransportConfiguration: TransportConfiguration{ + Address: testXMPPAddress, + }, + Jid: "test@localhost", + Credential: Password("test"), + Insecure: true, + StreamManagementEnable: true, + streamManagementResume: true} // Enable stream management + + var client *Client + router := NewRouter() + client, err := NewClient(&config, router, clientDefaultErrorHandler) + if err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + client.PostReconnectHook = func() error { + go func() { + hookChan <- struct{}{} + }() + return nil + } + + err = client.Connect() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + transp, ok := client.transport.(*XMPPTransport) + if !ok { + t.Fatalf("problem with client transport ") + } + + transp.conn.Close() + mock.Stop() + + // Check if the client can have its connection resumed using its state but also its configuration + if !IsStreamResumable(client) { + t.Fatalf("should support resumption") + } + + // Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler + // and they should be different between a first connection and a stream resume since exchanged messages + // are different (See XEP-0198) + mock2 := ServerMock{} + mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) { + // Reconnect + checkClientOpenStream(t, sc) + + sendStreamFeatures(t, sc) // Send initial features + readAuth(t, sc.decoder) + sc.connection.Write([]byte("")) + + checkClientOpenStream(t, sc) // Reset stream + sendFeaturesStreamManagment(t, sc) // Send post auth features + resumeStream(t, sc) + }) + + // Reconnect + err = client.Resume() + if err != nil { + t.Fatalf("could not connect client to mock server: %s", err) + } + + select { + case <-time.After(defaultChannelTimeout): // If we timeout + t.Fatal("Failed to call post connection client hook") + case <-hookChan: + // Test succeeded, channel was written to. + } + + mock2.Stop() +} + //============================================================================= // Basic XMPP Server Mock Handlers. @@ -449,7 +607,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) { var token xml.Token token, err := sc.decoder.Token() if err != nil { - t.Errorf("cannot read next token: %s", err) + t.Fatalf("cannot read next token: %s", err) } switch elem := token.(type) { @@ -464,6 +622,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) { } return } + } } @@ -472,7 +631,6 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port) mock.Start(t, testServerAddress, serverHandler) - config := Config{ TransportConfiguration: TransportConfiguration{ Address: testServerAddress, @@ -484,7 +642,7 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo var client *Client var err error router := NewRouter() - if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil { + if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil { t.Errorf("connect create XMPP client: %s", err) } diff --git a/cmd/fluuxmpp/send.go b/cmd/fluuxmpp/send.go index 7e7ed97..27c1a67 100644 --- a/cmd/fluuxmpp/send.go +++ b/cmd/fluuxmpp/send.go @@ -32,7 +32,7 @@ func sendxmpp(cmd *cobra.Command, args []string) { msgText := args[1] var err error - client, err := xmpp.NewClient(xmpp.Config{ + client, err := xmpp.NewClient(&xmpp.Config{ TransportConfiguration: xmpp.TransportConfiguration{ Address: viper.GetString("addr"), }, diff --git a/cmd/go.sum b/cmd/go.sum index c7e00fa..8398605 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -65,6 +65,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -92,6 +93,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -148,10 +150,12 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk= github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -159,6 +163,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= @@ -207,6 +212,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -231,6 +237,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -238,9 +245,11 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= diff --git a/component.go b/component.go index a57b07b..ec0e8df 100644 --- a/component.go +++ b/component.go @@ -60,11 +60,10 @@ func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (* // Connect triggers component connection to XMPP server component port. // TODO: Failed handshake should be a permanent error func (c *Component) Connect() error { - var state SMState - return c.Resume(state) + return c.Resume() } -func (c *Component) Resume(sm SMState) error { +func (c *Component) Resume() error { var err error var streamId string if c.ComponentOptions.TransportConfiguration.Domain == "" { @@ -73,16 +72,13 @@ func (c *Component) Resume(sm SMState) error { c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration) if err != nil { c.updateState(StatePermanentError) - return NewConnError(err, true) } if streamId, err = c.transport.Connect(); err != nil { c.updateState(StatePermanentError) - return NewConnError(err, true) } - c.updateState(StateConnected) // Authentication if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("%s", c.handshake(streamId)))); err != nil { diff --git a/config.go b/config.go index 178da2e..4609a0a 100644 --- a/config.go +++ b/config.go @@ -21,4 +21,15 @@ type Config struct { // Insecure can be set to true to allow to open a session without TLS. If TLS // is supported on the server, we will still try to use it. Insecure bool + + // Activate stream management process during session + StreamManagementEnable bool + // Enable stream management resume capability + streamManagementResume bool +} + +// IsStreamResumable tells if a stream session is resumable by reading the "config" part of a client. +// It checks if stream management is enabled, and if stream resumption was set and accepted by the server. +func IsStreamResumable(c *Client) bool { + return c.config.StreamManagementEnable && c.config.streamManagementResume } diff --git a/router.go b/router.go index f20af5b..d334d36 100644 --- a/router.go +++ b/router.go @@ -42,6 +42,17 @@ 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 stanza.Packet) { + a, isA := p.(stanza.SMAnswer) + if isA { + switch tt := s.(type) { + case *Client: + lastAcked := a.H + SendMissingStz(int(lastAcked), s, tt.Session.SMState.UnAckQueue) + case *Component: + // TODO + default: + } + } iq, isIq := p.(*stanza.IQ) if isIq { r.IQResultRouteLock.RLock() @@ -70,6 +81,32 @@ func (r *Router) route(s Sender, p stanza.Packet) { } } +func SendMissingStz(lastSent int, s Sender, uaq *stanza.UnAckQueue) error { + uaq.RWMutex.Lock() + if len(uaq.Uslice) <= 0 { + uaq.RWMutex.Unlock() + return nil + } + last := uaq.Uslice[len(uaq.Uslice)-1] + if last.Id > lastSent { + // Remove sent stanzas from the queue + uaq.PopN(lastSent - last.Id) + // Re-send non acknowledged stanzas + for _, elt := range uaq.PopN(len(uaq.Uslice)) { + eltStz := elt.(*stanza.UnAckedStz) + err := s.SendRaw(eltStz.Stz) + if err != nil { + return err + } + + } + // Ask for updates on stanzas we just sent to the entity + s.Send(stanza.SMRequest{}) + } + uaq.RWMutex.Unlock() + return nil +} + func iqNotImplemented(s Sender, iq *stanza.IQ) { err := stanza.Err{ XMLName: xml.Name{Local: "error"}, diff --git a/session.go b/session.go index 182e32b..05bdce3 100644 --- a/session.go +++ b/session.go @@ -1,10 +1,11 @@ package xmpp import ( + "encoding/xml" "errors" "fmt" - "gosrc.io/xmpp/stanza" + "strconv" ) type Session struct { @@ -23,44 +24,67 @@ type Session struct { err error } -func NewSession(transport Transport, o Config, state SMState) (*Session, error) { - s := new(Session) - s.transport = transport - s.SMState = state - s.init(o) +func NewSession(c *Client, state SMState) (*Session, error) { + var s *Session + if c.Session == nil { + s = new(Session) + s.transport = c.transport + s.SMState = state + s.init() + } else { + s = c.Session + // We keep information about the previously set session, like the session ID, but we read server provided + // info again in case it changed between session break and resume, such as features. + s.init() + } if s.err != nil { return nil, NewConnError(s.err, true) } - if !transport.IsSecure() { - s.startTlsIfSupported(o) + if !c.transport.IsSecure() { + s.startTlsIfSupported(c.config) } - if !transport.IsSecure() && !o.Insecure { + if !c.transport.IsSecure() && !c.config.Insecure { err := fmt.Errorf("failed to negotiate TLS session : %s", s.err) return nil, NewConnError(err, true) } if s.TlsEnabled { - s.reset(o) + s.reset() } // auth - s.auth(o) - s.reset(o) + s.auth(c.config) + if s.err != nil { + return s, s.err + } + s.reset() + if s.err != nil { + return s, s.err + } // attempt resumption - if s.resume(o) { + if s.resume(c.config) { return s, s.err } // otherwise, bind resource and 'start' XMPP session - s.bind(o) - s.rfc3921Session(o) + s.bind(c.config) + if s.err != nil { + return s, s.err + } + s.rfc3921Session() + if s.err != nil { + return s, s.err + } // Enable stream management if supported - s.EnableStreamManagement(o) + s.EnableStreamManagement(c.config) + if s.err != nil { + return s, s.err + } return s, s.err } @@ -70,19 +94,20 @@ func (s *Session) PacketId() string { return fmt.Sprintf("%x", s.lastPacketId) } -func (s *Session) init(o Config) { - s.Features = s.open(o.parsedJid.Domain) +// init gathers information on the session such as stream features from the server. +func (s *Session) init() { + s.Features = s.extractStreamFeatures() } -func (s *Session) reset(o Config) { +func (s *Session) reset() { if s.StreamId, s.err = s.transport.StartStream(); s.err != nil { return } - s.Features = s.open(o.parsedJid.Domain) + s.Features = s.extractStreamFeatures() } -func (s *Session) open(domain string) (f stanza.StreamFeatures) { +func (s *Session) extractStreamFeatures() (f stanza.StreamFeatures) { // extract stream features if s.err = s.transport.GetDecoder().Decode(&f); s.err != nil { s.err = errors.New("stream open decode features: " + s.err.Error()) @@ -90,7 +115,7 @@ func (s *Session) open(domain string) (f stanza.StreamFeatures) { return } -func (s *Session) startTlsIfSupported(o Config) { +func (s *Session) startTlsIfSupported(o *Config) { if s.err != nil { return } @@ -125,7 +150,7 @@ func (s *Session) startTlsIfSupported(o Config) { } } -func (s *Session) auth(o Config) { +func (s *Session) auth(o *Config) { if s.err != nil { return } @@ -134,7 +159,7 @@ func (s *Session) auth(o Config) { } // Attempt to resume session using stream management -func (s *Session) resume(o Config) bool { +func (s *Session) resume(o *Config) bool { if !s.Features.DoesStreamManagement() { return false } @@ -142,9 +167,16 @@ func (s *Session) resume(o Config) bool { return false } - fmt.Fprintf(s.transport, "", - stanza.NSStreamManagement, s.SMState.Inbound, s.SMState.Id) + rsm := stanza.SMResume{ + PrevId: s.SMState.Id, + H: &s.SMState.Inbound, + } + data, err := xml.Marshal(rsm) + _, err = s.transport.Write(data) + if err != nil { + return false + } var packet stanza.Packet packet, s.err = stanza.NextPacket(s.transport.GetDecoder()) if s.err == nil { @@ -165,20 +197,48 @@ func (s *Session) resume(o Config) bool { return false } -func (s *Session) bind(o Config) { +func (s *Session) bind(o *Config) { if s.err != nil { return } // Send IQ message asking to bind to the local user name. var resource = o.parsedJid.Resource - if resource != "" { - fmt.Fprintf(s.transport, "%s", - s.PacketId(), stanza.NSBind, resource) - } else { - fmt.Fprintf(s.transport, "", s.PacketId(), stanza.NSBind) + iqB, err := stanza.NewIQ(stanza.Attrs{ + Type: stanza.IQTypeSet, + Id: s.PacketId(), + }) + if err != nil { + s.err = err + return } + // Check if we already have a resource name, and include it in the request if so + if resource != "" { + iqB.Payload = &stanza.Bind{ + Resource: resource, + } + } else { + iqB.Payload = &stanza.Bind{} + + } + + // Send the bind request IQ + data, err := xml.Marshal(iqB) + if err != nil { + s.err = err + return + } + n, err := s.transport.Write(data) + if err != nil { + s.err = err + return + } else if n == 0 { + s.err = errors.New("failed to write bind iq stanza to the server : wrote 0 bytes") + return + } + + // Check the server response var iq stanza.IQ if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil { s.err = errors.New("error decoding iq bind result: " + s.err.Error()) @@ -197,7 +257,7 @@ func (s *Session) bind(o Config) { } // After the bind, if the session is not optional (as per old RFC 3921), we send the session open iq. -func (s *Session) rfc3921Session(o Config) { +func (s *Session) rfc3921Session() { if s.err != nil { return } @@ -205,7 +265,29 @@ func (s *Session) rfc3921Session(o Config) { var iq stanza.IQ // We only negotiate session binding if it is mandatory, we skip it when optional. if !s.Features.Session.IsOptional() { - fmt.Fprintf(s.transport, "", s.PacketId(), stanza.NSSession) + se, err := stanza.NewIQ(stanza.Attrs{ + Type: stanza.IQTypeSet, + Id: s.PacketId(), + }) + if err != nil { + s.err = err + return + } + se.Payload = &stanza.StreamSession{} + data, err := xml.Marshal(se) + if err != nil { + s.err = err + return + } + n, err := s.transport.Write(data) + if err != nil { + s.err = err + return + } else if n == 0 { + s.err = errors.New("there was a problem marshaling the session IQ : wrote 0 bytes to server") + return + } + if s.err = s.transport.GetDecoder().Decode(&iq); s.err != nil { s.err = errors.New("expecting iq result after session open: " + s.err.Error()) return @@ -214,28 +296,47 @@ func (s *Session) rfc3921Session(o Config) { } // Enable stream management, with session resumption, if supported. -func (s *Session) EnableStreamManagement(o Config) { +func (s *Session) EnableStreamManagement(o *Config) { if s.err != nil { return } - if !s.Features.DoesStreamManagement() { + if !s.Features.DoesStreamManagement() || !o.StreamManagementEnable { + return + } + q := stanza.NewUnAckQueue() + ebleNonza := stanza.SMEnable{Resume: &o.streamManagementResume} + pktStr, err := xml.Marshal(ebleNonza) + if err != nil { + s.err = err + return + } + _, err = s.transport.Write(pktStr) + if err != nil { + s.err = err return } - - fmt.Fprintf(s.transport, "", stanza.NSStreamManagement) var packet stanza.Packet packet, s.err = stanza.NextPacket(s.transport.GetDecoder()) if s.err == nil { switch p := packet.(type) { case stanza.SMEnabled: - s.SMState = SMState{Id: p.Id} + // Server allows resumption or not using SMEnabled attribute "resume". We must read the server response + // and update config accordingly + b, err := strconv.ParseBool(p.Resume) + if err != nil || !b { + o.StreamManagementEnable = false + } + s.SMState = SMState{Id: p.Id, preferredReconAddr: p.Location} + s.SMState.UnAckQueue = q case stanza.SMFailed: // TODO: Store error in SMState, for later inspection + s.SMState = SMState{StreamErrorGroup: p.StreamErrorGroup} + s.SMState.UnAckQueue = q + s.err = errors.New("failed to establish session : " + s.SMState.StreamErrorGroup.GroupErrorName()) default: s.err = errors.New("unexpected reply to SM enable") } } - return } diff --git a/stanza/fifo_queue.go b/stanza/fifo_queue.go new file mode 100644 index 0000000..dcdab02 --- /dev/null +++ b/stanza/fifo_queue.go @@ -0,0 +1,32 @@ +package stanza + +// FIFO queue for string contents +// Implementations have no guarantee regarding thread safety ! +type FifoQueue interface { + // Pop returns the first inserted element still in queue and delete it from queue + // No guarantee regarding thread safety ! + Pop() Queueable + + // PopN returns the N first inserted elements still in queue and delete them from queue + // No guarantee regarding thread safety ! + PopN(i int) []Queueable + + // Peek returns a copy of the first inserted element in queue without deleting it + // No guarantee regarding thread safety ! + Peek() Queueable + + // Peek returns a copy of the first inserted element in queue without deleting it + // No guarantee regarding thread safety ! + PeekN() []Queueable + // Push adds an element to the queue + // No guarantee regarding thread safety ! + Push(s Queueable) error + + // Empty returns true if queue is empty + // No guarantee regarding thread safety ! + Empty() bool +} + +type Queueable interface { + QueueableName() string +} diff --git a/stanza/sasl_auth.go b/stanza/sasl_auth.go index 9dfe557..2fb660e 100644 --- a/stanza/sasl_auth.go +++ b/stanza/sasl_auth.go @@ -93,8 +93,8 @@ func (b *Bind) GetSet() *ResultSet { // This is the draft defining how to handle the transition: // https://tools.ietf.org/html/draft-cridland-xmpp-session-01 type StreamSession struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` - Optional bool // If element does exist, it mean we are not required to open session + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-session session"` + Optional *struct{} // If element does exist, it mean we are not required to open session // Result sets ResultSet *ResultSet `xml:"set,omitempty"` } @@ -109,7 +109,7 @@ func (s *StreamSession) GetSet() *ResultSet { func (s *StreamSession) IsOptional() bool { if s.XMLName.Local == "session" { - return s.Optional + return s.Optional != nil } // If session element is missing, then we should not use session return true diff --git a/stanza/sasl_auth_test.go b/stanza/sasl_auth_test.go index 3e37453..600035c 100644 --- a/stanza/sasl_auth_test.go +++ b/stanza/sasl_auth_test.go @@ -9,7 +9,7 @@ import ( // Check that we can detect optional session from advertised stream features func TestSessionFeatures(t *testing.T) { - streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: true}} + streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: &struct{}{}}} data, err := xml.Marshal(streamFeatures) if err != nil { @@ -32,7 +32,7 @@ func TestSessionIQ(t *testing.T) { if err != nil { t.Fatalf("failed to create IQ: %v", err) } - iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true} + iq.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: &struct{}{}} data, err := xml.Marshal(iq) if err != nil { diff --git a/stanza/stanza_errors.go b/stanza/stanza_errors.go new file mode 100644 index 0000000..c66ea33 --- /dev/null +++ b/stanza/stanza_errors.go @@ -0,0 +1,171 @@ +package stanza + +import ( + "encoding/xml" +) + +type StanzaErrorGroup interface { + GroupErrorName() string +} + +type BadFormat struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-format"` +} + +func (e *BadFormat) GroupErrorName() string { return "bad-format" } + +type BadNamespacePrefix struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas bad-namespace-prefix"` +} + +func (e *BadNamespacePrefix) GroupErrorName() string { return "bad-namespace-prefix" } + +type Conflict struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas conflict"` +} + +func (e *Conflict) GroupErrorName() string { return "conflict" } + +type ConnectionTimeout struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas connection-timeout"` +} + +func (e *ConnectionTimeout) GroupErrorName() string { return "connection-timeout" } + +type HostGone struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-gone"` +} + +func (e *HostGone) GroupErrorName() string { return "host-gone" } + +type HostUnknown struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas host-unknown"` +} + +func (e *HostUnknown) GroupErrorName() string { return "host-unknown" } + +type ImproperAddressing struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas improper-addressing"` +} + +func (e *ImproperAddressing) GroupErrorName() string { return "improper-addressing" } + +type InternalServerError struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error"` +} + +func (e *InternalServerError) GroupErrorName() string { return "internal-server-error" } + +type InvalidForm struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-from"` +} + +func (e *InvalidForm) GroupErrorName() string { return "invalid-from" } + +type InvalidId struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-id"` +} + +func (e *InvalidId) GroupErrorName() string { return "invalid-id" } + +type InvalidNamespace struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-namespace"` +} + +func (e *InvalidNamespace) GroupErrorName() string { return "invalid-namespace" } + +type InvalidXML struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas invalid-xml"` +} + +func (e *InvalidXML) GroupErrorName() string { return "invalid-xml" } + +type NotAuthorized struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-authorized"` +} + +func (e *NotAuthorized) GroupErrorName() string { return "not-authorized" } + +type NotWellFormed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas not-well-formed"` +} + +func (e *NotWellFormed) GroupErrorName() string { return "not-well-formed" } + +type PolicyViolation struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas policy-violation"` +} + +func (e *PolicyViolation) GroupErrorName() string { return "policy-violation" } + +type RemoteConnectionFailed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas remote-connection-failed"` +} + +func (e *RemoteConnectionFailed) GroupErrorName() string { return "remote-connection-failed" } + +type Reset struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas reset"` +} + +func (e *Reset) GroupErrorName() string { return "reset" } + +type ResourceConstraint struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint"` +} + +func (e *ResourceConstraint) GroupErrorName() string { return "resource-constraint" } + +type RestrictedXML struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas restricted-xml"` +} + +func (e *RestrictedXML) GroupErrorName() string { return "restricted-xml" } + +type SeeOtherHost struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas see-other-host"` +} + +func (e *SeeOtherHost) GroupErrorName() string { return "see-other-host" } + +type SystemShutdown struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas system-shutdown"` +} + +func (e *SystemShutdown) GroupErrorName() string { return "system-shutdown" } + +type UndefinedCondition struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition"` +} + +func (e *UndefinedCondition) GroupErrorName() string { return "undefined-condition" } + +type UnsupportedEncoding struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-encoding"` +} + +type UnexpectedRequest struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request"` +} + +func (e *UnexpectedRequest) GroupErrorName() string { return "unexpected-request" } + +func (e *UnsupportedEncoding) GroupErrorName() string { return "unsupported-encoding" } + +type UnsupportedStanzaType struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-stanza-type"` +} + +func (e *UnsupportedStanzaType) GroupErrorName() string { return "unsupported-stanza-type" } + +type UnsupportedVersion struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas unsupported-version"` +} + +func (e *UnsupportedVersion) GroupErrorName() string { return "unsupported-version" } + +type XMLNotWellFormed struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-stanzas xml-not-well-formed"` +} + +func (e *XMLNotWellFormed) GroupErrorName() string { return "xml-not-well-formed" } diff --git a/stanza/stream_features.go b/stanza/stream_features.go index d5bed5c..d1b6274 100644 --- a/stanza/stream_features.go +++ b/stanza/stream_features.go @@ -118,6 +118,10 @@ type streamManagement struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 sm"` } +func (streamManagement) Name() string { + return "streamManagement" +} + func (sf *StreamFeatures) DoesStreamManagement() (isSupported bool) { if sf.StreamManagement.XMLName.Space+" "+sf.StreamManagement.XMLName.Local == "urn:xmpp:sm:3 sm" { return true diff --git a/stanza/stream_management.go b/stanza/stream_management.go index ddbe9cd..f48d9ca 100644 --- a/stanza/stream_management.go +++ b/stanza/stream_management.go @@ -3,12 +3,19 @@ package stanza import ( "encoding/xml" "errors" + "sync" ) const ( NSStreamManagement = "urn:xmpp:sm:3" ) +type SMEnable struct { + XMLName xml.Name `xml:"urn:xmpp:sm:3 enable"` + Max *uint `xml:"max,attr,omitempty"` + Resume *bool `xml:"resume,attr,omitempty"` +} + // Enabled as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#enable type SMEnabled struct { @@ -23,6 +30,94 @@ func (SMEnabled) Name() string { return "Stream Management: enabled" } +type UnAckQueue struct { + Uslice []*UnAckedStz + sync.RWMutex +} +type UnAckedStz struct { + Id int + Stz string +} + +func NewUnAckQueue() *UnAckQueue { + return &UnAckQueue{ + Uslice: make([]*UnAckedStz, 0, 10), // Capacity is 0 to comply with "Push" implementation (so that no reachable element is nil) + RWMutex: sync.RWMutex{}, + } +} + +func (u *UnAckedStz) QueueableName() string { + return "Un-acknowledged stanza" +} + +func (uaq *UnAckQueue) PeekN(n int) []Queueable { + if n <= 0 { + return []Queueable{} + } + if len(uaq.Uslice) < n { + n = len(uaq.Uslice) + } + + if len(uaq.Uslice) == 0 { + return []Queueable{} + } + var r []Queueable + for i := 0; i < n; i++ { + r = append(r, uaq.Uslice[i]) + } + return r +} + +// No guarantee regarding thread safety ! +func (uaq *UnAckQueue) Pop() Queueable { + r := uaq.Peek() + if r != nil { + uaq.Uslice = uaq.Uslice[1:] + } + return r +} + +// No guarantee regarding thread safety ! +func (uaq *UnAckQueue) PopN(n int) []Queueable { + r := uaq.PeekN(n) + uaq.Uslice = uaq.Uslice[len(r):] + return r +} + +func (uaq *UnAckQueue) Peek() Queueable { + if len(uaq.Uslice) == 0 { + return nil + } + r := uaq.Uslice[0] + return r +} + +func (uaq *UnAckQueue) Push(s Queueable) error { + pushIdx := 1 + if len(uaq.Uslice) != 0 { + pushIdx = uaq.Uslice[len(uaq.Uslice)-1].Id + 1 + } + + sStz, ok := s.(*UnAckedStz) + if !ok { + return errors.New("element in not compatible with this queue. expected an UnAckedStz") + } + + e := UnAckedStz{ + Id: pushIdx, + Stz: sStz.Stz, + } + + uaq.Uslice = append(uaq.Uslice, &e) + + return nil +} + +func (uaq *UnAckQueue) Empty() bool { + r := len(uaq.Uslice) + return r == 0 +} + // Request as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMRequest struct { @@ -37,7 +132,7 @@ func (SMRequest) Name() string { // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMAnswer struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 a"` - H uint `xml:"h,attr,omitempty"` + H uint `xml:"h,attr"` } func (SMAnswer) Name() string { @@ -49,24 +144,175 @@ func (SMAnswer) Name() string { type SMResumed struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 resumed"` PrevId string `xml:"previd,attr,omitempty"` - H uint `xml:"h,attr,omitempty"` + H *uint `xml:"h,attr,omitempty"` } func (SMResumed) Name() string { return "Stream Management: resumed" } +// Resumed as defined in Stream Management spec +// Reference: https://xmpp.org/extensions/xep-0198.html#acking +type SMResume struct { + XMLName xml.Name `xml:"urn:xmpp:sm:3 resume"` + PrevId string `xml:"previd,attr,omitempty"` + H *uint `xml:"h,attr,omitempty"` +} + +func (SMResume) Name() string { + return "Stream Management: resume" +} + // Failed as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMFailed struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 failed"` - // TODO: Handle decoding error cause (need custom parsing). + H *uint `xml:"h,attr,omitempty"` + + StreamErrorGroup StanzaErrorGroup } func (SMFailed) Name() string { return "Stream Management: failed" } +func (smf *SMFailed) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + smf.XMLName = start.Name + + // According to https://xmpp.org/rfcs/rfc3920.html#def we should have no attributes aside from the namespace + // which we don't use internally + + // decode inner elements + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + + case xml.StartElement: + // Decode sub-elements + var err error + switch tt.Name.Local { + case "bad-format": + bf := BadFormat{} + err = d.DecodeElement(&bf, &tt) + smf.StreamErrorGroup = &bf + case "bad-namespace-prefix": + bnp := BadNamespacePrefix{} + err = d.DecodeElement(&bnp, &tt) + smf.StreamErrorGroup = &bnp + case "conflict": + c := Conflict{} + err = d.DecodeElement(&c, &tt) + smf.StreamErrorGroup = &c + case "connection-timeout": + ct := ConnectionTimeout{} + err = d.DecodeElement(&ct, &tt) + smf.StreamErrorGroup = &ct + case "host-gone": + hg := HostGone{} + err = d.DecodeElement(&hg, &tt) + smf.StreamErrorGroup = &hg + case "host-unknown": + hu := HostUnknown{} + err = d.DecodeElement(&hu, &tt) + smf.StreamErrorGroup = &hu + case "improper-addressing": + ia := ImproperAddressing{} + err = d.DecodeElement(&ia, &tt) + smf.StreamErrorGroup = &ia + case "internal-server-error": + ise := InternalServerError{} + err = d.DecodeElement(&ise, &tt) + smf.StreamErrorGroup = &ise + case "invalid-from": + ifrm := InvalidForm{} + err = d.DecodeElement(&ifrm, &tt) + smf.StreamErrorGroup = &ifrm + case "invalid-id": + id := InvalidId{} + err = d.DecodeElement(&id, &tt) + smf.StreamErrorGroup = &id + case "invalid-namespace": + ins := InvalidNamespace{} + err = d.DecodeElement(&ins, &tt) + smf.StreamErrorGroup = &ins + case "invalid-xml": + ix := InvalidXML{} + err = d.DecodeElement(&ix, &tt) + smf.StreamErrorGroup = &ix + case "not-authorized": + na := NotAuthorized{} + err = d.DecodeElement(&na, &tt) + smf.StreamErrorGroup = &na + case "not-well-formed": + nwf := NotWellFormed{} + err = d.DecodeElement(&nwf, &tt) + smf.StreamErrorGroup = &nwf + case "policy-violation": + pv := PolicyViolation{} + err = d.DecodeElement(&pv, &tt) + smf.StreamErrorGroup = &pv + case "remote-connection-failed": + rcf := RemoteConnectionFailed{} + err = d.DecodeElement(&rcf, &tt) + smf.StreamErrorGroup = &rcf + case "resource-constraint": + rc := ResourceConstraint{} + err = d.DecodeElement(&rc, &tt) + smf.StreamErrorGroup = &rc + case "restricted-xml": + rx := RestrictedXML{} + err = d.DecodeElement(&rx, &tt) + smf.StreamErrorGroup = &rx + case "see-other-host": + soh := SeeOtherHost{} + err = d.DecodeElement(&soh, &tt) + smf.StreamErrorGroup = &soh + case "system-shutdown": + ss := SystemShutdown{} + err = d.DecodeElement(&ss, &tt) + smf.StreamErrorGroup = &ss + case "undefined-condition": + uc := UndefinedCondition{} + err = d.DecodeElement(&uc, &tt) + smf.StreamErrorGroup = &uc + case "unexpected-request": + ur := UnexpectedRequest{} + err = d.DecodeElement(&ur, &tt) + smf.StreamErrorGroup = &ur + case "unsupported-encoding": + ue := UnsupportedEncoding{} + err = d.DecodeElement(&ue, &tt) + smf.StreamErrorGroup = &ue + case "unsupported-stanza-type": + ust := UnsupportedStanzaType{} + err = d.DecodeElement(&ust, &tt) + smf.StreamErrorGroup = &ust + case "unsupported-version": + uv := UnsupportedVersion{} + err = d.DecodeElement(&uv, &tt) + smf.StreamErrorGroup = &uv + case "xml-not-well-formed": + xnwf := XMLNotWellFormed{} + err = d.DecodeElement(&xnwf, &tt) + smf.StreamErrorGroup = &xnwf + default: + return errors.New("error is unknown") + } + if err != nil { + return err + } + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + type smDecoder struct{} var sm smDecoder @@ -78,9 +324,11 @@ func (s smDecoder) decode(p *xml.Decoder, se xml.StartElement) (Packet, error) { return s.decodeEnabled(p, se) case "resumed": return s.decodeResumed(p, se) + case "resume": + return s.decodeResume(p, se) case "r": return s.decodeRequest(p, se) - case "h": + case "a": return s.decodeAnswer(p, se) case "failed": return s.decodeFailed(p, se) @@ -102,6 +350,11 @@ func (smDecoder) decodeResumed(p *xml.Decoder, se xml.StartElement) (SMResumed, return packet, err } +func (smDecoder) decodeResume(p *xml.Decoder, se xml.StartElement) (SMResume, error) { + var packet SMResume + err := p.DecodeElement(&packet, &se) + return packet, err +} func (smDecoder) decodeRequest(p *xml.Decoder, se xml.StartElement) (SMRequest, error) { var packet SMRequest err := p.DecodeElement(&packet, &se) diff --git a/stanza/stream_management_test.go b/stanza/stream_management_test.go new file mode 100644 index 0000000..8f51ba0 --- /dev/null +++ b/stanza/stream_management_test.go @@ -0,0 +1,187 @@ +package stanza_test + +import ( + "gosrc.io/xmpp/stanza" + "math/rand" + "reflect" + "testing" + "time" +) + +// TODO : tests to add +// - Pop on nil or empty slice +// - PeekN (normal and too long) + +func TestPushUnack(t *testing.T) { + uaq := initUnAckQueue() + toPush := stanza.UnAckedStz{ + Id: 3, + Stz: ` + + confucius + Qui + Kong + +`, + } + + err := uaq.Push(&toPush) + if err != nil { + t.Fatalf("could not push element to the queue : %v", err) + } + + if len(uaq.Uslice) != 4 { + t.Fatalf("push to the non-acked queue failed") + } + for i := 0; i < 4; i++ { + if uaq.Uslice[i].Id != i+1 { + t.Fatalf("indexes were not updated correctly. Expected %d got %d", i, uaq.Uslice[i].Id) + } + } + + // Check that the queue is a fifo : popped element should not be the one we just pushed. + popped := uaq.Pop() + poppedElt, ok := popped.(*stanza.UnAckedStz) + if !ok { + t.Fatalf("popped element is not a *stanza.UnAckedStz") + } + + if reflect.DeepEqual(*poppedElt, toPush) { + t.Fatalf("pushed element is at the top of the fifo queue when it should be at the bottom") + } + +} + +func TestPeekUnack(t *testing.T) { + uaq := initUnAckQueue() + + expectedPeek := stanza.UnAckedStz{ + Id: 1, + Stz: ` + + Capulet + +`, + } + + if !reflect.DeepEqual(expectedPeek, *uaq.Uslice[0]) { + t.Fatalf("peek failed to return the correct stanza") + } + +} + +func TestPopNUnack(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + randPop := rand.Int31n(int32(initLen)) + + popped := uaq.PopN(int(randPop)) + + if len(uaq.Uslice)+len(popped) != initLen { + t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped)) + } + + for _, elt := range popped { + for _, oldElt := range uaq.Uslice { + if reflect.DeepEqual(elt, oldElt) { + t.Fatalf("pop n operation duplicated some elements") + } + } + } +} + +func TestPopNUnackTooLong(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + + // Have a random number of elements to pop that's greater than the queue size + randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen) + + popped := uaq.PopN(int(randPop)) + + if len(uaq.Uslice)+len(popped) != initLen { + t.Fatalf("total length changed whith pop n operation : had %d found %d after pop", initLen, len(uaq.Uslice)+len(popped)) + } + + for _, elt := range popped { + for _, oldElt := range uaq.Uslice { + if reflect.DeepEqual(elt, oldElt) { + t.Fatalf("pop n operation duplicated some elements") + } + } + } +} + +func TestPopUnack(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + + popped := uaq.Pop() + + if len(uaq.Uslice)+1 != initLen { + t.Fatalf("total length changed whith pop operation : had %d found %d after pop", initLen, len(uaq.Uslice)+1) + } + for _, oldElt := range uaq.Uslice { + if reflect.DeepEqual(popped, oldElt) { + t.Fatalf("pop n operation duplicated some elements") + } + } + +} + +func initUnAckQueue() stanza.UnAckQueue { + q := []*stanza.UnAckedStz{ + { + Id: 1, + Stz: ` + + Capulet + +`, + }, + {Id: 2, + Stz: ` + +`}, + {Id: 3, + Stz: ` + + + + jabber:iq:search + + + male + + + +`}, + } + + return stanza.UnAckQueue{Uslice: q} + +} + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} diff --git a/stream_manager.go b/stream_manager.go index ebef1fa..da23df1 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -25,7 +25,7 @@ import ( // set callback and trigger reconnection. type StreamClient interface { Connect() error - Resume(state SMState) error + Resume() error Send(packet stanza.Packet) error SendIQ(ctx context.Context, iq *stanza.IQ) (chan stanza.IQ, error) SendRaw(packet string) error @@ -75,9 +75,7 @@ func (sm *StreamManager) Run() error { } handler := func(e Event) error { - switch e.State { - case StateConnected: - sm.Metrics.setConnectTime() + switch e.State.state { case StateSessionEstablished: sm.Metrics.setLoginTime() case StateDisconnected: @@ -128,7 +126,7 @@ func (sm *StreamManager) resume(state SMState) error { // TODO: Make it possible to define logger to log disconnect and reconnection attempts sm.Metrics = initMetrics() - if err = sm.client.Resume(state); err != nil { + if err = sm.client.Resume(); err != nil { var actualErr ConnError if xerrors.As(err, &actualErr) { if actualErr.Permanent { @@ -152,11 +150,6 @@ func (sm *StreamManager) resume(state SMState) error { type Metrics struct { startTime time.Time - // ConnectTime returns the duration between client initiation of the TCP/IP - // connection to the server and actual TCP/IP session establishment. - // This time includes DNS resolution and can be slightly higher if the DNS - // resolution result was not in cache. - ConnectTime time.Duration // LoginTime returns the between client initiation of the TCP/IP // connection to the server and the return of the login result. // This includes ConnectTime, but also XMPP level protocol negotiation @@ -172,10 +165,6 @@ func initMetrics() *Metrics { } } -func (m *Metrics) setConnectTime() { - m.ConnectTime = time.Since(m.startTime) -} - func (m *Metrics) setLoginTime() { m.LoginTime = time.Since(m.startTime) } diff --git a/tcp_server_mock.go b/tcp_server_mock.go index 55740fa..d189e3a 100644 --- a/tcp_server_mock.go +++ b/tcp_server_mock.go @@ -36,6 +36,10 @@ const ( testClientRawPort testClientIqPort testClientIqFailPort + testClientPostConnectHook + + // Client internal tests + testClientStreamManagement ) // ClientHandler is passed by the test client to provide custom behaviour to diff --git a/xmpp_transport.go b/xmpp_transport.go index 092b95d..800f1b1 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -24,7 +24,8 @@ type XMPPTransport struct { readWriter io.ReadWriter logFile io.Writer isSecure bool - closeChan chan stanza.StreamClosePacket + // Used to close TCP connection when a stream close message is received from the server + closeChan chan stanza.StreamClosePacket } var componentStreamOpen = fmt.Sprintf("", stanza.NSComponent, stanza.NSStream) From eff622df76c452375ec222733fd9d1f0fb667012 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Mon, 9 Mar 2020 16:50:12 +0100 Subject: [PATCH 45/54] Changelog update --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07598b8..83dd323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Fluux XMPP Changelog +## v0.5.0 + +### Changes + +- Added support for XEP-0198 (Stream management) +- Added message queue : when using "SendX" methods on a client, messages are also stored in a queue. When requesting +acks from the server, sent messages will be discarded, and unsent ones will be sent again. (see https://xmpp.org/extensions/xep-0198.html#acking) +- Added support for stanza_errors (see https://xmpp.org/rfcs/rfc3920.html#def C.2. Stream error namespace and https://xmpp.org/rfcs/rfc6120.html#schemas-streamerror) +- Client state update is now thread safe +- Changed the Config struct to use pointer semantics +- Tests +- Refactoring, including removing some Fprintf statements in favor of Marshal + Write and using structs from the library +instead of strings + ## v0.4.0 ### Changes From 7a932d0504ac4a276e3ca7be383fbb06310d7ebd Mon Sep 17 00:00:00 2001 From: rcorniere Date: Mon, 9 Mar 2020 17:12:32 +0100 Subject: [PATCH 46/54] Added missing tests --- stanza/fifo_queue.go | 10 ++++--- stanza/stream_management.go | 24 ++++++++++++++--- stanza/stream_management_test.go | 45 +++++++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/stanza/fifo_queue.go b/stanza/fifo_queue.go index dcdab02..ca28810 100644 --- a/stanza/fifo_queue.go +++ b/stanza/fifo_queue.go @@ -3,19 +3,21 @@ package stanza // FIFO queue for string contents // Implementations have no guarantee regarding thread safety ! type FifoQueue interface { - // Pop returns the first inserted element still in queue and delete it from queue + // Pop returns the first inserted element still in queue and deletes it from queue. If queue is empty, returns nil // No guarantee regarding thread safety ! Pop() Queueable - // PopN returns the N first inserted elements still in queue and delete them from queue + // PopN returns the N first inserted elements still in queue and deletes them from queue. If queue is empty or i<=0, returns nil + // If number to pop is greater than queue length, returns all queue elements // No guarantee regarding thread safety ! PopN(i int) []Queueable - // Peek returns a copy of the first inserted element in queue without deleting it + // Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty, returns nil // No guarantee regarding thread safety ! Peek() Queueable - // Peek returns a copy of the first inserted element in queue without deleting it + // Peek returns a copy of the first inserted element in queue without deleting it. If queue is empty or i<=0, returns nil. + // If number to peek is greater than queue length, returns all queue elements // No guarantee regarding thread safety ! PeekN() []Queueable // Push adds an element to the queue diff --git a/stanza/stream_management.go b/stanza/stream_management.go index f48d9ca..a2a4f0b 100644 --- a/stanza/stream_management.go +++ b/stanza/stream_management.go @@ -51,15 +51,18 @@ func (u *UnAckedStz) QueueableName() string { } func (uaq *UnAckQueue) PeekN(n int) []Queueable { + if uaq == nil { + return nil + } if n <= 0 { - return []Queueable{} + return nil } if len(uaq.Uslice) < n { n = len(uaq.Uslice) } if len(uaq.Uslice) == 0 { - return []Queueable{} + return nil } var r []Queueable for i := 0; i < n; i++ { @@ -70,6 +73,9 @@ func (uaq *UnAckQueue) PeekN(n int) []Queueable { // No guarantee regarding thread safety ! func (uaq *UnAckQueue) Pop() Queueable { + if uaq == nil { + return nil + } r := uaq.Peek() if r != nil { uaq.Uslice = uaq.Uslice[1:] @@ -79,12 +85,18 @@ func (uaq *UnAckQueue) Pop() Queueable { // No guarantee regarding thread safety ! func (uaq *UnAckQueue) PopN(n int) []Queueable { + if uaq == nil { + return nil + } r := uaq.PeekN(n) uaq.Uslice = uaq.Uslice[len(r):] return r } func (uaq *UnAckQueue) Peek() Queueable { + if uaq == nil { + return nil + } if len(uaq.Uslice) == 0 { return nil } @@ -93,6 +105,9 @@ func (uaq *UnAckQueue) Peek() Queueable { } func (uaq *UnAckQueue) Push(s Queueable) error { + if uaq == nil { + return nil + } pushIdx := 1 if len(uaq.Uslice) != 0 { pushIdx = uaq.Uslice[len(uaq.Uslice)-1].Id + 1 @@ -114,6 +129,9 @@ func (uaq *UnAckQueue) Push(s Queueable) error { } func (uaq *UnAckQueue) Empty() bool { + if uaq == nil { + return true + } r := len(uaq.Uslice) return r == 0 } @@ -151,7 +169,7 @@ func (SMResumed) Name() string { return "Stream Management: resumed" } -// Resumed as defined in Stream Management spec +// Resume as defined in Stream Management spec // Reference: https://xmpp.org/extensions/xep-0198.html#acking type SMResume struct { XMLName xml.Name `xml:"urn:xmpp:sm:3 resume"` diff --git a/stanza/stream_management_test.go b/stanza/stream_management_test.go index 8f51ba0..1b3443e 100644 --- a/stanza/stream_management_test.go +++ b/stanza/stream_management_test.go @@ -8,9 +8,13 @@ import ( "time" ) -// TODO : tests to add -// - Pop on nil or empty slice -// - PeekN (normal and too long) +func TestPopEmptyQueue(t *testing.T) { + var uaq stanza.UnAckQueue + popped := uaq.Pop() + if popped != nil { + t.Fatalf("queue is empty but something was popped !") + } +} func TestPushUnack(t *testing.T) { uaq := initUnAckQueue() @@ -78,6 +82,41 @@ func TestPeekUnack(t *testing.T) { } +func TestPeekNUnack(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + randPop := rand.Int31n(int32(initLen)) + + peeked := uaq.PeekN(int(randPop)) + + if len(uaq.Uslice) != initLen { + t.Fatalf("queue length changed whith peek n operation : had %d found %d after peek", initLen, len(uaq.Uslice)) + } + + if len(peeked) != int(randPop) { + t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", randPop, len(peeked)) + } +} + +func TestPeekNUnackTooLong(t *testing.T) { + uaq := initUnAckQueue() + initLen := len(uaq.Uslice) + + // Have a random number of elements to peek that's greater than the queue size + randPop := rand.Int31n(int32(initLen)) + 1 + int32(initLen) + + peeked := uaq.PeekN(int(randPop)) + + if len(uaq.Uslice) != initLen { + t.Fatalf("total length changed whith peek n operation : had %d found %d after pop", initLen, len(uaq.Uslice)) + } + + if len(peeked) != initLen { + t.Fatalf("did not peek the correct number of element from queue. Expected %d got %d", initLen, len(peeked)) + } + +} + func TestPopNUnack(t *testing.T) { uaq := initUnAckQueue() initLen := len(uaq.Uslice) From 477a2b114c513f9d1edff32ee9ae5e3f6760ff58 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Mon, 9 Mar 2020 17:19:29 +0100 Subject: [PATCH 47/54] Changelog and doc --- CHANGELOG.md | 2 ++ router.go | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83dd323..41f58ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - Added message queue : when using "SendX" methods on a client, messages are also stored in a queue. When requesting acks from the server, sent messages will be discarded, and unsent ones will be sent again. (see https://xmpp.org/extensions/xep-0198.html#acking) - Added support for stanza_errors (see https://xmpp.org/rfcs/rfc3920.html#def C.2. Stream error namespace and https://xmpp.org/rfcs/rfc6120.html#schemas-streamerror) +- Added separate hooks for connection and reconnection on the client. One can now specify different actions to get triggered on client connect +and reconnect, at client init time. - Client state update is now thread safe - Changed the Config struct to use pointer semantics - Tests diff --git a/router.go b/router.go index d334d36..7bba8b9 100644 --- a/router.go +++ b/router.go @@ -81,6 +81,7 @@ func (r *Router) route(s Sender, p stanza.Packet) { } } +// SendMissingStz sends all stanzas that did not reach the server, according to the response to an ack request (see XEP-0198, acks) func SendMissingStz(lastSent int, s Sender, uaq *stanza.UnAckQueue) error { uaq.RWMutex.Lock() if len(uaq.Uslice) <= 0 { @@ -100,7 +101,7 @@ func SendMissingStz(lastSent int, s Sender, uaq *stanza.UnAckQueue) error { } } - // Ask for updates on stanzas we just sent to the entity + // Ask for updates on stanzas we just sent to the entity. Not sure I should leave this. Maybe let users call ack again by themselves ? s.Send(stanza.SMRequest{}) } uaq.RWMutex.Unlock() From 7850d07d37184a0dfe921c4703a1b85d065ecb16 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 10 Mar 2020 16:31:27 +0100 Subject: [PATCH 48/54] Renamed Hooks --- client.go | 14 +++++++------- client_test.go | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index 1be1f4d..446dcbe 100644 --- a/client.go +++ b/client.go @@ -139,10 +139,10 @@ type Client struct { ErrorHandler func(error) // Post connection hook. This will be executed on first connection - PostFirstConnHook func() error + PostConnectHook func() error // Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198) - PostReconnectHook func() error + PostResumeHook func() error } /* @@ -213,7 +213,7 @@ func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, } // Connect establishes a first time connection to a XMPP server. -// It calls the PostFirstConnHook +// It calls the PostConnectHook func (c *Client) Connect() error { err := c.connect() if err != nil { @@ -223,8 +223,8 @@ func (c *Client) Connect() error { // Do we need an option to avoid that or do we rely on client to send the presence itself ? err = c.sendWithWriter(c.transport, []byte(InitialPresence)) // Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions. - if c.PostFirstConnHook != nil { - err = c.PostFirstConnHook() + if c.PostConnectHook != nil { + err = c.PostConnectHook() if err != nil { return err } @@ -287,8 +287,8 @@ func (c *Client) Resume() error { } // Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrival // for example. - if c.PostReconnectHook != nil { - err = c.PostReconnectHook() + if c.PostResumeHook != nil { + err = c.PostResumeHook() } return err } diff --git a/client_test.go b/client_test.go index 3cdd77a..4f30c0e 100644 --- a/client_test.go +++ b/client_test.go @@ -417,7 +417,7 @@ func Test_ClientPostConnectHook(t *testing.T) { } // The post connection client hook should just write to a channel that we will read later. - client.PostFirstConnHook = func() error { + client.PostConnectHook = func() error { go func() { hookChan <- struct{}{} }() @@ -486,7 +486,7 @@ func Test_ClientPostReconnectHook(t *testing.T) { t.Errorf("connect create XMPP client: %s", err) } - client.PostReconnectHook = func() error { + client.PostResumeHook = func() error { go func() { hookChan <- struct{}{} }() From f1331dcebc7f45981962650aba74f1d63978ce31 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 10 Mar 2020 17:14:26 +0100 Subject: [PATCH 49/54] Reset isSecure in StartTLS() --- xmpp_transport.go | 1 + 1 file changed, 1 insertion(+) diff --git a/xmpp_transport.go b/xmpp_transport.go index 800f1b1..9e7cb21 100644 --- a/xmpp_transport.go +++ b/xmpp_transport.go @@ -93,6 +93,7 @@ func (t *XMPPTransport) StartTLS() error { return err } + t.isSecure = false t.conn = tlsConn t.readWriter = newStreamLogger(tlsConn, t.logFile) t.decoder = xml.NewDecoder(bufio.NewReaderSize(t.readWriter, maxPacketSize)) From 6e84084bb35a416a5ef1942511a0f6110eb8087f Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 10 Mar 2020 17:32:03 +0100 Subject: [PATCH 50/54] Close keepalive when recv() ends in client.go --- client.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 446dcbe..0c691a4 100644 --- a/client.go +++ b/client.go @@ -379,11 +379,13 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Loop: Receive data from server func (c *Client) recv(keepaliveQuit chan<- struct{}) { + defer func() { + close(keepaliveQuit) + }() for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { c.ErrorHandler(err) - close(keepaliveQuit) c.disconnected(c.Session.SMState) return } @@ -392,7 +394,6 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) { switch packet := val.(type) { case stanza.StreamError: c.router.route(c, val) - close(keepaliveQuit) c.streamError(packet.Error.Local, packet.Text) c.ErrorHandler(errors.New("stream error: " + packet.Error.Local)) // We don't return here, because we want to wait for the stream close tag from the server, or timeout. From d4960afc979b04ac60686a650488f674720010d3 Mon Sep 17 00:00:00 2001 From: rcorniere Date: Tue, 10 Mar 2020 18:01:47 +0100 Subject: [PATCH 51/54] Close keepalive when recv() ends in client.go --- client.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 0c691a4..7613af4 100644 --- a/client.go +++ b/client.go @@ -379,9 +379,8 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error { // Loop: Receive data from server func (c *Client) recv(keepaliveQuit chan<- struct{}) { - defer func() { - close(keepaliveQuit) - }() + defer close(keepaliveQuit) + for { val, err := stanza.NextPacket(c.transport.GetDecoder()) if err != nil { From 0a4acd12c34b0048ff7554b7c56ddce975eb9286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Mon, 16 Mar 2020 16:20:54 +0100 Subject: [PATCH 52/54] Fix issue #160 --- client.go | 4 ++-- stream_manager.go | 25 +++++++++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index 7613af4..9969472 100644 --- a/client.go +++ b/client.go @@ -42,12 +42,12 @@ func (scs *SyncConnState) setState(cs ConnState) { // This is a the list of events happening on the connection that the // client can be notified about. const ( - InitialPresence = "" StateDisconnected ConnState = iota StateResuming StateSessionEstablished StateStreamError StatePermanentError + InitialPresence = "" ) // Event is a structure use to convey event changes related to client state. This @@ -285,7 +285,7 @@ func (c *Client) Resume() error { if err != nil { return err } - // Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrival + // Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrieval // for example. if c.PostResumeHook != nil { err = c.PostResumeHook() diff --git a/stream_manager.go b/stream_manager.go index da23df1..7bfb42c 100644 --- a/stream_manager.go +++ b/stream_manager.go @@ -80,13 +80,13 @@ func (sm *StreamManager) Run() error { sm.Metrics.setLoginTime() case StateDisconnected: // Reconnect on disconnection - return sm.resume(e.SMState) + return sm.resume() case StateStreamError: sm.client.Disconnect() // Only try reconnecting if we have not been kicked by another session to avoid connection loop. // TODO: Make this conflict exception a permanent error if e.StreamError != "conflict" { - return sm.connect() + return sm.resume() } case StatePermanentError: // Do not attempt to reconnect @@ -113,19 +113,32 @@ func (sm *StreamManager) Stop() { } func (sm *StreamManager) connect() error { - var state SMState - return sm.resume(state) + if sm.client != nil { + if c, ok := sm.client.(*Client); ok { + if c.CurrentState.getState() == StateDisconnected { + sm.Metrics = initMetrics() + err := c.Connect() + if err != nil { + return err + } + if sm.PostConnect != nil { + sm.PostConnect(sm.client) + } + return nil + } + } + } + return errors.New("client is not disconnected") } // resume manages the reconnection loop and apply the define backoff to avoid overloading the server. -func (sm *StreamManager) resume(state SMState) error { +func (sm *StreamManager) resume() error { var backoff backoff // TODO: Group backoff calculation features with connection manager? for { var err error // TODO: Make it possible to define logger to log disconnect and reconnection attempts sm.Metrics = initMetrics() - if err = sm.client.Resume(); err != nil { var actualErr ConnError if xerrors.As(err, &actualErr) { From 6a3ee5b0a5311fd80f0150ee13dd867a126f0f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Thu, 9 Apr 2020 10:02:11 +0200 Subject: [PATCH 53/54] Support for XEP-0334 --- go.sum | 2 +- stanza/msg_hint.go | 36 +++++++++++++++++++++ stanza/msg_hint_test.go | 72 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 stanza/msg_hint.go create mode 100644 stanza/msg_hint_test.go diff --git a/go.sum b/go.sum index 38fd55b..e51d671 100644 --- a/go.sum +++ b/go.sum @@ -201,7 +201,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/stanza/msg_hint.go b/stanza/msg_hint.go new file mode 100644 index 0000000..205d0ef --- /dev/null +++ b/stanza/msg_hint.go @@ -0,0 +1,36 @@ +package stanza + +import "encoding/xml" + +/* +Support for: +- XEP-0334: Message Processing Hints: https://xmpp.org/extensions/xep-0334.html +Pointers should be used to keep consistent with unmarshal. Eg : +msg.Extensions = append(msg.Extensions, &stanza.HintNoCopy{}, &stanza.HintStore{}) +*/ + +type HintNoPermanentStore struct { + MsgExtension + XMLName xml.Name `xml:"urn:xmpp:hints no-permanent-store"` +} + +type HintNoStore struct { + MsgExtension + XMLName xml.Name `xml:"urn:xmpp:hints no-store"` +} + +type HintNoCopy struct { + MsgExtension + XMLName xml.Name `xml:"urn:xmpp:hints no-copy"` +} +type HintStore struct { + MsgExtension + XMLName xml.Name `xml:"urn:xmpp:hints store"` +} + +func init() { + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-permanent-store"}, HintNoPermanentStore{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-store"}, HintNoStore{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "no-copy"}, HintNoCopy{}) + TypeRegistry.MapExtension(PKTMessage, xml.Name{Space: "urn:xmpp:hints", Local: "store"}, HintStore{}) +} diff --git a/stanza/msg_hint_test.go b/stanza/msg_hint_test.go new file mode 100644 index 0000000..b86d2f9 --- /dev/null +++ b/stanza/msg_hint_test.go @@ -0,0 +1,72 @@ +package stanza_test + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "reflect" + "strings" + "testing" +) + +const msg_const = ` + + V unir avtugf pybnx gb uvqr zr sebz gurve fvtug + + + + +` + +func TestSerializationHint(t *testing.T) { + msg := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"}) + msg.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug" + msg.Extensions = append(msg.Extensions, stanza.HintNoCopy{}, stanza.HintNoPermanentStore{}, stanza.HintNoStore{}, stanza.HintStore{}) + data, _ := xml.Marshal(msg) + if strings.ReplaceAll(strings.Join(strings.Fields(msg_const), ""), "\n", "") != strings.Join(strings.Fields(string(data)), "") { + t.Fatalf("marshalled message does not match expected message") + } +} + +func TestUnmarshalHints(t *testing.T) { + // Init message as in the const value + msgConst := stanza.NewMessage(stanza.Attrs{To: "juliet@capulet.lit/laptop", From: "romeo@montague.lit/laptop"}) + msgConst.Body = "V unir avtugf pybnx gb uvqr zr sebz gurve fvtug" + msgConst.Extensions = append(msgConst.Extensions, &stanza.HintNoCopy{}, &stanza.HintNoPermanentStore{}, &stanza.HintNoStore{}, &stanza.HintStore{}) + + // Compare message with the const value + msg := stanza.Message{} + err := xml.Unmarshal([]byte(msg_const), &msg) + if err != nil { + t.Fatal(err) + } + + if msgConst.XMLName.Local != msg.XMLName.Local { + t.Fatalf("message tags do not match. Expected: %s, Actual: %s", msgConst.XMLName.Local, msg.XMLName.Local) + } + if msgConst.Body != msg.Body { + t.Fatalf("message bodies do not match. Expected: %s, Actual: %s", msgConst.Body, msg.Body) + } + + if !reflect.DeepEqual(msgConst.Attrs, msg.Attrs) { + t.Fatalf("attributes do not match") + } + + if !reflect.DeepEqual(msgConst.Error, msg.Error) { + t.Fatalf("attributes do not match") + } + var found bool + for _, ext := range msgConst.Extensions { + for _, strExt := range msg.Extensions { + if reflect.TypeOf(ext) == reflect.TypeOf(strExt) { + found = true + break + } + } + if !found { + t.Fatalf("extensions do not match") + } + found = false + } +} From 17d561f8296b18855e8f8846ca0093e2957e9343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CORNIERE=20R=C3=A9mi?= Date: Wed, 29 Apr 2020 10:13:31 +0200 Subject: [PATCH 54/54] Support for XEP-0082. Parsing of times with an offset does not work for now (should it ?) --- stanza/datetime_profiles.go | 70 +++++++++++ stanza/datetime_profiles_test.go | 191 +++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 stanza/datetime_profiles.go create mode 100644 stanza/datetime_profiles_test.go diff --git a/stanza/datetime_profiles.go b/stanza/datetime_profiles.go new file mode 100644 index 0000000..7a2ef74 --- /dev/null +++ b/stanza/datetime_profiles.go @@ -0,0 +1,70 @@ +package stanza + +import ( + "errors" + "strings" + "time" +) + +// Helper structures and functions to manage dates and timestamps as defined in +// XEP-0082: XMPP Date and Time Profiles (https://xmpp.org/extensions/xep-0082.html) + +const dateLayoutXEP0082 = "2006-01-02" +const timeLayoutXEP0082 = "15:04:05+00:00" + +var InvalidDateInput = errors.New("could not parse date. Input might not be in a supported format") +var InvalidDateOutput = errors.New("could not format date as desired") + +type JabberDate struct { + value time.Time +} + +func (d JabberDate) DateToString() string { + return d.value.Format(dateLayoutXEP0082) +} + +func (d JabberDate) DateTimeToString(nanos bool) string { + if nanos { + return d.value.Format(time.RFC3339Nano) + } + return d.value.Format(time.RFC3339) +} + +func (d JabberDate) TimeToString(nanos bool) (string, error) { + if nanos { + spl := strings.Split(d.value.Format(time.RFC3339Nano), "T") + if len(spl) != 2 { + return "", InvalidDateOutput + } + return spl[1], nil + } + spl := strings.Split(d.value.Format(time.RFC3339), "T") + if len(spl) != 2 { + return "", InvalidDateOutput + } + return spl[1], nil +} + +func NewJabberDateFromString(strDate string) (JabberDate, error) { + t, err := time.Parse(time.RFC3339, strDate) + if err == nil { + return JabberDate{value: t}, nil + } + + t, err = time.Parse(time.RFC3339Nano, strDate) + if err == nil { + return JabberDate{value: t}, nil + } + + t, err = time.Parse(dateLayoutXEP0082, strDate) + if err == nil { + return JabberDate{value: t}, nil + } + + t, err = time.Parse(timeLayoutXEP0082, strDate) + if err == nil { + return JabberDate{value: t}, nil + } + + return JabberDate{}, InvalidDateInput +} diff --git a/stanza/datetime_profiles_test.go b/stanza/datetime_profiles_test.go new file mode 100644 index 0000000..98aa4cd --- /dev/null +++ b/stanza/datetime_profiles_test.go @@ -0,0 +1,191 @@ +package stanza + +import ( + "testing" + "time" +) + +func TestDateToString(t *testing.T) { + t1 := JabberDate{value: time.Now()} + t2 := JabberDate{value: time.Now().Add(24 * time.Hour)} + + t1Str := t1.DateToString() + t2Str := t2.DateToString() + + if t1Str == t2Str { + t.Fatalf("time representations should not be identical") + } +} + +func TestDateToStringOracle(t *testing.T) { + expected := "2009-11-10" + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatalf(err.Error()) + } + t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)} + + t1Str := t1.DateToString() + if t1Str != expected { + t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str) + } +} + +func TestDateTimeToString(t *testing.T) { + t1 := JabberDate{value: time.Now()} + t2 := JabberDate{value: time.Now().Add(10 * time.Second)} + + t1Str := t1.DateTimeToString(false) + t2Str := t2.DateTimeToString(false) + + if t1Str == t2Str { + t.Fatalf("time representations should not be identical") + } +} + +func TestDateTimeToStringOracle(t *testing.T) { + expected := "2009-11-10T23:03:22+08:00" + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatalf(err.Error()) + } + t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)} + + t1Str := t1.DateTimeToString(false) + if t1Str != expected { + t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str) + } +} + +func TestDateTimeToStringNanos(t *testing.T) { + t1 := JabberDate{value: time.Now()} + time.After(10 * time.Millisecond) + t2 := JabberDate{value: time.Now()} + + t1Str := t1.DateTimeToString(true) + t2Str := t2.DateTimeToString(true) + + if t1Str == t2Str { + t.Fatalf("time representations should not be identical") + } +} + +func TestDateTimeToStringNanosOracle(t *testing.T) { + expected := "2009-11-10T23:03:22.000000089+08:00" + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatalf(err.Error()) + } + t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)} + + t1Str := t1.DateTimeToString(true) + if t1Str != expected { + t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str) + } +} + +func TestTimeToString(t *testing.T) { + t1 := JabberDate{value: time.Now()} + t2 := JabberDate{value: time.Now().Add(10 * time.Second)} + + t1Str, err := t1.TimeToString(false) + if err != nil { + t.Fatalf(err.Error()) + } + t2Str, err := t2.TimeToString(false) + if err != nil { + t.Fatalf(err.Error()) + } + + if t1Str == t2Str { + t.Fatalf("time representations should not be identical") + } +} + +func TestTimeToStringOracle(t *testing.T) { + expected := "23:03:22+08:00" + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatalf(err.Error()) + } + t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)} + + t1Str, err := t1.TimeToString(false) + if err != nil { + t.Fatalf(err.Error()) + } + + if t1Str != expected { + t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str) + } +} + +func TestTimeToStringNanos(t *testing.T) { + t1 := JabberDate{value: time.Now()} + time.After(10 * time.Millisecond) + t2 := JabberDate{value: time.Now()} + + t1Str, err := t1.TimeToString(true) + if err != nil { + t.Fatalf(err.Error()) + } + t2Str, err := t2.TimeToString(true) + if err != nil { + t.Fatalf(err.Error()) + } + + if t1Str == t2Str { + t.Fatalf("time representations should not be identical") + } +} +func TestTimeToStringNanosOracle(t *testing.T) { + expected := "23:03:22.000000089+08:00" + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + t.Fatalf(err.Error()) + } + t1 := JabberDate{value: time.Date(2009, time.November, 10, 23, 3, 22, 89, loc)} + + t1Str, err := t1.TimeToString(true) + if err != nil { + t.Fatalf(err.Error()) + } + + if t1Str != expected { + t.Fatalf("time is different than expected. Expected: %s, Actual: %s", expected, t1Str) + } +} + +func TestJabberDateParsing(t *testing.T) { + date := "2009-11-10" + _, err := NewJabberDateFromString(date) + if err != nil { + t.Fatalf(err.Error()) + } + + dateTime := "2009-11-10T23:03:22+08:00" + _, err = NewJabberDateFromString(dateTime) + if err != nil { + t.Fatalf(err.Error()) + } + + dateTimeNanos := "2009-11-10T23:03:22.000000089+08:00" + _, err = NewJabberDateFromString(dateTimeNanos) + if err != nil { + t.Fatalf(err.Error()) + } + + // TODO : fix these. Parsing a time with an offset doesn't work + //time := "23:03:22+08:00" + //_, err = NewJabberDateFromString(time) + //if err != nil { + // t.Fatalf(err.Error()) + //} + + //timeNanos := "23:03:22.000000089+08:00" + //_, err = NewJabberDateFromString(timeNanos) + //if err != nil { + // t.Fatalf(err.Error()) + //} + +}