1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import struct
23 import socket
24
25 from twisted.web import http, server
26 from twisted.web import resource as web_resource
27 from twisted.internet import reactor, defer
28 from twisted.python import reflect, failure
29
30 from flumotion.configure import configure
31 from flumotion.common import errors
32 from flumotion.twisted.credentials import cryptChallenge
33
34 from flumotion.common import common, log, keycards
35
36
37 __version__ = "$Rev: 8058 $"
38
39
40 HTTP_SERVER_NAME = 'FlumotionHTTPServer'
41 HTTP_SERVER_VERSION = configure.version
42
43 ERROR_TEMPLATE = """<!doctype html public "-//IETF//DTD HTML 2.0//EN">
44 <html>
45 <head>
46 <title>%(code)d %(error)s</title>
47 </head>
48 <body>
49 <h2>%(code)d %(error)s</h2>
50 </body>
51 </html>
52 """
53
54 HTTP_SERVER = '%s/%s' % (HTTP_SERVER_NAME, HTTP_SERVER_VERSION)
55
56
57
58
59
61 """
62 I am a base class for all Issuers.
63 An issuer issues keycards of a given class based on an object
64 (incoming HTTP request, ...)
65 """
66
67 - def issue(self, *args, **kwargs):
68 """
69 Return a keycard, or None, based on the given arguments.
70 """
71 raise NotImplementedError
72
73
75 """
76 I create L{flumotion.common.keycards.Keycard} based on just a
77 standard HTTP request. Useful for authenticating based on
78 server-side checks such as time, rather than client credentials.
79 """
80
81 - def issue(self, request):
85
86
88 """
89 I create L{flumotion.common.keycards.KeycardUACPP} keycards based on
90 an incoming L{twisted.protocols.http.Request} request's standard
91 HTTP authentication information.
92 """
93
94 - def issue(self, request):
103
104
106 """
107 I create L{flumotion.common.keycards.KeycardToken} keycards based on
108 an incoming L{twisted.protocols.http.Request} request's GET "token"
109 parameter.
110 """
111
112 - def issue(self, request):
124
125
127 """
128 I create L{flumotion.common.keycards.KeycardHTTPGetArguments}
129 keycards based on an incoming L{twisted.protocols.http.Request}.
130 """
131
132 - def issue(self, request):
137
138
139 BOUNCER_SOCKET = 'flumotion.component.bouncers.plug.BouncerPlug'
140
141
143 """
144 Helper object for handling HTTP authentication for twisted.web
145 Resources, using issuers and bouncers.
146 """
147
148 logCategory = 'httpauth'
149
150 KEYCARD_TTL = 60 * 60
151 KEYCARD_KEEPALIVE_INTERVAL = 20 * 60
152 KEYCARD_TRYAGAIN_INTERVAL = 1 * 60
153
175
177
178 def timeout():
179
180 def reschedule(res):
181 if isinstance(res, failure.Failure):
182 self.info('keepAlive failed, rescheduling in %d '
183 'seconds', self.KEYCARD_TRYAGAIN_INTERVAL)
184 self._keepAlive = None
185 self.scheduleKeepAlive(tryingAgain=True)
186 else:
187 self.info('keepAlive successful')
188 self._keepAlive = None
189 self.scheduleKeepAlive(tryingAgain=False)
190
191 if self.bouncerName is not None:
192 self.debug('calling keepAlive on bouncer %s',
193 self.bouncerName)
194 d = self.keepAlive(self.bouncerName, self.issuerName,
195 self.KEYCARD_TTL)
196 d.addCallbacks(reschedule, reschedule)
197 else:
198 self.scheduleKeepAlive()
199
200 if tryingAgain:
201 self._keepAlive = reactor.callLater(
202 self.KEYCARD_TRYAGAIN_INTERVAL, timeout)
203 else:
204 self._keepAlive = reactor.callLater(
205 self.KEYCARD_KEEPALIVE_INTERVAL, timeout)
206
208 if self._keepAlive is not None:
209 self._keepAlive.cancel()
210 self._keepAlive = None
211
212 - def setDomain(self, domain):
213 """
214 Set a domain name on the resource, used in HTTP auth challenges and
215 on the keycard.
216
217 @type domain: string
218 """
219 self._domain = domain
220
222 self.bouncerName = bouncerName
223
225 self.requesterId = requesterId
226
227 self.issuerName = str(self.requesterId) + '-' + cryptChallenge()
228
230 self._defaultDuration = defaultDuration
231
233
234
235 if issuerClass == 'HTTPTokenIssuer':
236 self._issuer = HTTPTokenIssuer()
237 elif issuerClass == 'HTTPGetArgumentsIssuer':
238 self._issuer = HTTPGetArgumentsIssuer()
239 elif issuerClass == 'HTTPAuthIssuer':
240 self._issuer = HTTPAuthIssuer()
241 elif issuerClass == 'HTTPGenericIssuer':
242 self._issuer = HTTPGenericIssuer()
243 else:
244 raise ValueError("issuerClass %s not accepted" % issuerClass)
245
271
274
275 - def keepAlive(self, bouncerName, issuerName, ttl):
277
280
281
282
285
287
288
289
290 def cleanup(bouncerName, keycard):
291
292 def cleanupLater(res, pair):
293 self.log('failed to clean up keycard %r, will do '
294 'so later', keycard)
295 self._pendingCleanups.append(pair)
296 d = self.cleanupKeycard(bouncerName, keycard)
297 d.addErrback(cleanupLater, (bouncerName, keycard))
298 pending = self._pendingCleanups
299 self._pendingCleanups = []
300 cleanup(bouncerName, keycard)
301 for bouncerName, keycard in pending:
302 cleanup(bouncerName, keycard)
303
304
305
313
315 if self.bouncerName and fd in self._fdToKeycard:
316 keycard = self._fdToKeycard[fd]
317 del self._fdToKeycard[fd]
318 del self._idToKeycard[keycard.id]
319 if fd in self._fdToDurationCall:
320 self.debug('[fd %5d] canceling later expiration call' % fd)
321 self._fdToDurationCall[fd].cancel()
322 del self._fdToDurationCall[fd]
323
325 """
326 Expire a client due to a duration expiration.
327 """
328 self.debug('[fd %5d] duration exceeded, expiring client' % fd)
329
330
331 if fd in self._fdToDurationCall:
332 del self._fdToDurationCall[fd]
333
334 self.debug('[fd %5d] asking streamer to remove client' % fd)
335 self.clientDone(fd)
336
338 """
339 Expire a client's connection associated with the keycard Id.
340 """
341 keycard = self._idToKeycard[keycardId]
342 fd = keycard._fd
343
344 self.debug('[fd %5d] expiring client' % fd)
345
346 self._removeKeycard(fd)
347
348 self.debug('[fd %5d] asking streamer to remove client' % fd)
349 self.clientDone(fd)
350
352 """
353 Expire client's connections associated with the keycard Ids.
354 """
355 expired = 0
356 for keycardId in keycardIds:
357 try:
358 self.expireKeycard(keycardId)
359 expired += 1
360 except:
361 pass
362 return expired
363
364
365
373
409
415
423
441
442
444
447
449 """
450 Add an IP filter of the form IP/prefix-length (CIDR syntax), or just
451 a single IP address
452 """
453 definition = filter.split('/')
454 if len(definition) == 2:
455 (net, prefixlen) = definition
456 prefixlen = int(prefixlen)
457 elif len(definition) == 1:
458 net = definition[0]
459 prefixlen = 32
460 else:
461 raise errors.ConfigError(
462 "Cannot parse filter definition %s" % filter)
463
464 if prefixlen < 0 or prefixlen > 32:
465 raise errors.ConfigError("Invalid prefix length")
466
467 mask = ~((1 << (32 - prefixlen)) - 1)
468 try:
469 net = struct.unpack(">I", socket.inet_pton(socket.AF_INET, net))[0]
470 except socket.error:
471 raise errors.ConfigError(
472 "Failed to parse network address %s" % net)
473 net = net & mask
474
475 self.filters.append((net, mask))
476
478 """
479 Return true if ip is in any of the defined network(s) for this filter
480 """
481
482 realip = struct.unpack(">I", socket.inet_pton(socket.AF_INET, ip))[0]
483 for f in self.filters:
484 if (realip & f[1]) == f[0]:
485 return True
486 return False
487