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$"
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 create L{flumotion.common.keycards.Keycard} based on an
63 HTTP request. Useful for authenticating based on
64 server-side checks such as time, as well as client credentials
65 such as HTTP Auth, get parameters, IP address and token.
66 """
67
68 - def issue(self, request):
82
83
84 BOUNCER_SOCKET = 'flumotion.component.bouncers.plug.BouncerPlug'
85 BUS_SOCKET = 'flumotion.component.plugs.bus.BusPlug'
86
87
89 """
90 Helper object for handling HTTP authentication for twisted.web
91 Resources, using issuers and bouncers.
92 """
93
94 logCategory = 'httpauth'
95
96 KEYCARD_TTL = 60 * 60
97 KEYCARD_KEEPALIVE_INTERVAL = 20 * 60
98 KEYCARD_TRYAGAIN_INTERVAL = 1 * 60
99
125
127
128 def timeout():
129
130 def reschedule(res):
131 if isinstance(res, failure.Failure):
132 self.info('keepAlive failed, rescheduling in %d '
133 'seconds', self.KEYCARD_TRYAGAIN_INTERVAL)
134 self._keepAlive = None
135 self.scheduleKeepAlive(tryingAgain=True)
136 else:
137 self.info('keepAlive successful')
138 self._keepAlive = None
139 self.scheduleKeepAlive(tryingAgain=False)
140
141 if self.bouncerName is not None:
142 self.debug('calling keepAlive on bouncer %s',
143 self.bouncerName)
144 d = self.keepAlive(self.bouncerName, self.issuerName,
145 self.KEYCARD_TTL)
146 d.addCallbacks(reschedule, reschedule)
147 else:
148 self.scheduleKeepAlive()
149
150 if tryingAgain:
151 self._keepAlive = reactor.callLater(
152 self.KEYCARD_TRYAGAIN_INTERVAL, timeout)
153 else:
154 self._keepAlive = reactor.callLater(
155 self.KEYCARD_KEEPALIVE_INTERVAL, timeout)
156
158 if self._keepAlive is not None:
159 self._keepAlive.cancel()
160 self._keepAlive = None
161
162 - def setDomain(self, domain):
163 """
164 Set a domain name on the resource, used in HTTP auth challenges and
165 on the keycard.
166
167 @type domain: string
168 """
169 self._domain = domain
170
172 self.bouncerName = bouncerName
173
175 self.requesterId = requesterId
176
177 self.issuerName = str(self.requesterId) + '-' + cryptChallenge()
178
180 self._defaultDuration = defaultDuration
181
183 self._allowDefault = allowDefault
184
210
213
214 - def keepAlive(self, bouncerName, issuerName, ttl):
216
219
220
221
224
226
227
228
229 def cleanup(bouncerName, keycard):
230
231 def cleanupLater(res, pair):
232 self.log('failed to clean up keycard %r, will do '
233 'so later', keycard)
234 self._pendingCleanups.append(pair)
235 d = self.cleanupKeycard(bouncerName, keycard)
236 d.addErrback(cleanupLater, (bouncerName, keycard))
237 pending = self._pendingCleanups
238 self._pendingCleanups = []
239 cleanup(bouncerName, keycard)
240 for bouncerName, keycard in pending:
241 cleanup(bouncerName, keycard)
242
243
244
252
254 if (self.bouncerName or self.plug) and fd in self._fdToKeycard:
255 keycard = self._fdToKeycard[fd]
256 del self._fdToKeycard[fd]
257 del self._idToKeycard[keycard.id]
258 if fd in self._fdToDurationCall:
259 self.debug('[fd %5d] canceling later expiration call' % fd)
260 self._fdToDurationCall[fd].cancel()
261 del self._fdToDurationCall[fd]
262
264 """
265 Expire a client due to a duration expiration.
266 """
267 self.debug('[fd %5d] duration exceeded, expiring client' % fd)
268
269
270 if fd in self._fdToDurationCall:
271 del self._fdToDurationCall[fd]
272
273 self.debug('[fd %5d] asking streamer to remove client' % fd)
274 self.clientDone(fd)
275
277 """
278 Expire a client's connection associated with the keycard Id.
279 """
280 keycard = self._idToKeycard[keycardId]
281
282 fd = keycard._fd
283
284 self.debug('[fd %5d] expiring client' % fd)
285
286 self._removeKeycard(fd)
287
288 self.debug('[fd %5d] asking streamer to remove client' % fd)
289 self.clientDone(fd)
290
292 """
293 Expire client's connections associated with the keycard Ids.
294 """
295 expired = 0
296 for keycardId in keycardIds:
297 try:
298 self.expireKeycard(keycardId)
299 expired += 1
300 except KeyError, e:
301 self.warn("Failed to expire keycard %r: %s",
302 keycardId, log.getExceptionMessage(e))
303 return expired
304
305
306
314
350
355
368
387
388
390
393
395 """
396 Add an IP filter of the form IP/prefix-length (CIDR syntax), or just
397 a single IP address
398 """
399 definition = filter.split('/')
400 if len(definition) == 2:
401 (net, prefixlen) = definition
402 prefixlen = int(prefixlen)
403 elif len(definition) == 1:
404 net = definition[0]
405 prefixlen = 32
406 else:
407 raise errors.ConfigError(
408 "Cannot parse filter definition %s" % filter)
409
410 if prefixlen < 0 or prefixlen > 32:
411 raise errors.ConfigError("Invalid prefix length")
412
413 mask = ~((1 << (32 - prefixlen)) - 1)
414 try:
415 net = struct.unpack(">I", socket.inet_pton(socket.AF_INET, net))[0]
416 except socket.error:
417 raise errors.ConfigError(
418 "Failed to parse network address %s" % net)
419 net = net & mask
420
421 self.filters.append((net, mask))
422
424 """
425 Return true if ip is in any of the defined network(s) for this filter
426 """
427
428 realip = struct.unpack(">I", socket.inet_pton(socket.AF_INET, ip))[0]
429 for f in self.filters:
430 if (realip & f[1]) == f[0]:
431 return True
432 return False
433