From 2579c84481663f628c8547660c30c0b228fe2d04 Mon Sep 17 00:00:00 2001 From: Mickael Remond Date: Wed, 4 Oct 2017 19:27:35 -0400 Subject: [PATCH] Initial basic XMPP server mock. Work in progress: Need refactoring. --- xmpp/auth.go | 5 + xmpp/client_test.go | 227 ++++++++++++++++++++++++++++++++++++++++++++ xmpp/ns.go | 4 +- xmpp/parser.go | 8 +- xmpp/session.go | 14 +-- 5 files changed, 245 insertions(+), 13 deletions(-) create mode 100644 xmpp/client_test.go diff --git a/xmpp/auth.go b/xmpp/auth.go index 2874cae..23e76c2 100644 --- a/xmpp/auth.go +++ b/xmpp/auth.go @@ -61,7 +61,12 @@ type saslSuccess struct { type saslFailure struct { XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl failure"` Any xml.Name // error reason is a subelement +} +type auth struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-sasl auth"` + Mechanism string `xml:"mecanism,attr"` + Value string `xml:",innerxml"` } type bindBind struct { diff --git a/xmpp/client_test.go b/xmpp/client_test.go new file mode 100644 index 0000000..2728ff1 --- /dev/null +++ b/xmpp/client_test.go @@ -0,0 +1,227 @@ +package xmpp + +import ( + "encoding/xml" + "errors" + "fmt" + "net" + "testing" +) + +const ( + // Default port is not standard XMPP port to avoid interfering + // with local running XMPP server + testXMPPAddress = "localhost:15222" +) + +func TestClient_Connect(t *testing.T) { + // Setup Mock server + mock := XMPPServerMock{} + mock.Start(t, handlerConnackSuccess) + + // Test / Check result + options := Options{Address: testXMPPAddress, Jid: "test@localhost", Password: "test"} + + var client *Client + var err error + if client, err = NewClient(options); err != nil { + t.Errorf("connect create XMPP client: %s", err) + } + + var session *Session + if session, err = client.Connect(); err != nil { + t.Errorf("XMPP connection failed: %s", err) + } + + fmt.Println("Stream opened, we have streamID = ", session.StreamId) + + mock.Stop() +} + +//============================================================================= +// Basic XMPP Server Mock Handlers. + +const serverStreamOpen = "" + +func handlerConnackSuccess(t *testing.T, c net.Conn) { + decoder := xml.NewDecoder(c) + checkOpenStream(t, decoder) + + if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", NSClient, NSStream); err != nil { + t.Errorf("cannot write server stream open: %s", err) + } + fmt.Println("Sent stream Open") + sendStreamFeatures(t, c, decoder) + fmt.Println("Sent stream feature") + readAuth(t, decoder) + fmt.Fprintln(c, "") + + checkOpenStream(t, decoder) + if _, err := fmt.Fprintf(c, serverStreamOpen, "localhost", "streamid1", NSClient, NSStream); err != nil { + t.Errorf("cannot write server stream open: %s", err) + } + sendBindFeature(t, c, decoder) + + bind(t, c, decoder) + session(t, c, decoder) +} + +func checkOpenStream(t *testing.T, decoder *xml.Decoder) { + for { + var token xml.Token + 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 != NSStream || elem.Name.Local != "stream" { + err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space) + } + fmt.Printf("Received: %v\n", elem.Name.Local) + return + case xml.ProcInst: + fmt.Printf("Received: %v\n", elem.Inst) + } + } +} + +func sendStreamFeatures(t *testing.T, c net.Conn, decoder *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 := nextStart(decoder) + if err != nil { + t.Errorf("cannot read auth: %s", err) + return "" + } + + var nv interface{} + nv = &auth{} + // Decode element into pointer storage + if err = decoder.DecodeElement(nv, &se); err != nil { + fmt.Println(err) + t.Errorf("cannot decode auth: %s", err) + return "" + } + + switch v := nv.(type) { + case *auth: + return v.Value + } + return "" +} + +func sendBindFeature(t *testing.T, c net.Conn, decoder *xml.Decoder) { + // This is a basic server, supporting only 1 stream feature: SASL Plain Auth + 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 := nextStart(decoder) + if err != nil { + t.Errorf("cannot read bind: %s", err) + return + } + + iq := &ClientIQ{} + // Decode element into pointer storage + if err = decoder.DecodeElement(&iq, &se); err != nil { + fmt.Println(err) + t.Errorf("cannot decode bind iq: %s", err) + return + } + + switch payload := iq.Payload.(type) { + case *bindBind: + fmt.Println("JID:", payload.Jid) + } + 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) { + +} + +type testHandler func(t *testing.T, conn net.Conn) + +type XMPPServerMock struct { + t *testing.T + handler testHandler + listener net.Listener + connections []net.Conn + done chan struct{} +} + +func (mock *XMPPServerMock) Start(t *testing.T, handler testHandler) { + mock.t = t + mock.handler = handler + if err := mock.init(); err != nil { + return + } + go mock.loop() +} + +func (mock *XMPPServerMock) Stop() { + close(mock.done) + if mock.listener != nil { + mock.listener.Close() + } + // Close all existing connections + for _, c := range mock.connections { + c.Close() + } +} + +func (mock *XMPPServerMock) init() error { + mock.done = make(chan struct{}) + + l, err := net.Listen("tcp", testXMPPAddress) + if err != nil { + mock.t.Errorf("TCPServerMock cannot listen on address: %q", testXMPPAddress) + return err + } + mock.listener = l + return nil +} + +func (mock *XMPPServerMock) loop() { + listener := mock.listener + for { + conn, err := listener.Accept() + if err != nil { + select { + case <-mock.done: + return + default: + mock.t.Error("TCPServerMock accept error:", err.Error()) + } + return + } + mock.connections = append(mock.connections, conn) + // 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) + } +} diff --git a/xmpp/ns.go b/xmpp/ns.go index cf0f5fe..6e9e5ef 100644 --- a/xmpp/ns.go +++ b/xmpp/ns.go @@ -1,10 +1,10 @@ package xmpp const ( - nsStream = "http://etherx.jabber.org/streams" + NSStream = "http://etherx.jabber.org/streams" nsTLS = "urn:ietf:params:xml:ns:xmpp-tls" nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl" nsBind = "urn:ietf:params:xml:ns:xmpp-bind" nsSession = "urn:ietf:params:xml:ns:xmpp-session" - nsClient = "jabber:client" + NSClient = "jabber:client" ) diff --git a/xmpp/parser.go b/xmpp/parser.go index 58c689a..2346ed9 100644 --- a/xmpp/parser.go +++ b/xmpp/parser.go @@ -23,7 +23,7 @@ func initDecoder(p *xml.Decoder) (sessionID string, err error) { switch elem := t.(type) { case xml.StartElement: - if elem.Name.Space != nsStream || elem.Name.Local != "stream" { + if elem.Name.Space != NSStream || elem.Name.Local != "stream" { err = errors.New("xmpp: expected but got <" + elem.Name.Local + "> in " + elem.Name.Space) return } @@ -77,11 +77,11 @@ func next(p *xml.Decoder) (xml.Name, interface{}, error) { nv = &saslSuccess{} case nsSASL + " failure": nv = &saslFailure{} - case nsClient + " message": + case NSClient + " message": nv = &ClientMessage{} - case nsClient + " presence": + case NSClient + " presence": nv = &ClientPresence{} - case nsClient + " iq": + case NSClient + " iq": nv = &ClientIQ{} default: return xml.Name{}, nil, errors.New("unexpected XMPP message " + diff --git a/xmpp/session.go b/xmpp/session.go index 6a27fc8..34d2413 100644 --- a/xmpp/session.go +++ b/xmpp/session.go @@ -82,7 +82,7 @@ func (s *Session) setProxy(conn net.Conn, newConn net.Conn, o Options) { func (s *Session) open(domain string) (f streamFeatures) { // Send stream open tag - if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, nsClient, nsStream); s.err != nil { + if _, s.err = fmt.Fprintf(s.socketProxy, xmppStreamOpen, domain, NSClient, NSStream); s.err != nil { return } @@ -177,11 +177,11 @@ func (s *Session) rfc3921Session(o Options) { } var iq ClientIQ - - // TODO: Do no send unconditionally, check if session is optional and omit it - fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsSession) - if s.err = s.decoder.Decode(&iq); s.err != nil { - s.err = errors.New("expecting iq result after session open: " + s.err.Error()) - return + if s.Features.Session.optional.Local != "" { + fmt.Fprintf(s.socketProxy, "", s.PacketId(), nsSession) + if s.err = s.decoder.Decode(&iq); s.err != nil { + s.err = errors.New("expecting iq result after session open: " + s.err.Error()) + return + } } }