Package flumotion :: Package component :: Package base :: Module http
[hide private]

Source Code for Module flumotion.component.base.http

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_http -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 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 
 29   
 30  from flumotion.configure import configure 
 31  from flumotion.common import errors 
 32   
 33  from flumotion.common import common, log, keycards 
 34   
 35  #__all__ = ['HTTPStreamingResource', 'MultifdSinkStreamer'] 
 36   
 37  HTTP_SERVER_NAME = 'FlumotionHTTPServer' 
 38  HTTP_SERVER_VERSION = configure.version 
 39   
 40  ERROR_TEMPLATE = """<!doctype html public "-//IETF//DTD HTML 2.0//EN"> 
 41  <html> 
 42  <head> 
 43    <title>%(code)d %(error)s</title> 
 44  </head> 
 45  <body> 
 46  <h2>%(code)d %(error)s</h2> 
 47  </body> 
 48  </html> 
 49  """ 
 50   
 51  HTTP_SERVER = '%s/%s' % (HTTP_SERVER_NAME, HTTP_SERVER_VERSION) 
 52   
 53  ### This is new Issuer code that eventually should move to e.g. 
 54  ### flumotion.common.keycards or related 
 55   
56 -class Issuer(log.Loggable):
57 """ 58 I am a base class for all Issuers. 59 An issuer issues keycards of a given class based on an object 60 (incoming HTTP request, ...) 61 """
62 - def issue(self, *args, **kwargs):
63 """ 64 Return a keycard, or None, based on the given arguments. 65 """ 66 raise NotImplementedError
67
68 -class HTTPGenericIssuer(Issuer):
69 """ 70 I create L{flumotion.common.keycards.Keycard} based on just a 71 standard HTTP request. Useful for authenticating based on 72 server-side checks such as time, rather than client credentials. 73 """
74 - def issue(self, request):
75 keycard = keycards.KeycardGeneric() 76 self.debug("Asking for authentication, generic HTTP") 77 return keycard
78
79 -class HTTPAuthIssuer(Issuer):
80 """ 81 I create L{flumotion.common.keycards.KeycardUACPP} keycards based on 82 an incoming L{twisted.protocols.http.Request} request's standard 83 HTTP authentication information. 84 """
85 - def issue(self, request):
86 # for now, we're happy with a UACPP keycard; the password arrives 87 # plaintext anyway 88 keycard = keycards.KeycardUACPP( 89 request.getUser(), 90 request.getPassword(), request.getClientIP()) 91 self.debug('Asking for authentication, user %s, password %s, ip %s' % ( 92 keycard.username, keycard.password, keycard.address)) 93 return keycard
94
95 -class HTTPTokenIssuer(Issuer):
96 """ 97 I create L{flumotion.common.keycards.KeycardToken} keycards based on 98 an incoming L{twisted.protocols.http.Request} request's GET "token" 99 parameter. 100 """
101 - def issue(self, request):
102 if not 'token' in request.args.keys(): 103 return None 104 105 # args can have lists as values, if more than one specified 106 token = request.args['token'] 107 if not isinstance(token, str): 108 token = token[0] 109 110 keycard = keycards.KeycardToken(token, 111 request.getClientIP()) 112 return keycard
113
114 -class HTTPAuthentication(log.Loggable):
115 """ 116 Mixin for handling HTTP authentication for twisted.web Resources, using 117 issuers and bouncers. 118 """ 119 120 logCategory = 'httpauth' 121 122 __reserve_fds__ = 50 # number of fd's to reserve for non-streaming 123
124 - def __init__(self, component):
125 126 self._fdToKeycard = {} # request fd -> Keycard 127 self._idToKeycard = {} # keycard id -> Keycard 128 self._fdToDurationCall = {} # request fd -> IDelayedCall for duration 129 self._domain = None # used for auth challenge and on keycard 130 self._issuer = HTTPAuthIssuer() # issues keycards; default for compat 131 self.bouncerName = None 132 self.requesterId = component.getName() # avatarId of streamer component 133 self._defaultDuration = None # default duration to use if the keycard
134 # doesn't specify one. 135
136 - def setDomain(self, domain):
137 """ 138 Set a domain name on the resource, used in HTTP auth challenges and 139 on the keycard. 140 141 @type domain: string 142 """ 143 self._domain = domain
144
145 - def setBouncerName(self, bouncerName):
146 self.bouncerName = bouncerName
147
148 - def setRequesterId(self, requesterId):
149 self.requesterId = requesterId
150
151 - def setDefaultDuration(self, defaultDuration):
152 self._defaultDuration = defaultDuration
153
154 - def setIssuerClass(self, issuerClass):
155 # FIXME: in the future, we want to make this pluggable and have it 156 # look up somewhere ? 157 if issuerClass == 'HTTPTokenIssuer': 158 self._issuer = HTTPTokenIssuer() 159 elif issuerClass == 'HTTPAuthIssuer': 160 self._issuer = HTTPAuthIssuer() 161 elif issuerClass == 'HTTPGenericIssuer': 162 self._issuer = HTTPGenericIssuer() 163 else: 164 raise ValueError, "issuerClass %s not accepted" % issuerClass
165
166 - def authenticate(self, request):
167 """ 168 Returns: a deferred returning a keycard or None 169 """ 170 keycard = self._issuer.issue(request) 171 if not keycard: 172 self.debug('no keycard from issuer, firing None') 173 return defer.succeed(None) 174 175 keycard.requesterId = self.requesterId 176 keycard._fd = request.transport.fileno() 177 178 if self.bouncerName == None: 179 self.debug('no bouncer, accepting') 180 return defer.succeed(keycard) 181 182 keycard.setDomain(self._domain) 183 self.debug('sending keycard to bouncer %r' % self.bouncerName) 184 185 return self.authenticateKeycard(self.bouncerName, keycard)
186 187 # Must be implemented in subclasses
188 - def authenticateKeycard(self, bouncerName, keycard):
189 pass
190
191 - def cleanupKeycard(self, bouncerName, keycard):
192 pass
193
194 - def clientDone(self, fd):
195 pass
196
197 - def cleanupAuth(self, fd):
198 if self.bouncerName and self._fdToKeycard.has_key(fd): 199 keycard = self._fdToKeycard[fd] 200 del self._fdToKeycard[fd] 201 del self._idToKeycard[keycard.id] 202 self.debug('[fd %5d] asking bouncer %s to remove keycard id %s' % ( 203 fd, self.bouncerName, keycard.id)) 204 self.cleanupKeycard(self.bouncerName, keycard) 205 if self._fdToDurationCall.has_key(fd): 206 self.debug('[fd %5d] canceling later expiration call' % fd) 207 self._fdToDurationCall[fd].cancel() 208 del self._fdToDurationCall[fd]
209
210 - def _durationCallLater(self, fd):
211 """ 212 Expire a client due to a duration expiration. 213 """ 214 self.debug('[fd %5d] duration exceeded, expiring client' % fd) 215 216 # we're called from a callLater, so we've already run; just delete 217 if self._fdToDurationCall.has_key(fd): 218 del self._fdToDurationCall[fd] 219 220 self.debug('[fd %5d] asking streamer to remove client' % fd) 221 self.clientDone(fd)
222
223 - def expireKeycard(self, keycardId):
224 """ 225 Expire a client's connection associated with the keycard Id. 226 """ 227 keycard = self._idToKeycard[keycardId] 228 fd = keycard._fd 229 230 self.debug('[fd %5d] expiring client' % fd) 231 232 if self._fdToDurationCall.has_key(fd): 233 self.debug('[fd %5d] canceling later expiration call' % fd) 234 self._fdToDurationCall[fd].cancel() 235 del self._fdToDurationCall[fd] 236 237 self.debug('[fd %5d] asking streamer to remove client' % fd) 238 self.clientDone(fd)
239 240 ### resource.Resource methods 241
242 - def startAuthentication(self, request):
243 d = self.authenticate(request) 244 d.addCallback(self._authenticatedCallback, request) 245 d.addErrback(self._authenticatedErrback, request) 246 247 return d
248
249 - def _authenticatedCallback(self, keycard, request):
250 # !: since we are a callback, the incoming fd might have gone away 251 # and closed 252 self.debug('_authenticatedCallback: keycard %r' % keycard) 253 if not keycard: 254 raise errors.NotAuthenticatedError() 255 256 # properly authenticated 257 if request.method == 'GET': 258 fd = request.transport.fileno() 259 260 if self.bouncerName: 261 self._fdToKeycard[fd] = keycard 262 self._idToKeycard[keycard.id] = keycard 263 264 duration = keycard.duration or self._defaultDuration 265 266 if duration: 267 self.debug('new connection on %d will expire in %f seconds' % ( 268 fd, duration)) 269 self._fdToDurationCall[fd] = reactor.callLater( 270 duration, self._durationCallLater, fd) 271 272 return None
273
274 - def _authenticatedErrback(self, failure, request):
278
279 - def _handleUnauthorized(self, request):
280 self.debug('client from %s is unauthorized' % (request.getClientIP())) 281 request.setHeader('content-type', 'text/html') 282 request.setHeader('server', HTTP_SERVER_VERSION) 283 if self._domain: 284 request.setHeader('WWW-Authenticate', 285 'Basic realm="%s"' % self._domain) 286 287 error_code = http.UNAUTHORIZED 288 request.setResponseCode(error_code) 289 290 # we have to write data ourselves, 291 # since we already returned NOT_DONE_YET 292 html = ERROR_TEMPLATE % {'code': error_code, 293 'error': http.RESPONSES[error_code]} 294 request.write(html) 295 request.finish()
296
297 -class LogFilter:
298 - def __init__(self):
299 self.filters = [] # list of (network, mask)
300
301 - def addIPFilter(self, filter):
302 """ 303 Add an IP filter of the form IP/prefix-length (CIDR syntax), or just 304 a single IP address 305 """ 306 definition = filter.split('/') 307 if len(definition) == 2: 308 (net, prefixlen) = definition 309 prefixlen = int(prefixlen) 310 elif len(definition) == 1: 311 net = definition[0] 312 prefixlen = 32 313 else: 314 raise errors.ConfigError( 315 "Cannot parse filter definition %s" % filter) 316 317 if prefixlen < 0 or prefixlen > 32: 318 raise errors.ConfigError("Invalid prefix length") 319 320 mask = ~((1 << (32 - prefixlen)) - 1) 321 try: 322 net = struct.unpack(">I", socket.inet_pton(socket.AF_INET, net))[0] 323 except: 324 raise errors.ConfigError("Failed to parse network address %s" % net) 325 net = net & mask # just in case 326 327 self.filters.append((net, mask))
328
329 - def isInRange(self, ip):
330 """ 331 Return true if ip is in any of the defined network(s) for this filter 332 """ 333 # Handles IPv4 only. 334 realip = struct.unpack(">I", socket.inet_pton(socket.AF_INET, ip))[0] 335 for f in self.filters: 336 if (realip & f[1]) == f[0]: 337 return True 338 return False
339