diff --git a/client_test.go b/client_test.go index d55a1d6..c5e575d 100644 --- a/client_test.go +++ b/client_test.go @@ -86,7 +86,28 @@ func TestClient_FeaturesTracking(t *testing.T) { } mock.Stop() +} +func TestClient_RFC3921Session(t *testing.T) { + // Setup Mock server + mock := ServerMock{} + mock.Start(t, testXMPPAddress, handlerConnectWithSession) + + // Test / Check result + config := Config{Address: testXMPPAddress, Jid: "test@localhost", Password: "test", Insecure: true} + + var client *Client + var err error + router := NewRouter() + if client, err = NewClient(config, router); err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + if err = client.Connect(); err != nil { + t.Errorf("XMPP connection failed: %s", err) + } + + mock.Stop() } //============================================================================= @@ -94,6 +115,7 @@ func TestClient_FeaturesTracking(t *testing.T) { const serverStreamOpen = "" +// Test connection with a basic straightforward workflow func handlerConnectSuccess(t *testing.T, c net.Conn) { decoder := xml.NewDecoder(c) checkOpenStream(t, c, decoder) @@ -114,6 +136,21 @@ func handlerAbortTLS(t *testing.T, c net.Conn) { sendStreamFeatures(t, c, decoder) // Send initial features } +// Test connection with mandatory session (RFC-3921) +func handlerConnectWithSession(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + checkOpenStream(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 + bind(t, c, decoder) + session(t, c, decoder) +} + func checkOpenStream(t *testing.T, c net.Conn, decoder *xml.Decoder) { c.SetDeadline(time.Now().Add(defaultTimeout)) defer c.SetDeadline(time.Time{}) @@ -176,7 +213,7 @@ func readAuth(t *testing.T, decoder *xml.Decoder) string { } func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { - // This is a basic server, supporting only 1 stream feature after auth: session binding + // This is a basic server, supporting only 1 stream feature after auth: resource binding features := ` ` @@ -185,6 +222,17 @@ func sendBindFeature(t *testing.T, c net.Conn, _ *xml.Decoder) { } } +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 { @@ -210,3 +258,24 @@ func bind(t *testing.T, c net.Conn, decoder *xml.Decoder) { 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) + } +} diff --git a/session.go b/session.go index 84dd040..fa7d329 100644 --- a/session.go +++ b/session.go @@ -183,7 +183,7 @@ func (s *Session) rfc3921Session(o Config) { } var iq stanza.IQ - if s.Features.Session.XMLName.Local == "session" && !s.Features.Session.Optional { + if !s.Features.Session.IsOptional() { fmt.Fprintf(s.streamLogger, "", s.PacketId(), stanza.NSSession) if s.err = s.decoder.Decode(&iq); s.err != nil { s.err = errors.New("expecting iq result after session open: " + s.err.Error()) diff --git a/stanza/sasl_auth.go b/stanza/sasl_auth.go index b38570c..8bd4e06 100644 --- a/stanza/sasl_auth.go +++ b/stanza/sasl_auth.go @@ -95,10 +95,17 @@ func (s *StreamSession) Namespace() string { return s.XMLName.Space } +func (s *StreamSession) IsOptional() bool { + if s.XMLName.Local == "session" { + return s.Optional + } + return true +} + // ============================================================================ // 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", "bind"}, StreamSession{}) + TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:ietf:params:xml:ns:xmpp-session", "session"}, StreamSession{}) } diff --git a/stanza/sasl_auth_test.go b/stanza/sasl_auth_test.go index df2ee9a..d9ba1dc 100644 --- a/stanza/sasl_auth_test.go +++ b/stanza/sasl_auth_test.go @@ -8,7 +8,7 @@ import ( ) // Check that we can detect optional session from advertised stream features -func TestSession(t *testing.T) { +func TestSessionFeatures(t *testing.T) { streamFeatures := stanza.StreamFeatures{Session: stanza.StreamSession{Optional: true}} data, err := xml.Marshal(streamFeatures) @@ -18,10 +18,38 @@ func TestSession(t *testing.T) { parsedStream := stanza.StreamFeatures{} if err = xml.Unmarshal(data, &parsedStream); err != nil { - t.Errorf("Unmarshal(%s) returned error", data) + t.Errorf("Unmarshal(%s) returned error: %s", data, err) } - if !parsedStream.Session.Optional { + if !parsedStream.Session.IsOptional() { + t.Error("Session should be optional") + } +} + +// 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.Payload = &stanza.StreamSession{XMLName: xml.Name{Local: "session"}, Optional: true} + + data, err := xml.Marshal(iq) + if err != nil { + t.Errorf("cannot marshal xml structure: %s", err) + return + } + + parsedIQ := stanza.IQ{} + if err = xml.Unmarshal(data, &parsedIQ); err != nil { + t.Errorf("Unmarshal(%s) returned error: %s", data, err) + return + } + + session, ok := parsedIQ.Payload.(*stanza.StreamSession) + if !ok { + t.Error("Missing session payload") + return + } + + if !session.IsOptional() { t.Error("Session should be optional") } }