-
Notifications
You must be signed in to change notification settings - Fork 62
/
Copy pathrouter.go
374 lines (319 loc) · 10 KB
/
router.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package xmpp
import (
"context"
"encoding/xml"
"strings"
"sync"
"gosrc.io/xmpp/stanza"
)
/*
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.
- An empty route will match everything. Adding an empty route as the last route in your router will
allow you to get all stanzas that did not match any previous route. You can for example use this to
log all unexpected stanza received by your client or component.
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
IQResultRoutes map[string]*IQResultRoute
IQResultRouteLock sync.RWMutex
}
// NewRouter returns a new router instance.
func NewRouter() *Router {
return &Router{
IQResultRoutes: make(map[string]*IQResultRoute),
}
}
// 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()
route, ok := r.IQResultRoutes[iq.Id]
r.IQResultRouteLock.RUnlock()
if ok {
r.IQResultRouteLock.Lock()
delete(r.IQResultRoutes, iq.Id)
r.IQResultRouteLock.Unlock()
route.result <- *iq
close(route.result)
return
}
}
var match RouteMatch
if r.Match(p, &match) {
// If we match, route the packet
match.Handler.HandlePacket(s, p)
return
}
// If there is no match and we receive an iq set or get, we need to send a reply
if isIq && (iq.Type == stanza.IQTypeGet || iq.Type == stanza.IQTypeSet) {
iqNotImplemented(s, iq)
}
}
// 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 {
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. Not sure I should leave this. Maybe let users call ack again by themselves ?
s.Send(stanza.SMRequest{})
}
uaq.RWMutex.Unlock()
return nil
}
func iqNotImplemented(s Sender, iq *stanza.IQ) {
err := stanza.Err{
XMLName: xml.Name{Local: "error"},
Code: 501,
Type: "cancel",
Reason: "feature-not-implemented",
}
reply := iq.MakeError(err)
_ = s.Send(reply)
}
// NewRoute registers an empty routes
func (r *Router) NewRoute() *Route {
route := &Route{}
r.routes = append(r.routes, route)
return route
}
// NewIQResultRoute register a route that will catch an IQ result stanza with
// the given Id. The route will only match ones, after which it will automatically
// be unregistered
func (r *Router) NewIQResultRoute(ctx context.Context, id string) chan stanza.IQ {
route := NewIQResultRoute(ctx)
r.IQResultRouteLock.Lock()
r.IQResultRoutes[id] = route
r.IQResultRouteLock.Unlock()
// Start a go function to make sure the route is unregistered when the context
// is done.
go func() {
<-route.context.Done()
r.IQResultRouteLock.Lock()
delete(r.IQResultRoutes, id)
r.IQResultRouteLock.Unlock()
}()
return route.result
}
func (r *Router) Match(p stanza.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 stanza.Packet)) *Route {
return r.NewRoute().Packet(name).HandlerFunc(f)
}
// ============================================================================
// TimeoutHandlerFunc is a function type for handling IQ result timeouts.
type TimeoutHandlerFunc func(err error)
// IQResultRoute is a temporary route to match IQ result stanzas
type IQResultRoute struct {
context context.Context
result chan stanza.IQ
}
// NewIQResultRoute creates a new IQResultRoute instance
func NewIQResultRoute(ctx context.Context) *IQResultRoute {
return &IQResultRoute{
context: ctx,
result: make(chan stanza.IQ),
}
}
// ============================================================================
// IQ result handler
// IQResultHandler is a utility interface for IQ result handlers
type IQResultHandler interface {
HandleIQ(ctx context.Context, s Sender, iq stanza.IQ)
}
// IQResultHandlerFunc is an adapter to allow using functions as IQ result handlers.
type IQResultHandlerFunc func(ctx context.Context, s Sender, iq stanza.IQ)
// HandleIQ is a proxy function to implement IQResultHandler using a function.
func (f IQResultHandlerFunc) HandleIQ(ctx context.Context, s Sender, iq stanza.IQ) {
f(ctx, s, iq)
}
// ============================================================================
// Route
type Handler interface {
HandlePacket(s Sender, p stanza.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 stanza.Packet)
// HandlePacket calls f(s, p)
func (f HandlerFunc) HandlePacket(s Sender, p stanza.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 stanza.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 stanza.Packet, match *RouteMatch) bool {
var name string
// TODO: To avoid type switch everywhere in matching, I think we will need to have
// to move to a concrete type for packets, to make matching and comparison more natural.
// Current code structure is probably too rigid.
// Maybe packet types should even be from an enum.
switch p.(type) {
case stanza.Message:
name = "message"
case *stanza.IQ:
name = "iq"
case stanza.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 stanza type
// nsTypeMather matches on a list of IQ payload namespaces
type nsTypeMatcher []string
func (m nsTypeMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
var stanzaType stanza.StanzaType
switch packet := p.(type) {
case *stanza.IQ:
stanzaType = packet.Type
case stanza.Presence:
stanzaType = packet.Type
case stanza.Message:
if packet.Type == "" {
// optional on message, normal is the default type
stanzaType = "normal"
} else {
stanzaType = packet.Type
}
default:
return false
}
return matchInArray(m, string(stanzaType))
}
// IQNamespaces adds an IQ matcher, expecting both an IQ and a
func (r *Route) StanzaType(types ...string) *Route {
for k, v := range types {
types[k] = strings.ToLower(v)
}
return r.AddMatcher(nsTypeMatcher(types))
}
// -------------------------
// Match on IQ and namespace
// nsIqMather matches on a list of IQ payload namespaces
type nsIQMatcher []string
func (m nsIQMatcher) Match(p stanza.Packet, match *RouteMatch) bool {
iq, ok := p.(*stanza.IQ)
if !ok {
return false
}
if iq.Payload == nil {
return false
}
return matchInArray(m, iq.Payload.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.
// You can register attach them to a route via the AddMatcher method.
type Matcher interface {
Match(stanza.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
}