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: 6628 $"
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
60 """
61 I am a base class for all Issuers.
62 An issuer issues keycards of a given class based on an object
63 (incoming HTTP request, ...)
64 """
65 - def issue(self, *args, **kwargs):
66 """
67 Return a keycard, or None, based on the given arguments.
68 """
69 raise NotImplementedError
70
72 """
73 I create L{flumotion.common.keycards.Keycard} based on just a
74 standard HTTP request. Useful for authenticating based on
75 server-side checks such as time, rather than client credentials.
76 """
77 - def issue(self, request):
81
83 """
84 I create L{flumotion.common.keycards.KeycardUACPP} keycards based on
85 an incoming L{twisted.protocols.http.Request} request's standard
86 HTTP authentication information.
87 """
88 - def issue(self, request):
97
99 """
100 I create L{flumotion.common.keycards.KeycardToken} keycards based on
101 an incoming L{twisted.protocols.http.Request} request's GET "token"
102 parameter.
103 """
104 - def issue(self, request):
105 if not 'token' in request.args.keys():
106 return None
107
108
109 token = request.args['token']
110 if not isinstance(token, str):
111 token = token[0]
112
113 keycard = keycards.KeycardToken(token,
114 request.getClientIP(), request.path)
115 return keycard
116
117 BOUNCER_SOCKET = 'flumotion.component.bouncers.plug.BouncerPlug'
118
120 """
121 Helper object for handling HTTP authentication for twisted.web
122 Resources, using issuers and bouncers.
123 """
124
125 logCategory = 'httpauth'
126
127 KEYCARD_TTL = 60 * 60
128 KEYCARD_KEEPALIVE_INTERVAL = 20 * 60
129 KEYCARD_TRYAGAIN_INTERVAL = 1 * 60
130
151
153 def timeout():
154 def reschedule(res):
155 if isinstance(res, failure.Failure):
156 self.info('keepAlive failed, rescheduling in %d '
157 'seconds', self.KEYCARD_TRYAGAIN_INTERVAL)
158 self._keepAlive = None
159 self.scheduleKeepAlive(tryingAgain=True)
160 else:
161 self.info('keepAlive successful')
162 self._keepAlive = None
163 self.scheduleKeepAlive(tryingAgain=False)
164
165 if self.bouncerName is not None:
166 self.debug('calling keepAlive on bouncer %s',
167 self.bouncerName)
168 d = self.keepAlive(self.bouncerName, self.issuerName,
169 self.KEYCARD_TTL)
170 d.addCallbacks(reschedule, reschedule)
171 else:
172 self.scheduleKeepAlive()
173
174 if tryingAgain:
175 self._keepAlive = reactor.callLater(self.KEYCARD_TRYAGAIN_INTERVAL,
176 timeout)
177 else:
178 self._keepAlive = reactor.callLater(self.KEYCARD_KEEPALIVE_INTERVAL,
179 timeout)
180
182 if self._keepAlive is not None:
183 self._keepAlive.cancel()
184 self._keepAlive = None
185
186 - def setDomain(self, domain):
187 """
188 Set a domain name on the resource, used in HTTP auth challenges and
189 on the keycard.
190
191 @type domain: string
192 """
193 self._domain = domain
194
196 self.bouncerName = bouncerName
197
199 self.requesterId = requesterId
200
201 self.issuerName = str(self.requesterId) + '-' + cryptChallenge()
202
204 self._defaultDuration = defaultDuration
205
207
208
209 if issuerClass == 'HTTPTokenIssuer':
210 self._issuer = HTTPTokenIssuer()
211 elif issuerClass == 'HTTPAuthIssuer':
212 self._issuer = HTTPAuthIssuer()
213 elif issuerClass == 'HTTPGenericIssuer':
214 self._issuer = HTTPGenericIssuer()
215 else:
216 raise ValueError, "issuerClass %s not accepted" % issuerClass
217
243
246
247 - def keepAlive(self, bouncerName, issuerName, ttl):
249
252
253
256
258
259
260 def cleanup(bouncerName, keycard):
261 def cleanupLater(res, pair):
262 self.log('failed to clean up keycard %r, will do '
263 'so later', keycard)
264 self._pendingCleanups.append(pair)
265 d = self.cleanupKeycard(bouncerName, keycard)
266 d.addErrback(cleanupLater, (bouncerName, keycard))
267 pending = self._pendingCleanups
268 self._pendingCleanups = []
269 cleanup(bouncerName, keycard)
270 for bouncerName, keycard in pending:
271 cleanup(bouncerName, keycard)
272
273
275 if self.bouncerName and self._fdToKeycard.has_key(fd):
276 keycard = self._fdToKeycard[fd]
277 del self._fdToKeycard[fd]
278 del self._idToKeycard[keycard.id]
279 self.debug('[fd %5d] asking bouncer %s to remove keycard id %s',
280 fd, self.bouncerName, keycard.id)
281 self.doCleanupKeycard(self.bouncerName, keycard)
282 if self._fdToDurationCall.has_key(fd):
283 self.debug('[fd %5d] canceling later expiration call' % fd)
284 self._fdToDurationCall[fd].cancel()
285 del self._fdToDurationCall[fd]
286
288 """
289 Expire a client due to a duration expiration.
290 """
291 self.debug('[fd %5d] duration exceeded, expiring client' % fd)
292
293
294 if self._fdToDurationCall.has_key(fd):
295 del self._fdToDurationCall[fd]
296
297 self.debug('[fd %5d] asking streamer to remove client' % fd)
298 self.clientDone(fd)
299
301 """
302 Expire a client's connection associated with the keycard Id.
303 """
304 keycard = self._idToKeycard[keycardId]
305 fd = keycard._fd
306
307 self.debug('[fd %5d] expiring client' % fd)
308
309 if self._fdToDurationCall.has_key(fd):
310 self.debug('[fd %5d] canceling later expiration call' % fd)
311 self._fdToDurationCall[fd].cancel()
312 del self._fdToDurationCall[fd]
313
314 self.debug('[fd %5d] asking streamer to remove client' % fd)
315 self.clientDone(fd)
316
317
318
326
355
360
368
385
389
391 """
392 Add an IP filter of the form IP/prefix-length (CIDR syntax), or just
393 a single IP address
394 """
395 definition = filter.split('/')
396 if len(definition) == 2:
397 (net, prefixlen) = definition
398 prefixlen = int(prefixlen)
399 elif len(definition) == 1:
400 net = definition[0]
401 prefixlen = 32
402 else:
403 raise errors.ConfigError(
404 "Cannot parse filter definition %s" % filter)
405
406 if prefixlen < 0 or prefixlen > 32:
407 raise errors.ConfigError("Invalid prefix length")
408
409 mask = ~((1 << (32 - prefixlen)) - 1)
410 try:
411 net = struct.unpack(">I", socket.inet_pton(socket.AF_INET, net))[0]
412 except socket.error:
413 raise errors.ConfigError("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