1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 import struct
19 import socket
20
21 from twisted.web import http
22 from twisted.internet import reactor, defer
23 from twisted.python import failure
24
25 from flumotion.configure import configure
26 from flumotion.common import errors
27 from flumotion.twisted.credentials import cryptChallenge
28
29 from flumotion.common import log, keycards
30
31
32 __version__ = "$Rev$"
33
34
35 HTTP_SERVER_NAME = 'FlumotionHTTPServer'
36 HTTP_SERVER_VERSION = configure.version
37
38 ERROR_TEMPLATE = """<!doctype html public "-//IETF//DTD HTML 2.0//EN">
39 <html>
40 <head>
41 <title>%(code)d %(error)s</title>
42 </head>
43 <body>
44 <h2>%(code)d %(error)s</h2>
45 </body>
46 </html>
47 """
48
49 HTTP_SERVER = '%s/%s' % (HTTP_SERVER_NAME, HTTP_SERVER_VERSION)
50
51
52
53
54
56 """
57 I create L{flumotion.common.keycards.Keycard} based on an
58 HTTP request. Useful for authenticating based on
59 server-side checks such as time, as well as client credentials
60 such as HTTP Auth, get parameters, IP address and token.
61 """
62
63 - def issue(self, request):
77
78
79 BOUNCER_SOCKET = 'flumotion.component.bouncers.plug.BouncerPlug'
80 BUS_SOCKET = 'flumotion.component.plugs.bus.BusPlug'
81
82
84 """
85 Helper object for handling HTTP authentication for twisted.web
86 Resources, using issuers and bouncers.
87 """
88
89 logCategory = 'httpauth'
90
91 KEYCARD_TTL = 60 * 60
92 KEYCARD_KEEPALIVE_INTERVAL = 20 * 60
93 KEYCARD_TRYAGAIN_INTERVAL = 1 * 60
94
120
122
123 def timeout():
124
125 def reschedule(res):
126 if isinstance(res, failure.Failure):
127 self.info('keepAlive failed, rescheduling in %d '
128 'seconds', self.KEYCARD_TRYAGAIN_INTERVAL)
129 self._keepAlive = None
130 self.scheduleKeepAlive(tryingAgain=True)
131 else:
132 self.info('keepAlive successful')
133 self._keepAlive = None
134 self.scheduleKeepAlive(tryingAgain=False)
135
136 if self.bouncerName is not None:
137 self.debug('calling keepAlive on bouncer %s',
138 self.bouncerName)
139 d = self.keepAlive(self.bouncerName, self.issuerName,
140 self.KEYCARD_TTL)
141 d.addCallbacks(reschedule, reschedule)
142 else:
143 self.scheduleKeepAlive()
144
145 if tryingAgain:
146 self._keepAlive = reactor.callLater(
147 self.KEYCARD_TRYAGAIN_INTERVAL, timeout)
148 else:
149 self._keepAlive = reactor.callLater(
150 self.KEYCARD_KEEPALIVE_INTERVAL, timeout)
151
153 if self._keepAlive is not None:
154 self._keepAlive.cancel()
155 self._keepAlive = None
156
157 - def setDomain(self, domain):
158 """
159 Set a domain name on the resource, used in HTTP auth challenges and
160 on the keycard.
161
162 @type domain: string
163 """
164 self._domain = domain
165
167 self.bouncerName = bouncerName
168
170 self.requesterId = requesterId
171
172 self.issuerName = str(self.requesterId) + '-' + cryptChallenge()
173
175 self._defaultDuration = defaultDuration
176
178 self._allowDefault = allowDefault
179
205
208
209 - def keepAlive(self, bouncerName, issuerName, ttl):
211
214
215
216
219
221
222
223
224 def cleanup(bouncerName, keycard):
225
226 def cleanupLater(res, pair):
227 self.log('failed to clean up keycard %r, will do '
228 'so later', keycard)
229 self._pendingCleanups.append(pair)
230 d = self.cleanupKeycard(bouncerName, keycard)
231 d.addErrback(cleanupLater, (bouncerName, keycard))
232 pending = self._pendingCleanups
233 self._pendingCleanups = []
234 cleanup(bouncerName, keycard)
235 for bouncerName, keycard in pending:
236 cleanup(bouncerName, keycard)
237
238
239
247
249 if (self.bouncerName or self.plug) and fd in self._fdToKeycard:
250 keycard = self._fdToKeycard[fd]
251 del self._fdToKeycard[fd]
252 del self._idToKeycard[keycard.id]
253 if fd in self._fdToDurationCall:
254 self.debug('[fd %5d] canceling later expiration call' % fd)
255 self._fdToDurationCall[fd].cancel()
256 del self._fdToDurationCall[fd]
257
259 """
260 Expire a client due to a duration expiration.
261 """
262 self.debug('[fd %5d] duration exceeded, expiring client' % fd)
263
264
265 if fd in self._fdToDurationCall:
266 del self._fdToDurationCall[fd]
267
268 self.debug('[fd %5d] asking streamer to remove client' % fd)
269 self.clientDone(fd)
270
272 """
273 Expire a client's connection associated with the keycard Id.
274 """
275 keycard = self._idToKeycard[keycardId]
276
277 fd = keycard._fd
278
279 self.debug('[fd %5d] expiring client' % fd)
280
281 self._removeKeycard(fd)
282
283 self.debug('[fd %5d] asking streamer to remove client' % fd)
284 self.clientDone(fd)
285
287 """
288 Expire client's connections associated with the keycard Ids.
289 """
290 expired = 0
291 for keycardId in keycardIds:
292 try:
293 self.expireKeycard(keycardId)
294 expired += 1
295 except KeyError, e:
296 self.warn("Failed to expire keycard %r: %s",
297 keycardId, log.getExceptionMessage(e))
298 return expired
299
300
301
309
345
350
363
382
383
385
388
390 """
391 Add an IP filter of the form IP/prefix-length (CIDR syntax), or just
392 a single IP address
393 """
394 definition = filter.split('/')
395 if len(definition) == 2:
396 (net, prefixlen) = definition
397 prefixlen = int(prefixlen)
398 elif len(definition) == 1:
399 net = definition[0]
400 prefixlen = 32
401 else:
402 raise errors.ConfigError(
403 "Cannot parse filter definition %s" % filter)
404
405 if prefixlen < 0 or prefixlen > 32:
406 raise errors.ConfigError("Invalid prefix length")
407
408 mask = ~((1 << (32 - prefixlen)) - 1)
409 try:
410 net = struct.unpack(">I", socket.inet_pton(socket.AF_INET, net))[0]
411 except socket.error:
412 raise errors.ConfigError(
413 "Failed to parse network address %s" % net)
414 net = net & mask
415
416 self.filters.append((net, mask))
417
419 """
420 Return true if ip is in any of the defined network(s) for this filter
421 """
422
423 realip = struct.unpack(">I", socket.inet_pton(socket.AF_INET, ip))[0]
424 for f in self.filters:
425 if (realip & f[1]) == f[0]:
426 return True
427 return False
428