Repository: FluuxIO/go-xmpp
Pull request #55 (2019-06-18 12:33:19)

Add support for initial Router for packet matching & delegation
- Add basic support for Router to make it easier to get your code trigger when some packets are received.
- Add initial support for namespace delegation (XEP-0355) and priviledged entities (XEP-0356) and example component.
- Clean up un IQ structures.

mremond: @genofire Here is a big change in API for components. You can check the diff on the example to get an idea of the impact. The main goal was to be able to support XEP-0355 and XEP-0356.
I have also tagged previous version as v0.1.0 so that you can stay on that branch if you are not yet confortable with new API or have doubts about it.

To get an idea of where we should be heading: I think we still should make it easier to generate XMPP packets. I need to think about how to improve that.

Patch:
diff --git a/.gitignore b/.gitignore
index 04fa968..79a4126 100755
--- a/.gitignore
+++ b/.gitignore
@@ -31,4 +31,6 @@ coverage.txt
 
 # Do not commit codeship key
 codeship.aes
-codeship.env
\ No newline at end of file
+codeship.env
+
+priv/
\ No newline at end of file
diff --git a/README.md b/README.md
index 06f9bfb..ba5d951 100644
--- a/README.md
+++ b/README.md
@@ -37,30 +37,30 @@ func main() {
 		Insecure:     true,
 	}
 
-	client, err := xmpp.NewClient(config)
+	router := xmpp.NewRouter()
+	router.HandleFunc("message", HandleMessage)
+
+	client, err := xmpp.NewClient(config, router)
 	if err != nil {
 		log.Fatalf("%+v", err)
 	}
 
 	// If you pass the client to a connection manager, it will handle the reconnect policy
 	// for you automatically.
-	cm := xmpp.NewClientManager(client, nil)
-	err = cm.Start()
-	if err != nil {
-		log.Fatal(err)
-	}
+	cm := xmpp.NewStreamManager(client, nil)
+	log.Fatal(cm.Run())
+}
 
-	// Iterator to receive packets coming from our XMPP connection
-	for packet := range client.Recv() {
-		switch packet := packet.(type) {
-		case xmpp.Message:
-			_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
-			reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body}
-			_ = client.Send(reply)
-		default:
-			_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
-		}
+func HandleMessage(s xmpp.Sender, p xmpp.Packet) {
+	msg, ok := p.(xmpp.Message)
+	if !ok {
+		_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
+		return
 	}
+
+	_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
+	reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: msg.From}, Body: msg.Body}
+	_ = s.Send(reply)
 }
 ```
 
diff --git a/_examples/delegation/README.md b/_examples/delegation/README.md
new file mode 100644
index 0000000..4389a36
--- /dev/null
+++ b/_examples/delegation/README.md
@@ -0,0 +1,5 @@
+# Advanced component: delegation
+
+`delegation` is an example of advanced component supporting Namespace Delegation
+([XEP-0355](https://xmpp.org/extensions/xep-0355.html)) and privileged entity
+([XEP-356](https://xmpp.org/extensions/xep-0356.html)).
diff --git a/_examples/delegation/delegation.go b/_examples/delegation/delegation.go
new file mode 100644
index 0000000..6f40b2a
--- /dev/null
+++ b/_examples/delegation/delegation.go
@@ -0,0 +1,227 @@
+package main
+
+import (
+	"encoding/xml"
+	"fmt"
+	"log"
+
+	"gosrc.io/xmpp"
+)
+
+func main() {
+	opts := xmpp.ComponentOptions{
+		Domain:  "service.localhost",
+		Secret:  "mypass",
+		Address: "localhost:9999",
+
+		// TODO: Move that part to a component discovery handler
+		Name:     "Test Component",
+		Category: "gateway",
+		Type:     "service",
+	}
+
+	router := xmpp.NewRouter()
+	router.HandleFunc("message", HandleMessage)
+	router.NewRoute().
+		IQNamespaces(xmpp.NSDiscoInfo).
+		HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
+			DiscoInfo(s, p, opts)
+		})
+	router.NewRoute().
+		IQNamespaces("urn:xmpp:delegation:1").
+		HandlerFunc(HandleDelegation)
+
+	component, err := xmpp.NewComponent(opts, router)
+	if err != nil {
+		log.Fatalf("%+v", err)
+	}
+
+	// If you pass the component to a stream manager, it will handle the reconnect policy
+	// for you automatically.
+	// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
+	cm := xmpp.NewStreamManager(component, nil)
+	log.Fatal(cm.Run())
+}
+
+func HandleMessage(_ xmpp.Sender, p xmpp.Packet) {
+	msg, ok := p.(xmpp.Message)
+	if !ok {
+		return
+	}
+	var msgProcessed bool
+	for _, ext := range msg.Extensions {
+		delegation, ok := ext.(*xmpp.Delegation)
+		if ok {
+			msgProcessed = true
+			fmt.Printf("Delegation confirmed for namespace %s\n", delegation.Delegated.Namespace)
+		}
+	}
+	// TODO: Decode privilege message
+	// <message to='service.localhost' from='localhost'><privilege xmlns='urn:xmpp:privilege:1'><perm type='outgoing' access='message'/><perm type='get' access='roster'/><perm type='managed_entity' access='presence'/></privilege></message>
+
+	if !msgProcessed {
+		fmt.Printf("Ignored received message, not related to delegation: %v\n", msg)
+	}
+}
+
+const (
+	pubsubNode = "urn:xmpp:delegation:1::http://jabber.org/protocol/pubsub"
+	pepNode    = "urn:xmpp:delegation:1:bare:http://jabber.org/protocol/pubsub"
+)
+
+// TODO: replace xmpp.Sender by ctx xmpp.Context ?
+// ctx.Stream.Send / SendRaw
+// ctx.Opts
+func DiscoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
+	// Type conversion & sanity checks
+	iq, ok := p.(xmpp.IQ)
+	if !ok {
+		return
+	}
+	info, ok := iq.Payload[0].(*xmpp.DiscoInfo)
+	if !ok {
+		return
+	}
+
+	iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+
+	switch info.Node {
+	case "":
+		DiscoInfoRoot(&iqResp, opts)
+	case pubsubNode:
+		DiscoInfoPubSub(&iqResp)
+	case pepNode:
+		DiscoInfoPEP(&iqResp)
+	}
+
+	_ = c.Send(iqResp)
+}
+
+func DiscoInfoRoot(iqResp *xmpp.IQ, opts xmpp.ComponentOptions) {
+	// Higher level discovery
+	identity := xmpp.Identity{
+		Name:     opts.Name,
+		Category: opts.Category,
+		Type:     opts.Type,
+	}
+	payload := xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: xmpp.NSDiscoInfo,
+			Local: "query",
+		},
+		Identity: identity,
+		Features: []xmpp.Feature{
+			{Var: xmpp.NSDiscoInfo},
+			{Var: xmpp.NSDiscoItems},
+			{Var: "jabber:iq:version"},
+			{Var: "urn:xmpp:delegation:1"},
+		},
+	}
+	iqResp.AddPayload(&payload)
+}
+
+func DiscoInfoPubSub(iqResp *xmpp.IQ) {
+	payload := xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: xmpp.NSDiscoInfo,
+			Local: "query",
+		},
+		Node: pubsubNode,
+		Features: []xmpp.Feature{
+			{Var: "http://jabber.org/protocol/pubsub"},
+			{Var: "http://jabber.org/protocol/pubsub#publish"},
+			{Var: "http://jabber.org/protocol/pubsub#subscribe"},
+			{Var: "http://jabber.org/protocol/pubsub#publish-options"},
+		},
+	}
+	iqResp.AddPayload(&payload)
+}
+
+func DiscoInfoPEP(iqResp *xmpp.IQ) {
+	identity := xmpp.Identity{
+		Category: "pubsub",
+		Type:     "pep",
+	}
+	payload := xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: xmpp.NSDiscoInfo,
+			Local: "query",
+		},
+		Identity: identity,
+		Node:     pepNode,
+		Features: []xmpp.Feature{
+			{Var: "http://jabber.org/protocol/pubsub#access-presence"},
+			{Var: "http://jabber.org/protocol/pubsub#auto-create"},
+			{Var: "http://jabber.org/protocol/pubsub#auto-subscribe"},
+			{Var: "http://jabber.org/protocol/pubsub#config-node"},
+			{Var: "http://jabber.org/protocol/pubsub#create-and-configure"},
+			{Var: "http://jabber.org/protocol/pubsub#create-nodes"},
+			{Var: "http://jabber.org/protocol/pubsub#filtered-notifications"},
+			{Var: "http://jabber.org/protocol/pubsub#persistent-items"},
+			{Var: "http://jabber.org/protocol/pubsub#publish"},
+			{Var: "http://jabber.org/protocol/pubsub#retrieve-items"},
+			{Var: "http://jabber.org/protocol/pubsub#subscribe"},
+		},
+	}
+	iqResp.AddPayload(&payload)
+}
+
+func HandleDelegation(s xmpp.Sender, p xmpp.Packet) {
+	// Type conversion & sanity checks
+	iq, ok := p.(xmpp.IQ)
+	if !ok {
+		return
+	}
+
+	payload1 := iq.Payload[0]
+	delegation, ok := payload1.(*xmpp.Delegation)
+	if !ok {
+		return
+	}
+	forwardedPacket := delegation.Forwarded.Stanza
+	fmt.Println(forwardedPacket)
+	forwardedIQ, ok := forwardedPacket.(xmpp.IQ)
+	if !ok {
+		return
+	}
+	payload := forwardedIQ.Payload
+	if len(payload) == 0 {
+		return
+	}
+
+	pubsub, ok := payload[0].(*xmpp.PubSub)
+	if !ok {
+		// We only support pubsub delegation
+		return
+	}
+
+	if pubsub.Publish.XMLName.Local == "publish" {
+		// Prepare pubsub IQ reply
+		iqResp := xmpp.NewIQ("result", forwardedIQ.To, forwardedIQ.From, forwardedIQ.Id, "en")
+		payload := xmpp.PubSub{
+			XMLName: xml.Name{
+				Space: "http://jabber.org/protocol/pubsub",
+				Local: "pubsub",
+			},
+		}
+		iqResp.AddPayload(&payload)
+		// Wrap the reply in delegation 'forward'
+		iqForward := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+		delegPayload := xmpp.Delegation{
+			XMLName: xml.Name{
+				Space: "urn:xmpp:delegation:1",
+				Local: "delegation",
+			},
+			Forwarded: &xmpp.Forwarded{
+				XMLName: xml.Name{
+					Space: "urn:xmpp:forward:0",
+					Local: "forward",
+				},
+				Stanza: iqResp,
+			},
+		}
+		iqForward.AddPayload(&delegPayload)
+		_ = s.Send(iqForward)
+		// TODO: The component should actually broadcast the mood to subscribers
+	}
+}
diff --git a/_examples/xmpp_component/README.md b/_examples/xmpp_component/README.md
index d58cef1..3cd25dd 100644
--- a/_examples/xmpp_component/README.md
+++ b/_examples/xmpp_component/README.md
@@ -3,6 +3,8 @@
 This component will connect to ejabberd and act as a subdomain "service" of your primary XMPP domain
 (in that case localhost).
 
+This component does nothing expect connect and show up in service discovery.
+
 To be able to connect this component, you need to add a listener to your XMPP server.
 
 Here is an example ejabberd configuration for that component listener:
diff --git a/_examples/xmpp_component/xmpp_component.go b/_examples/xmpp_component/xmpp_component.go
index 0981196..d013752 100644
--- a/_examples/xmpp_component/xmpp_component.go
+++ b/_examples/xmpp_component/xmpp_component.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"encoding/xml"
 	"fmt"
 	"log"
 
@@ -9,114 +10,123 @@ import (
 
 func main() {
 	opts := xmpp.ComponentOptions{
-		Domain:   "service.localhost",
+		Domain:   "service2.localhost",
 		Secret:   "mypass",
 		Address:  "localhost:8888",
 		Name:     "Test Component",
 		Category: "gateway",
 		Type:     "service",
 	}
-	component, err := xmpp.NewComponent(opts)
+
+	router := xmpp.NewRouter()
+	router.HandleFunc("message", HandleMessage)
+	router.NewRoute().
+		IQNamespaces(xmpp.NSDiscoInfo).
+		HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
+			DiscoInfo(s, p, opts)
+		})
+	router.NewRoute().
+		IQNamespaces(xmpp.NSDiscoItems).
+		HandlerFunc(DiscoItems)
+	router.NewRoute().
+		IQNamespaces("jabber:iq:version").
+		HandlerFunc(HandleVersion)
+
+	component, err := xmpp.NewComponent(opts, router)
 	if err != nil {
 		log.Fatalf("%+v", err)
 	}
 
-	// If you pass the component to a connection manager, it will handle the reconnect policy
+	// If you pass the component to a stream manager, it will handle the reconnect policy
 	// for you automatically.
+	// TODO: Post Connect could be a feature of the router or the client. Move it somewhere else.
 	cm := xmpp.NewStreamManager(component, nil)
-	err = cm.Start()
-	if err != nil {
-		log.Fatal(err)
-	}
+	log.Fatal(cm.Run())
+}
 
-	// Iterator to receive packets coming from our XMPP connection
-	for packet := range component.Recv() {
-		switch p := packet.(type) {
-		case xmpp.IQ:
-			switch inner := p.Payload[0].(type) {
-			case *xmpp.DiscoInfo:
-				fmt.Println("DiscoInfo")
-				if p.Type == "get" {
-					discoResult(component, p.PacketAttrs, inner)
-				}
-			case *xmpp.DiscoItems:
-				fmt.Println("DiscoItems")
-				if p.Type == "get" {
-					discoItems(component, p.PacketAttrs, inner)
-				}
-			case *xmpp.Version:
-				fmt.Println("Version")
-				if p.Type == "get" {
-					version(component, p.PacketAttrs)
-				}
-			default:
-				fmt.Println("ignoring iq packet", inner)
-				xError := xmpp.Err{
-					Code:   501,
-					Reason: "feature-not-implemented",
-					Type:   "cancel",
-				}
-				reply := p.MakeError(xError)
-				_ = component.Send(&reply)
-			}
-
-		case xmpp.Message:
-			fmt.Println("Received message:", p.Body)
-
-		case xmpp.Presence:
-			fmt.Println("Received presence:", p.Type)
-
-		default:
-			fmt.Println("ignoring packet:", packet)
-		}
+func HandleMessage(_ xmpp.Sender, p xmpp.Packet) {
+	msg, ok := p.(xmpp.Message)
+	if !ok {
+		return
 	}
+	fmt.Println("Received message:", msg.Body)
 }
 
-func discoResult(c *xmpp.Component, attrs xmpp.PacketAttrs, info *xmpp.DiscoInfo) {
-	iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
-	var identity xmpp.Identity
-	if info.Node == "" {
-		identity = xmpp.Identity{
-			Name:     c.Name,
-			Category: c.Category,
-			Type:     c.Type,
-		}
+func DiscoInfo(c xmpp.Sender, p xmpp.Packet, opts xmpp.ComponentOptions) {
+	// Type conversion & sanity checks
+	iq, ok := p.(xmpp.IQ)
+	if !ok {
+		return
 	}
 
+	iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
+	identity := xmpp.Identity{
+		Name:     opts.Name,
+		Category: opts.Category,
+		Type:     opts.Type,
+	}
 	payload := xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: xmpp.NSDiscoInfo,
+			Local: "query",
+		},
 		Identity: identity,
 		Features: []xmpp.Feature{
 			{Var: xmpp.NSDiscoInfo},
 			{Var: xmpp.NSDiscoItems},
 			{Var: "jabber:iq:version"},
+			{Var: "urn:xmpp:delegation:1"},
 		},
 	}
-	iq.AddPayload(&payload)
-
-	_ = c.Send(iq)
+	iqResp.AddPayload(&payload)
+	_ = c.Send(iqResp)
 }
 
-func discoItems(c *xmpp.Component, attrs xmpp.PacketAttrs, items *xmpp.DiscoItems) {
-	iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
+// TODO: Handle iq error responses
+func DiscoItems(c xmpp.Sender, p xmpp.Packet) {
+	// Type conversion & sanity checks
+	iq, ok := p.(xmpp.IQ)
+	if !ok {
+		return
+	}
+
+	if len(iq.Payload) == 0 {
+		return
+	}
+	discoItems, ok := iq.Payload[0].(*xmpp.DiscoItems)
+	if !ok {
+		return
+	}
+
+	if iq.Type != "get" {
+		return
+	}
+
+	iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
 
 	var payload xmpp.DiscoItems
-	if items.Node == "" {
+	if discoItems.Node == "" {
 		payload = xmpp.DiscoItems{
 			Items: []xmpp.DiscoItem{
 				{Name: "test node", JID: "service.localhost", Node: "node1"},
 			},
 		}
 	}
-	iq.AddPayload(&payload)
-	_ = c.Send(iq)
+	iqResp.AddPayload(&payload)
+	_ = c.Send(iqResp)
 }
 
-func version(c *xmpp.Component, attrs xmpp.PacketAttrs) {
-	iq := xmpp.NewIQ("result", attrs.To, attrs.From, attrs.Id, "en")
+func HandleVersion(c xmpp.Sender, p xmpp.Packet) {
+	// Type conversion & sanity checks
+	iq, ok := p.(xmpp.IQ)
+	if !ok {
+		return
+	}
 
+	iqResp := xmpp.NewIQ("result", iq.To, iq.From, iq.Id, "en")
 	var payload xmpp.Version
 	payload.Name = "Fluux XMPP Component"
 	payload.Version = "0.0.1"
 	iq.AddPayload(&payload)
-	_ = c.Send(iq)
+	_ = c.Send(iqResp)
 }
diff --git a/_examples/xmpp_echo/xmpp_echo.go b/_examples/xmpp_echo/xmpp_echo.go
index f87cdab..0bc6e92 100644
--- a/_examples/xmpp_echo/xmpp_echo.go
+++ b/_examples/xmpp_echo/xmpp_echo.go
@@ -21,7 +21,10 @@ func main() {
 		Insecure:     true,
 	}
 
-	client, err := xmpp.NewClient(config)
+	router := xmpp.NewRouter()
+	router.HandleFunc("message", HandleMessage)
+
+	client, err := xmpp.NewClient(config, router)
 	if err != nil {
 		log.Fatalf("%+v", err)
 	}
@@ -29,22 +32,19 @@ func main() {
 	// If you pass the client to a connection manager, it will handle the reconnect policy
 	// for you automatically.
 	cm := xmpp.NewStreamManager(client, nil)
-	err = cm.Start()
-	if err != nil {
-		log.Fatal(err)
-	}
+	log.Fatal(cm.Run())
+}
 
-	// Iterator to receive packets coming from our XMPP connection
-	for packet := range client.Recv() {
-		switch packet := packet.(type) {
-		case xmpp.Message:
-			_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", packet.Body, packet.From)
-			reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: packet.From}, Body: packet.Body}
-			_ = client.Send(reply)
-		default:
-			_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", packet)
-		}
+func HandleMessage(s xmpp.Sender, p xmpp.Packet) {
+	msg, ok := p.(xmpp.Message)
+	if !ok {
+		_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
+		return
 	}
+
+	_, _ = fmt.Fprintf(os.Stdout, "Body = %s - from = %s\n", msg.Body, msg.From)
+	reply := xmpp.Message{PacketAttrs: xmpp.PacketAttrs{To: msg.From}, Body: msg.Body}
+	_ = s.Send(reply)
 }
 
 // TODO create default command line client to send message or to send an arbitrary XMPP sequence from a file,
diff --git a/_examples/xmpp_jukebox/xmpp_jukebox.go b/_examples/xmpp_jukebox/xmpp_jukebox.go
index f5e373f..5e5a35a 100644
--- a/_examples/xmpp_jukebox/xmpp_jukebox.go
+++ b/_examples/xmpp_jukebox/xmpp_jukebox.go
@@ -74,6 +74,7 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
 
 		playSCURL(p, url)
 		setResponse := new(xmpp.ControlSetResponse)
+		// FIXME: Broken
 		reply := xmpp.IQ{PacketAttrs: xmpp.PacketAttrs{To: packet.From, Type: "result", Id: packet.Id}, Payload: []xmpp.IQPayload{setResponse}}
 		_ = client.Send(reply)
 		// TODO add Soundclound artist / title retrieval
@@ -86,7 +87,7 @@ func processIq(client *xmpp.Client, p *mpg123.Player, packet *xmpp.IQ) {
 func sendUserTune(client *xmpp.Client, artist string, title string) {
 	tune := xmpp.Tune{Artist: artist, Title: title}
 	iq := xmpp.NewIQ("set", "", "", "usertune-1", "en")
-	payload := xmpp.PubSub{Publish: xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: tune}}}
+	payload := xmpp.PubSub{Publish: &xmpp.Publish{Node: "http://jabber.org/protocol/tune", Item: xmpp.Item{Tune: &tune}}}
 	iq.AddPayload(&payload)
 	_ = client.Send(iq)
 }
diff --git a/auth.go b/auth.go
index e69f82e..5af154a 100644
--- a/auth.go
+++ b/auth.go
@@ -102,12 +102,15 @@ type auth struct {
 }
 
 type BindBind struct {
-	IQPayload
 	XMLName  xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-bind bind"`
 	Resource string   `xml:"resource,omitempty"`
 	Jid      string   `xml:"jid,omitempty"`
 }
 
+func (b *BindBind) Namespace() string {
+	return b.XMLName.Space
+}
+
 // Session is obsolete in RFC 6121.
 // Added for compliance with RFC 3121.
 // Remove when ejabberd purely conforms to RFC 6121.
diff --git a/client.go b/client.go
index 7d5a424..3cb43cd 100644
--- a/client.go
+++ b/client.go
@@ -70,8 +70,8 @@ type Client struct {
 	Session *Session
 	// TCP level connection / can be replaced by a TLS session after starttls
 	conn net.Conn
-	// Packet channel
-	RecvChannel chan Packet
+	// Router is used to dispatch packets
+	router *Router
 	// Track and broadcast connection state
 	EventManager
 }
@@ -84,7 +84,7 @@ 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.
 // TODO: better config checks
-func NewClient(config Config) (c *Client, err error) {
+func NewClient(config Config, r *Router) (c *Client, err error) {
 	// TODO: If option address is nil, use the Jid domain to compose the address
 	if config.Address, err = checkAddress(config.Address); err != nil {
 		return nil, NewConnError(err, true)
@@ -103,14 +103,12 @@ func NewClient(config Config) (c *Client, err error) {
 
 	c = new(Client)
 	c.config = config
+	c.router = r
 
 	if c.config.ConnectTimeout == 0 {
 		c.config.ConnectTimeout = 15 // 15 second as default
 	}
 
-	// Create a default channel that developers can override
-	c.RecvChannel = make(chan Packet)
-
 	return
 }
 
@@ -173,13 +171,6 @@ func (c *Client) SetHandler(handler EventHandler) {
 	c.Handler = handler
 }
 
-// Recv abstracts receiving preparsed XMPP packets from a channel.
-// Channel allow client to receive / dispatch packets in for range loop.
-// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
-func (c *Client) Recv() <-chan Packet {
-	return c.RecvChannel
-}
-
 // Send marshals XMPP stanza and sends it to the server.
 func (c *Client) Send(packet Packet) error {
 	data, err := xml.Marshal(packet)
@@ -219,13 +210,13 @@ func (c *Client) recv(keepaliveQuit chan<- struct{}) (err error) {
 		// Handle stream errors
 		switch packet := val.(type) {
 		case StreamError:
-			c.RecvChannel <- val
-			close(c.RecvChannel)
+			c.router.Route(c, val)
+			close(keepaliveQuit)
 			c.streamError(packet.Error.Local, packet.Text)
 			return errors.New("stream error: " + packet.Error.Local)
 		}
 
-		c.RecvChannel <- val
+		c.router.Route(c, val)
 	}
 }
 
diff --git a/component.go b/component.go
index 6a6445b..9f319f6 100644
--- a/component.go
+++ b/component.go
@@ -37,8 +37,6 @@ type ComponentOptions struct {
 	// =================================
 	// Communication with developer client / StreamManager
 
-	// Packet channel
-	RecvChannel chan Packet
 	// Track and broadcast connection state
 	EventManager
 }
@@ -48,6 +46,7 @@ type ComponentOptions struct {
 // in XEP-0114, XEP-0355 and XEP-0356.
 type Component struct {
 	ComponentOptions
+	router *Router
 
 	// TCP level connection
 	conn net.Conn
@@ -57,10 +56,8 @@ type Component struct {
 	decoder     *xml.Decoder
 }
 
-func NewComponent(opts ComponentOptions) (*Component, error) {
-	c := Component{ComponentOptions: opts}
-	// Create a default channel that developers can override
-	c.RecvChannel = make(chan Packet)
+func NewComponent(opts ComponentOptions, r *Router) (*Component, error) {
+	c := Component{ComponentOptions: opts, router: r}
 	return &c, nil
 }
 
@@ -119,13 +116,7 @@ func (c *Component) SetHandler(handler EventHandler) {
 	c.Handler = handler
 }
 
-// Recv abstracts receiving preparsed XMPP packets from a channel.
-// Channel allow client to receive / dispatch packets in for range loop.
-// TODO: Deprecate this function in favor of reading directly from the RecvChannel ?
-func (c *Component) Recv() <-chan Packet {
-	return c.RecvChannel
-}
-
+// Receiver Go routine receiver
 func (c *Component) recv() (err error) {
 	for {
 		val, err := next(c.decoder)
@@ -137,12 +128,11 @@ func (c *Component) recv() (err error) {
 		// Handle stream errors
 		switch p := val.(type) {
 		case StreamError:
-			c.RecvChannel <- val
-			close(c.RecvChannel)
+			c.router.Route(c, val)
 			c.streamError(p.Error.Local, p.Text)
 			return errors.New("stream error: " + p.Error.Local)
 		}
-		c.RecvChannel <- val
+		c.router.Route(c, val)
 	}
 }
 
@@ -213,6 +203,63 @@ func (handshakeDecoder) decode(p *xml.Decoder, se xml.StartElement) (Handshake,
 	return packet, err
 }
 
+// ============================================================================
+// Component delegation
+// XEP-0355
+
+// Delegation can be used both on message (for delegated) and IQ (for Forwarded),
+// depending on the context.
+type Delegation struct {
+	MsgExtension
+	XMLName   xml.Name   `xml:"urn:xmpp:delegation:1 delegation"`
+	Forwarded *Forwarded // This is used in iq to wrap delegated iqs
+	Delegated *Delegated // This is used in a message to confirm delegated namespace
+}
+
+func (d *Delegation) Namespace() string {
+	return d.XMLName.Space
+}
+
+type Forwarded struct {
+	XMLName xml.Name `xml:"urn:xmpp:forward:0 forwarded"`
+	Stanza  Packet
+}
+
+// UnmarshalXML is a custom unmarshal function used by xml.Unmarshal to
+// transform generic XML content into hierarchical Node structure.
+func (f *Forwarded) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+	// Check subelements to extract required field as boolean
+	for {
+		t, err := d.Token()
+		if err != nil {
+			return err
+		}
+
+		switch tt := t.(type) {
+
+		case xml.StartElement:
+			if packet, err := decodeClient(d, tt); err == nil {
+				f.Stanza = packet
+			}
+
+		case xml.EndElement:
+			if tt == start.End() {
+				return nil
+			}
+		}
+	}
+}
+
+type Delegated struct {
+	XMLName   xml.Name `xml:"delegated"`
+	Namespace string   `xml:"namespace,attr,omitempty"`
+}
+
+func init() {
+	TypeRegistry.MapExtension(PKTMessage, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
+	TypeRegistry.MapExtension(PKTIQ, xml.Name{"urn:xmpp:delegation:1", "delegation"}, Delegation{})
+}
+
 /*
 TODO: Add support for discovery management directly in component
 TODO: Support multiple identities on disco info
diff --git a/iot_control.go b/iot_control.go
index 3079246..5e80957 100644
--- a/iot_control.go
+++ b/iot_control.go
@@ -5,11 +5,14 @@ import (
 )
 
 type ControlSet struct {
-	IQPayload
 	XMLName xml.Name       `xml:"urn:xmpp:iot:control set"`
 	Fields  []ControlField `xml:",any"`
 }
 
+func (c *ControlSet) Namespace() string {
+	return c.XMLName.Space
+}
+
 type ControlGetForm struct {
 	XMLName xml.Name `xml:"urn:xmpp:iot:control getForm"`
 }
@@ -24,3 +27,7 @@ type ControlSetResponse struct {
 	IQPayload
 	XMLName xml.Name `xml:"urn:xmpp:iot:control setResponse"`
 }
+
+func (c *ControlSetResponse) Namespace() string {
+	return c.XMLName.Space
+}
diff --git a/iq.go b/iq.go
index d69df71..9cd334a 100644
--- a/iq.go
+++ b/iq.go
@@ -16,7 +16,6 @@ TODO support ability to put Raw payload inside IQ
 // presence or iq stanza.
 // It is intended to be added in the payload of the erroneous stanza.
 type Err struct {
-	IQPayload
 	XMLName xml.Name `xml:"error"`
 	Code    int      `xml:"code,attr,omitempty"`
 	Type    string   `xml:"type,attr,omitempty"`
@@ -24,6 +23,10 @@ type Err struct {
 	Text    string `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text,omitempty"`
 }
 
+func (x *Err) Namespace() string {
+	return x.XMLName.Space
+}
+
 // UnmarshalXML implements custom parsing for IQs
 func (x *Err) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
 	x.XMLName = start.Name
@@ -120,6 +123,10 @@ func (x Err) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
 type IQ struct { // Info/Query
 	XMLName xml.Name `xml:"iq"`
 	PacketAttrs
+	// FIXME: We can only have one payload:
+	//   "An IQ stanza of type "get" or "set" MUST contain exactly one
+	//    child element, which specifies the semantics of the particular
+	//    request."
 	Payload []IQPayload `xml:",omitempty"`
 	RawXML  string      `xml:",innerxml"`
 	Error   Err         `xml:"error,omitempty"`
@@ -229,18 +236,23 @@ func (iq *IQ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
 // ============================================================================
 // Generic IQ Payload
 
-type IQPayload interface{}
+type IQPayload interface {
+	Namespace() string
+}
 
 // Node is a generic structure to represent XML data. It is used to parse
 // unreferenced or custom stanza payload.
 type Node struct {
-	IQPayload
 	XMLName xml.Name
 	Attrs   []xml.Attr `xml:"-"`
 	Content string     `xml:",innerxml"`
 	Nodes   []Node     `xml:",any"`
 }
 
+func (n *Node) Namespace() string {
+	return n.XMLName.Space
+}
+
 // Attr represents generic XML attributes, as used on the generic XML Node
 // representation.
 type Attr struct {
@@ -283,13 +295,16 @@ const (
 
 // Disco Info
 type DiscoInfo struct {
-	IQPayload
 	XMLName  xml.Name  `xml:"http://jabber.org/protocol/disco#info query"`
 	Node     string    `xml:"node,attr,omitempty"`
 	Identity Identity  `xml:"identity"`
 	Features []Feature `xml:"feature"`
 }
 
+func (d *DiscoInfo) Namespace() string {
+	return d.XMLName.Space
+}
+
 type Identity struct {
 	XMLName  xml.Name `xml:"identity,omitempty"`
 	Name     string   `xml:"name,attr,omitempty"`
@@ -304,12 +319,15 @@ type Feature struct {
 
 // Disco Items
 type DiscoItems struct {
-	IQPayload
 	XMLName xml.Name    `xml:"http://jabber.org/protocol/disco#items query"`
 	Node    string      `xml:"node,attr,omitempty"`
 	Items   []DiscoItem `xml:"item"`
 }
 
+func (d *DiscoItems) Namespace() string {
+	return d.XMLName.Space
+}
+
 type DiscoItem struct {
 	XMLName xml.Name `xml:"item"`
 	Name    string   `xml:"name,attr,omitempty"`
@@ -322,13 +340,16 @@ type DiscoItem struct {
 
 // Version
 type Version struct {
-	IQPayload
 	XMLName xml.Name `xml:"jabber:iq:version query"`
 	Name    string   `xml:"name,omitempty"`
 	Version string   `xml:"version,omitempty"`
 	OS      string   `xml:"os,omitempty"`
 }
 
+func (v *Version) Namespace() string {
+	return v.XMLName.Space
+}
+
 // ============================================================================
 // Registry init
 
diff --git a/pep.go b/pep.go
index 6ead8ae..a3d0497 100644
--- a/pep.go
+++ b/pep.go
@@ -1,25 +1,11 @@
 package xmpp // import "gosrc.io/xmpp"
 
+// TODO: Move to a pubsub file
+
 import (
 	"encoding/xml"
 )
 
-type PubSub struct {
-	XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
-	Publish Publish
-}
-
-type Publish struct {
-	XMLName xml.Name `xml:"publish"`
-	Node    string   `xml:"node,attr"`
-	Item    Item
-}
-
-type Item struct {
-	XMLName xml.Name `xml:"item"`
-	Tune    Tune
-}
-
 type Tune struct {
 	XMLName xml.Name `xml:"http://jabber.org/protocol/tune tune"`
 	Artist  string   `xml:"artist,omitempty"`
@@ -31,53 +17,13 @@ type Tune struct {
 	Uri     string   `xml:"uri,omitempty"`
 }
 
-/*
-type PubsubPublish struct {
-	XMLName xml.Name `xml:"publish"`
-	node    string   `xml:"node,attr"`
-	item    PubSubItem
-}
-
-type PubSubItem struct {
-	xmlName xml.Name `xml:"item"`
-}
-
-type Thing2 struct {
-	XMLName xml.Name `xml:"publish"`
-	node    string   `xml:"node,attr"`
-	tune    string   `xml:"http://jabber.org/protocol/tune item>tune"`
-}
-
-type Tune struct {
-	artist string
-	length int
-	rating int
-	source string
-	title  string
-	track  string
-	uri    string
-}
-*/
-
-/*
-func (*Tune) XMPPFormat() string {
-	return fmt.Sprintf(
-		`<iq type='set' id='%s'>
- <pubsub xmlns='http://jabber.org/protocol/pubsub'>
-  <publish node='http://jabber.org/protocol/tune'>
-   <item>
-    <tune xmlns='http://jabber.org/protocol/tune'>
-     <artist>%s</artist>
-     <length>%i</length>
-     <rating>%i</rating>
-     <source>%s</source>
-     <title>%s</title>
-     <track>%s</track>
-     <uri>%s</uri>
-    </tune>
-   </item>
-  </publish>
- </pubsub>
-</iq>`)
+// Mood defines deta 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
+	XMLName      xml.Name `xml:"http://jabber.org/protocol/mood mood"`
+	// TODO: Custom parsing to extract mood type from tag name.
+	// Note: the list is predefined.
+	// Mood type
+	Text string `xml:"text,omitempty"`
 }
-*/
diff --git a/pubsub.go b/pubsub.go
new file mode 100644
index 0000000..88e3b35
--- /dev/null
+++ b/pubsub.go
@@ -0,0 +1,40 @@
+package xmpp // import "gosrc.io/xmpp"
+
+import (
+	"encoding/xml"
+)
+
+type PubSub struct {
+	XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
+	Publish *Publish
+	Retract *Retract
+	// TODO <configure/>
+}
+
+func (p *PubSub) Namespace() string {
+	return p.XMLName.Space
+}
+
+type Publish struct {
+	XMLName xml.Name `xml:"publish"`
+	Node    string   `xml:"node,attr"`
+	Item    Item
+}
+
+type Item struct {
+	XMLName xml.Name `xml:"item"`
+	Id      string   `xml:"id,attr,omitempty"`
+	Tune    *Tune
+	Mood    *Mood
+}
+
+type Retract struct {
+	XMLName xml.Name `xml:"retract"`
+	Node    string   `xml:"node,attr"`
+	Notify  string   `xml:"notify,attr"`
+	Item    Item
+}
+
+func init() {
+	TypeRegistry.MapExtension(PKTIQ, xml.Name{"http://jabber.org/protocol/pubsub", "pubsub"}, PubSub{})
+}
diff --git a/router.go b/router.go
new file mode 100644
index 0000000..92b1594
--- /dev/null
+++ b/router.go
@@ -0,0 +1,197 @@
+package xmpp
+
+import (
+	"strings"
+)
+
+/*
+The XMPP router helps client and component developers select which XMPP they would like to process,
+and associate processing code depending on the router configuration.
+
+Here are important rules to keep in mind while setting your routes and matchers:
+- Routes are evaluated in the order they are set.
+- When a route matches, it is executed and all others routes are ignored. For each packet, only a single
+  route is executed.
+
+TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard.
+*/
+
+type Router struct {
+	// Routes to be matched, in order.
+	routes []*Route
+}
+
+// NewRouter returns a new router instance.
+func NewRouter() *Router {
+	return &Router{}
+}
+
+func (r *Router) Route(s Sender, p Packet) {
+	var match RouteMatch
+	if r.Match(p, &match) {
+		match.Handler.HandlePacket(s, p)
+	}
+}
+
+// NewRoute registers an empty routes
+func (r *Router) NewRoute() *Route {
+	route := &Route{}
+	r.routes = append(r.routes, route)
+	return route
+}
+
+func (r *Router) Match(p Packet, match *RouteMatch) bool {
+	for _, route := range r.routes {
+		if route.Match(p, match) {
+			return true
+		}
+	}
+	return false
+}
+
+// Handle registers a new route with a matcher for a given packet name (iq, message, presence)
+// See Route.Packet() and Route.Handler().
+func (r *Router) Handle(name string, handler Handler) *Route {
+	return r.NewRoute().Packet(name).Handler(handler)
+}
+
+// HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence)
+// See Route.Path() and Route.HandlerFunc().
+func (r *Router) HandleFunc(name string, f func(s Sender, p Packet)) *Route {
+	return r.NewRoute().Packet(name).HandlerFunc(f)
+}
+
+// ============================================================================
+// Route
+type Handler interface {
+	HandlePacket(s Sender, p Packet)
+}
+
+type Route struct {
+	handler Handler
+	// Matchers are used to "specialize" routes and focus on specific packet features
+	matchers []matcher
+}
+
+func (r *Route) Handler(handler Handler) *Route {
+	r.handler = handler
+	return r
+}
+
+// The HandlerFunc type is an adapter to allow the use of
+// ordinary functions as XMPP handlers. If f is a function
+// with the appropriate signature, HandlerFunc(f) is a
+// Handler that calls f.
+type HandlerFunc func(s Sender, p Packet)
+
+// HandlePacket calls f(s, p)
+func (f HandlerFunc) HandlePacket(s Sender, p Packet) {
+	f(s, p)
+}
+
+// HandlerFunc sets a handler function for the route
+func (r *Route) HandlerFunc(f HandlerFunc) *Route {
+	return r.Handler(f)
+}
+
+// addMatcher adds a matcher to the route
+func (r *Route) addMatcher(m matcher) *Route {
+	r.matchers = append(r.matchers, m)
+	return r
+}
+
+func (r *Route) Match(p Packet, match *RouteMatch) bool {
+	for _, m := range r.matchers {
+		if matched := m.Match(p, match); !matched {
+			return false
+		}
+	}
+
+	// We have a match, let's pass info route match info
+	match.Route = r
+	match.Handler = r.handler
+	return true
+}
+
+// --------------------
+// Match on packet name
+
+type nameMatcher string
+
+func (n nameMatcher) Match(p Packet, match *RouteMatch) bool {
+	var name string
+	// TODO: To avoid type switch everywhere in matching, I think we will need to have
+	//    to move to a concrete type for packets, to make matching and comparison more natural.
+	//    Current code structure is probably too rigid.
+	// Maybe packet types should even be from an enum.
+	switch p.(type) {
+	case Message:
+		name = "message"
+	case IQ:
+		name = "iq"
+	case Presence:
+		name = "presence"
+	}
+	if name == string(n) {
+		return true
+	}
+	return false
+}
+
+// Packet matches on a packet name (iq, message, presence, ...)
+// It matches on the Local part of the xml.Name
+func (r *Route) Packet(name string) *Route {
+	name = strings.ToLower(name)
+	return r.addMatcher(nameMatcher(name))
+}
+
+// -------------------------
+// Match on IQ and namespace
+
+// nsIqMather matches on a list of IQ  payload namespaces
+type nsIQMatcher []string
+
+func (m nsIQMatcher) Match(p Packet, match *RouteMatch) bool {
+	// TODO
+	iq, ok := p.(IQ)
+	if !ok {
+		return false
+	}
+	if len(iq.Payload) < 1 {
+		return false
+	}
+	return matchInArray(m, iq.Payload[0].Namespace())
+}
+
+// IQNamespaces adds an IQ matcher, expecting both an IQ and a
+func (r *Route) IQNamespaces(namespaces ...string) *Route {
+	for k, v := range namespaces {
+		namespaces[k] = strings.ToLower(v)
+	}
+	return r.addMatcher(nsIQMatcher(namespaces))
+}
+
+// ============================================================================
+// Matchers
+
+// Matchers are used to "specialize" routes and focus on specific packet features
+type matcher interface {
+	Match(Packet, *RouteMatch) bool
+}
+
+// RouteMatch extracts and gather match information
+type RouteMatch struct {
+	Route   *Route
+	Handler Handler
+}
+
+// matchInArray is a generic matching function to check if a string is a list
+// of specific function
+func matchInArray(arr []string, value string) bool {
+	for _, str := range arr {
+		if str == value {
+			return true
+		}
+	}
+	return false
+}
diff --git a/stream.go b/stream.go
index d25ce58..20759ba 100644
--- a/stream.go
+++ b/stream.go
@@ -6,7 +6,9 @@ import (
 
 // ============================================================================
 // StreamFeatures Packet
-// Reference: https://xmpp.org/registrar/stream-features.html
+// Reference: The active stream features are published on
+//            https://xmpp.org/registrar/stream-features.html
+// Note: That page misses draft and experimental XEP (i.e CSI, etc)
 
 type StreamFeatures struct {
 	XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
diff --git a/stream_manager.go b/stream_manager.go
index b353c2b..f867e34 100644
--- a/stream_manager.go
+++ b/stream_manager.go
@@ -2,6 +2,7 @@ package xmpp // import "gosrc.io/xmpp"
 
 import (
 	"errors"
+	"sync"
 	"time"
 
 	"golang.org/x/xerrors"
@@ -18,12 +19,20 @@ import (
 //       permanent errors to avoid useless reconnection loops.
 //     - Metrics processing
 
+// StreamClient is an interface used by StreamManager to control Client lifecycle,
+// set callback and trigger reconnection.
 type StreamClient interface {
 	Connect() error
 	Disconnect()
 	SetHandler(handler EventHandler)
 }
 
+// Sender is an interface provided by Stream clients to allow sending XMPP data.
+type Sender interface {
+	Send(packet Packet) error
+	SendRaw(packet string) error
+}
+
 // StreamManager supervises an XMPP client connection. Its role is to handle connection events and
 // apply reconnection strategy.
 type StreamManager struct {
@@ -32,6 +41,8 @@ type StreamManager struct {
 
 	// Store low level metrics
 	Metrics *Metrics
+
+	wg sync.WaitGroup
 }
 
 type PostConnect func(c StreamClient)
@@ -47,8 +58,10 @@ func NewStreamManager(client StreamClient, pc PostConnect) *StreamManager {
 	}
 }
 
-// Start launch the connection loop
-func (sm *StreamManager) Start() error {
+// Run launchs the connection of the underlying client or component
+// and wait until Disconnect is called, or for the manager to terminate due
+// to an unrecoverable error.
+func (sm *StreamManager) Run() error {
 	if sm.client == nil {
 		return errors.New("missing stream client")
 	}
@@ -72,7 +85,13 @@ func (sm *StreamManager) Start() error {
 	}
 	sm.client.SetHandler(handler)
 
-	return sm.connect()
+	sm.wg.Add(1)
+	if err := sm.connect(); err != nil {
+		sm.wg.Done()
+		return err
+	}
+	sm.wg.Wait()
+	return nil
 }
 
 // Stop cancels pending operations and terminates existing XMPP client.
@@ -80,6 +99,7 @@ func (sm *StreamManager) Stop() {
 	// Remove on disconnect handler to avoid triggering reconnect
 	sm.client.SetHandler(nil)
 	sm.client.Disconnect()
+	sm.wg.Done()
 }
 
 // connect manages the reconnection loop and apply the define backoff to avoid overloading the server.


Test patch:
diff --git a/client_test.go b/client_test.go
index 7d68717..f231c97 100644
--- a/client_test.go
+++ b/client_test.go
@@ -27,7 +27,8 @@ func TestClient_Connect(t *testing.T) {
 
 	var client *Client
 	var err error
-	if client, err = NewClient(config); err != nil {
+	router := NewRouter()
+	if client, err = NewClient(config, router); err != nil {
 		t.Errorf("connect create XMPP client: %s", err)
 	}
 
@@ -48,7 +49,8 @@ func TestClient_NoInsecure(t *testing.T) {
 
 	var client *Client
 	var err error
-	if client, err = NewClient(config); err != nil {
+	router := NewRouter()
+	if client, err = NewClient(config, router); err != nil {
 		t.Errorf("cannot create XMPP client: %s", err)
 	}
 
@@ -71,7 +73,8 @@ func TestClient_FeaturesTracking(t *testing.T) {
 
 	var client *Client
 	var err error
-	if client, err = NewClient(config); err != nil {
+	router := NewRouter()
+	if client, err = NewClient(config, router); err != nil {
 		t.Errorf("cannot create XMPP client: %s", err)
 	}
 
diff --git a/component_test.go b/component_test.go
index 20e3772..55c4569 100644
--- a/component_test.go
+++ b/component_test.go
@@ -1,6 +1,9 @@
 package xmpp // import "gosrc.io/xmpp"
 
-import "testing"
+import (
+	"encoding/xml"
+	"testing"
+)
 
 func TestHandshake(t *testing.T) {
 	opts := ComponentOptions{
@@ -21,3 +24,78 @@ func TestHandshake(t *testing.T) {
 func TestGenerateHandshake(t *testing.T) {
 	// TODO
 }
+
+// We should be able to properly parse delegation confirmation messages
+func TestParsingDelegationMessage(t *testing.T) {
+	packetStr := `<message to='service.localhost' from='localhost'>
+ <delegation xmlns='urn:xmpp:delegation:1'>
+  <delegated namespace='http://jabber.org/protocol/pubsub'/>
+ </delegation>
+</message>`
+	var msg Message
+	data := []byte(packetStr)
+	if err := xml.Unmarshal(data, &msg); err != nil {
+		t.Errorf("Unmarshal(%s) returned error", data)
+	}
+
+	// Check that we have extracted the delegation info as MsgExtension
+	var nsDelegated string
+	for _, ext := range msg.Extensions {
+		if delegation, ok := ext.(*Delegation); ok {
+			nsDelegated = delegation.Delegated.Namespace
+		}
+	}
+	if nsDelegated != "http://jabber.org/protocol/pubsub" {
+		t.Errorf("Could not find delegated namespace in delegation: %#v\n", msg)
+	}
+}
+
+// Check that we can parse a delegation IQ.
+// The most important thing is to be able to
+func TestParsingDelegationIQ(t *testing.T) {
+	packetStr := `<iq to='service.localhost' from='localhost' type='set' id='1'>
+ <delegation xmlns='urn:xmpp:delegation:1'>
+  <forwarded xmlns='urn:xmpp:forward:0'>
+   <iq xml:lang='en' to='test1@localhost' from='test1@localhost/mremond-mbp' type='set' id='aaf3a' xmlns='jabber:client'>
+    <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+     <publish node='http://jabber.org/protocol/mood'>
+      <item id='current'>
+       <mood xmlns='http://jabber.org/protocol/mood'>
+        <excited/>
+       </mood>
+      </item>
+     </publish>
+    </pubsub>
+   </iq>
+  </forwarded>
+ </delegation>
+</iq>`
+	var iq IQ
+	data := []byte(packetStr)
+	if err := xml.Unmarshal(data, &iq); err != nil {
+		t.Errorf("Unmarshal(%s) returned error", data)
+	}
+
+	// Check that we have extracted the delegation info as IQPayload
+	var node string
+	for _, ext := range iq.Payload {
+		if delegation, ok := ext.(*Delegation); ok {
+			packet := delegation.Forwarded.Stanza
+			forwardedIQ, ok := packet.(IQ)
+			if !ok {
+				t.Errorf("Could not extract packet IQ")
+				return
+			}
+			payload := forwardedIQ.Payload
+			if len(payload) > 0 {
+				pl := payload[0]
+				if pubsub, ok := pl.(*PubSub); ok {
+					node = pubsub.Publish.Node
+				}
+			}
+		}
+	}
+	if node != "http://jabber.org/protocol/mood" {
+		t.Errorf("Could not find mood node name on delegated publish: %#v\n", iq)
+	}
+}
diff --git a/router_test.go b/router_test.go
new file mode 100644
index 0000000..8afbc9c
--- /dev/null
+++ b/router_test.go
@@ -0,0 +1,118 @@
+package xmpp_test
+
+import (
+	"bytes"
+	"encoding/xml"
+	"testing"
+
+	"gosrc.io/xmpp"
+)
+
+// ============================================================================
+// SenderMock
+
+// ============================================================================
+// Test route & matchers
+
+func TestNameMatcher(t *testing.T) {
+	router := xmpp.NewRouter()
+	router.HandleFunc("message", func(s xmpp.Sender, p xmpp.Packet) {
+		_ = s.SendRaw(successFlag)
+	})
+
+	// Check that a message packet is properly matched
+	conn := NewSenderMock()
+	// TODO: We want packet creation code to use struct to use default values
+	msg := xmpp.NewMessage("chat", "", "test@localhost", "1", "")
+	msg.Body = "Hello"
+	router.Route(conn, msg)
+	if conn.String() != successFlag {
+		t.Error("Message was not matched and routed properly")
+	}
+
+	// Check that an IQ packet is not matched
+	conn = NewSenderMock()
+	iq := xmpp.NewIQ("get", "", "localhost", "1", "")
+	iq.Payload = append(iq.Payload, &xmpp.DiscoInfo{})
+	router.Route(conn, iq)
+	if conn.String() == successFlag {
+		t.Error("IQ should not have been matched and routed")
+	}
+}
+
+func TestIQNSMatcher(t *testing.T) {
+	router := xmpp.NewRouter()
+	router.NewRoute().
+		IQNamespaces(xmpp.NSDiscoInfo, xmpp.NSDiscoItems).
+		HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
+			_ = s.SendRaw(successFlag)
+		})
+
+	// Check that an IQ with proper namespace does match
+	conn := NewSenderMock()
+	iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "")
+	// TODO: Add a function to generate payload with proper namespace initialisation
+	iqDisco.Payload = append(iqDisco.Payload, &xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: xmpp.NSDiscoInfo,
+			Local: "query",
+		}})
+	router.Route(conn, iqDisco)
+	if conn.String() != successFlag {
+		t.Errorf("IQ should have been matched and routed: %v", iqDisco)
+	}
+
+	// Check that another namespace is not matched
+	conn = NewSenderMock()
+	iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "")
+	// TODO: Add a function to generate payload with proper namespace initialisation
+	iqVersion.Payload = append(iqVersion.Payload, &xmpp.DiscoInfo{
+		XMLName: xml.Name{
+			Space: "jabber:iq:version",
+			Local: "query",
+		}})
+	router.Route(conn, iqVersion)
+	if conn.String() == successFlag {
+		t.Errorf("IQ should not have been matched and routed: %v", iqVersion)
+	}
+}
+
+var successFlag = "matched"
+
+type SenderMock struct {
+	buffer *bytes.Buffer
+}
+
+func NewSenderMock() SenderMock {
+	return SenderMock{buffer: new(bytes.Buffer)}
+}
+
+func (s SenderMock) Send(packet xmpp.Packet) error {
+	out, err := xml.Marshal(packet)
+	if err != nil {
+		return err
+	}
+	s.buffer.Write(out)
+	return nil
+}
+
+func (s SenderMock) SendRaw(str string) error {
+	s.buffer.WriteString(str)
+	return nil
+}
+
+func (s SenderMock) String() string {
+	return s.buffer.String()
+}
+
+func TestSenderMock(t *testing.T) {
+	conn := NewSenderMock()
+	msg := xmpp.NewMessage("", "", "test@localhost", "1", "")
+	msg.Body = "Hello"
+	if err := conn.Send(msg); err != nil {
+		t.Error("Could not send message")
+	}
+	if conn.String() != "<message id=\"1\" to=\"test@localhost\"><body>Hello</body></message>" {
+		t.Errorf("Incorrect packet sent: %s", conn.String())
+	}
+}