Package wsgitools :: Module digest
[hide private]
[frames] | no frames]

Source Code for Module wsgitools.digest

  1  #!/usr/bin/env python2.5 
  2   
  3  __all__ = [] 
  4   
  5  import random 
  6  try: 
  7      from hashlib import md5 
  8  except ImportError: 
  9      from md5 import md5 
 10  import binascii 
 11  import base64 
 12  import time 
 13   
 14  sysrand = random.SystemRandom() 
 15   
16 -def gen_rand_str(bytes=33):
17 """ 18 Generates a string of random base64 characters. 19 @param bytes: is the number of random 8bit values to be used 20 21 >>> gen_rand_str() != gen_rand_str() 22 True 23 """ 24 randnum = sysrand.getrandbits(bytes*8) 25 randstr = ("%%0%dX" % (2*bytes)) % randnum 26 randstr = binascii.unhexlify(randstr) 27 randstr = base64.encodestring(randstr).strip() 28 return randstr
29
30 -def parse_digest_response(data, ret=None):
31 """internal 32 @raises ValueError: 33 34 >>> parse_digest_response('foo=bar') 35 {'foo': 'bar'} 36 >>> parse_digest_response('foo="bar"') 37 {'foo': 'bar'} 38 >>> sorted(parse_digest_response('foo="bar=qux",spam=egg').items()) 39 [('foo', 'bar=qux'), ('spam', 'egg')] 40 >>> try: 41 ... parse_digest_response('spam') 42 ... except ValueError: 43 ... print("ValueError") 44 ValueError 45 >>> try: 46 ... parse_digest_response('spam="egg"error') 47 ... except ValueError: 48 ... print("ValueError") 49 ValueError 50 """ 51 if ret is None: 52 ret = {} 53 data = data.strip() 54 key, rest = data.split('=', 1) # raises ValueError 55 if rest.startswith('"'): 56 rest = rest[1:] 57 value, rest = rest.split('"', 1) # raises ValueError 58 if not rest: 59 ret[key] = value 60 return ret 61 if rest[0] != ',': 62 raise ValueError("invalid digest response") 63 rest = rest[1:] 64 else: 65 if ',' not in rest: 66 ret[key] = rest 67 return ret 68 value, rest = rest.split(',' , 1) 69 ret[key] = value 70 return parse_digest_response(rest, ret)
71
72 -class AuthenticationRequired(Exception):
73 """ 74 Internal Exception class that is thrown inside L{AuthDigestMiddleware}, but 75 not visible to other code. 76 """
77 78 __all__.append("AuthTokenGenerator")
79 -class AuthTokenGenerator:
80 """Generates authentification tokens for L{AuthDigestMiddleware}. The 81 interface consists of beeing callable with a username and having a 82 realm attribute being a string."""
83 - def __init__(self, realm, getpass):
84 """ 85 @type realm: str 86 @param realm: is a string according to RFC2617. 87 @type getpass: str -> str 88 @param getpass: this function is called with a username and password is 89 expected as result. C{None} may be used as an invalid password. 90 """ 91 self.realm = realm 92 self.getpass = getpass
93 - def __call__(self, username, algo="md5"):
94 """Generates an authentification token from a username. 95 @type username: str 96 @rtype: str or None 97 """ 98 assert algo.lower() in ["md5", "md5-sess"] 99 password = self.getpass(username) 100 if password is None: 101 return None 102 a1 = "%s:%s:%s" % (username, self.realm, password) 103 return md5(a1).hexdigest()
104 105 __all__.append("NonceStoreBase")
106 -class NonceStoreBase:
107 """Nonce storage interface."""
108 - def __init__(self):
109 pass
110 - def newnonce(self, ident=None):
111 """ 112 This method is to be overriden and should return new nonces. 113 @type ident: str 114 @param ident: is an identifier to be associated with this nonce 115 @rtype: str 116 """ 117 raise NotImplementedError
118 - def checknonce(self, nonce, count=1, ident=None):
119 """ 120 This method is to be overridden and should do a check for whether the 121 given nonce is valid as being used count times. 122 @type nonce: str 123 @type count: int 124 @param count: indicates how often the nonce has been used (including 125 this check) 126 @type ident: str 127 @param ident: it is also checked that the nonce was associated to this 128 identifier when given 129 @rtype: bool 130 """ 131 raise NotImplementedError
132
133 -def format_time(seconds):
134 """ 135 internal method formatting a unix time to a fixed-length string 136 @type seconds: float 137 @rtype: str 138 """ 139 # the overflow will happen about 2112 140 return "%013X" % int(seconds * 1000000)
141 142 __all__.append("StatelessNonceStore")
143 -class StatelessNonceStore(NonceStoreBase):
144 """ 145 This is a stateless nonce storage that cannot check the usage count for 146 a nonce and thus cannot protect against replay attacks. It however can make 147 it difficult by posing a timeout on nonces and making it difficult to forge 148 nonces. 149 150 This nonce store is usable with L{scgi.forkpool}. 151 152 >>> s = StatelessNonceStore() 153 >>> n = s.newnonce() 154 >>> s.checknonce("spam") 155 False 156 >>> s.checknonce(n) 157 True 158 >>> s.checknonce(n) 159 True 160 >>> s.checknonce(n.rsplit(':', 1)[0] + "bad hash") 161 False 162 """
163 - def __init__(self, maxage=300, secret=None):
164 """ 165 @type maxage: int 166 @param maxage: is the number of seconds a nonce may be valid. Choosing a 167 large value may result in more memory usage whereas a smaller 168 value results in more requests. Defaults to 5 minutes. 169 @type secret: str 170 @param secret: if not given, a secret is generated and is therefore 171 shared after forks. Knowing this secret permits creating nonces. 172 """ 173 NonceStoreBase.__init__(self) 174 self.maxage = maxage 175 if secret: 176 self.server_secret = secret 177 else: 178 self.server_secret = gen_rand_str()
179
180 - def newnonce(self, ident=None):
181 """ 182 Generates a new nonce string. 183 @type ident: None or str 184 @rtype: str 185 """ 186 nonce_time = format_time(time.time()) 187 nonce_value = gen_rand_str() 188 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 189 if ident is not None: 190 token = "%s:%s" % (token, ident) 191 token = md5(token).hexdigest() 192 return "%s:%s:%s" % (nonce_time, nonce_value, token)
193
194 - def checknonce(self, nonce, count=1, ident=None):
195 """ 196 Do a check for whether the provided string is a nonce and increase usage 197 count on returning True. 198 @type nonce: str 199 @type count: int 200 @type ident: None or str 201 @rtype: bool 202 """ 203 if count != 1: 204 return False 205 try: 206 nonce_time, nonce_value, nonce_hash = nonce.split(':') 207 except ValueError: 208 return False 209 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 210 if ident is not None: 211 token = "%s:%s" % (token, ident) 212 token = md5(token).hexdigest() 213 if token != nonce_hash: 214 return False 215 216 if nonce_time < format_time(time.time() - self.maxage): 217 return False 218 return True
219 220 __all__.append("MemoryNonceStore")
221 -class MemoryNonceStore(NonceStoreBase):
222 """ 223 Simple in-memory mechanism to store nonces. 224 225 >>> s = MemoryNonceStore(maxuses=1) 226 >>> n = s.newnonce() 227 >>> s.checknonce("spam") 228 False 229 >>> s.checknonce(n) 230 True 231 >>> s.checknonce(n) 232 False 233 """
234 - def __init__(self, maxage=300, maxuses=5):
235 """ 236 @type maxage: int 237 @param maxage: is the number of seconds a nonce may be valid. Choosing a 238 large value may result in more memory usage whereas a smaller 239 value results in more requests. Defaults to 5 minutes. 240 @type maxuses: int 241 @param maxuses: is the number of times a nonce may be used (with 242 different nc values). A value of 1 makes nonces usable exactly 243 once resulting in more requests. Defaults to 5. 244 """ 245 NonceStoreBase.__init__(self) 246 self.maxage = maxage 247 self.maxuses = maxuses 248 self.nonces = [] # [(creation_time, nonce_value, useage_count)] 249 # as [(float, str, int)] 250 self.server_secret = gen_rand_str()
251
252 - def _cleanup(self):
253 """internal methods cleaning list of valid nonces""" 254 old = format_time(time.time() - self.maxage) 255 while self.nonces and self.nonces[0][0] < old: 256 self.nonces.pop(0)
257
258 - def newnonce(self, ident=None):
259 """ 260 Generates a new nonce string. 261 @type ident: None or str 262 @rtype: str 263 """ 264 self._cleanup() # avoid growing self.nonces 265 nonce_time = format_time(time.time()) 266 nonce_value = gen_rand_str() 267 self.nonces.append((nonce_time, nonce_value, 1)) 268 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 269 if ident is not None: 270 token = "%s:%s" % (token, ident) 271 token = md5(token).hexdigest() 272 return "%s:%s:%s" % (nonce_time, nonce_value, token)
273
274 - def checknonce(self, nonce, count=1, ident=None):
275 """ 276 Do a check for whether the provided string is a nonce and increase usage 277 count on returning True. 278 @type nonce: str 279 @type count: int 280 @type ident: None or str 281 @rtype: bool 282 """ 283 try: 284 nonce_time, nonce_value, nonce_hash = nonce.split(':') 285 except ValueError: 286 return False 287 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 288 if ident is not None: 289 token = "%s:%s" % (token, ident) 290 token = md5(token).hexdigest() 291 if token != nonce_hash: 292 return False 293 294 self._cleanup() # avoid stale nonces 295 296 # searching nonce_time 297 lower, upper = 0, len(self.nonces) - 1 298 while lower < upper: 299 mid = (lower + upper) // 2 300 if nonce_time <= self.nonces[mid][0]: 301 upper = mid 302 else: 303 lower = mid + 1 304 305 if len(self.nonces) <= lower: 306 return False 307 (nt, nv, uses) = self.nonces[lower] 308 if nt != nonce_time or nv != nonce_value: 309 return False 310 if count != uses: 311 del self.nonces[lower] 312 return False 313 if uses >= self.maxuses: 314 del self.nonces[lower] 315 else: 316 self.nonces[lower] = (nt, nv, uses+1) 317 return True
318 319 __all__.append("LazyDBAPI2Opener")
320 -class LazyDBAPI2Opener:
321 """ 322 Connects to database on first request. Otherwise it behaves like a dbapi2 323 connection. This may be usefull in combination with scgi.forkpool, because 324 this way each worker child opens a new database connection when the first 325 request is to be answered. 326 """
327 - def __init__(self, function, *args, **kwargs):
328 """ 329 The database will be connected on the first method call. This is done 330 by calling the given function with the remaining parameters. 331 @param function: is the function that connects to the database 332 """ 333 self._function = function 334 self._args = args 335 self._kwargs = kwargs 336 self._dbhandle = None
337 - def _getdbhandle(self):
338 """Returns an open database connection. Open if necessary.""" 339 if self._dbhandle is None: 340 self._dbhandle = self._function(*self._args, **self._kwargs) 341 self._function = self._args = self._kwargs = None 342 return self._dbhandle
343 - def cursor(self):
344 """dbapi2""" 345 return self._getdbhandle().cursor()
346 - def commit(self):
347 """dbapi2""" 348 return self._getdbhandle().commit()
349 - def rollback(self):
350 """dbapi2""" 351 return self._getdbhandle().rollback()
352 - def close(self):
353 """dbapi2""" 354 return self._getdbhandle().close()
355 356 __all__.append("DBAPI2NonceStore")
357 -class DBAPI2NonceStore(NonceStoreBase):
358 """ 359 A dbapi2-backed nonce store implementation suitable for usage with forking 360 wsgi servers such as scgi.forkpool. 361 >>> import sqlite3 362 >>> db = sqlite3.connect(":memory:") 363 >>> db.cursor().execute("CREATE TABLE nonces (key, value);") and None 364 >>> db.commit() and None 365 >>> s = DBAPI2NonceStore(db, maxuses=1) 366 >>> n = s.newnonce() 367 >>> s.checknonce("spam") 368 False 369 >>> s.checknonce(n) 370 True 371 >>> s.checknonce(n) 372 False 373 """
374 - def __init__(self, dbhandle, maxage=300, maxuses=5, table="nonces"):
375 """ 376 @param dbhandle: is a dbapi2 connection 377 @type maxage: int 378 @param maxage: is the number of seconds a nonce may be valid. Choosing a 379 large value may result in more memory usage whereas a smaller 380 value results in more requests. Defaults to 5 minutes. 381 @type maxuses: int 382 @param maxuses: is the number of times a nonce may be used (with 383 different nc values). A value of 1 makes nonces usable exactly 384 once resulting in more requests. Defaults to 5. 385 """ 386 NonceStoreBase.__init__(self) 387 self.dbhandle = dbhandle 388 self.maxage = maxage 389 self.maxuses = maxuses 390 self.table = table 391 self.server_secret = gen_rand_str()
392
393 - def _cleanup(self, cur):
394 """internal methods cleaning list of valid nonces""" 395 old = format_time(time.time() - self.maxage) 396 cur.execute("DELETE FROM %s WHERE key < '%s:';" % (self.table, old))
397
398 - def newnonce(self, ident=None):
399 """ 400 Generates a new nonce string. 401 @rtype: str 402 """ 403 nonce_time = format_time(time.time()) 404 nonce_value = gen_rand_str() 405 dbkey = "%s:%s" % (nonce_time, nonce_value) 406 cur = self.dbhandle.cursor() 407 self._cleanup(cur) # avoid growing database 408 cur.execute("INSERT INTO %s VALUES ('%s', '1');" % (self.table, dbkey)) 409 self.dbhandle.commit() 410 token = "%s:%s" % (dbkey, self.server_secret) 411 if ident is not None: 412 token = "%s:%s" % (token, ident) 413 token = md5(token).hexdigest() 414 return "%s:%s:%s" % (nonce_time, nonce_value, token)
415
416 - def checknonce(self, nonce, count=1, ident=None):
417 """ 418 Do a check for whether the provided string is a nonce and increase usage 419 count on returning True. 420 @type nonce: str 421 @type count: int 422 @rtype: bool 423 """ 424 try: 425 nonce_time, nonce_value, nonce_hash = nonce.split(':') 426 except ValueError: 427 return False 428 if not nonce_time.isalnum() or not nonce_value.replace("+", ""). \ 429 replace("/", "").replace("=", "").isalnum(): 430 return False 431 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 432 if ident is not None: 433 token = "%s:%s" % (token, ident) 434 token = md5(token).hexdigest() 435 if token != nonce_hash: 436 return False 437 438 if nonce_time < format_time(time.time() - self.maxage): 439 return False 440 441 cur = self.dbhandle.cursor() 442 #self._cleanup(cur) # avoid growing database 443 444 dbkey = "%s:%s" % (nonce_time, nonce_value) 445 cur.execute("SELECT value FROM %s WHERE key = '%s';" % 446 (self.table, dbkey)) 447 uses = cur.fetchone() 448 if uses is None: 449 self.dbhandle.commit() 450 return False 451 uses = int(uses[0]) 452 if count != uses: 453 cur.execute("DELETE FROM %s WHERE key = '%s';" % 454 (self.table, dbkey)) 455 self.dbhandle.commit() 456 return False 457 if uses >= self.maxuses: 458 cur.execute("DELETE FROM %s WHERE key = '%s';" % 459 (self.table, dbkey)) 460 else: 461 cur.execute("UPDATE %s SET value = '%d' WHERE key = '%s';" % 462 (self.table, uses + 1, dbkey)) 463 self.dbhandle.commit() 464 return True
465 466 __all__.append("AuthDigestMiddleware")
467 -class AuthDigestMiddleware:
468 """Middleware partly implementing RFC2617. (md5-sess was omited)""" 469 algorithms = {"md5": lambda data: md5(data).hexdigest()}
470 - def __init__(self, app, gentoken, maxage=300, maxuses=5, store=None):
471 """ 472 @param app: is the wsgi application to be served with authentification. 473 @type gentoken: str -> (str or None) 474 @param gentoken: has to have the same functionality and interface as the 475 L{AuthTokenGenerator} class. 476 @type maxage: int 477 @param maxage: deprecated, see L{MemoryNonceStore} or 478 L{StatelessNonceStore} 479 @type maxuses: int 480 @param maxuses: deprecated, see L{MemoryNonceStore} 481 @type store: L{NonceStoreBase} 482 @param store: a nonce storage implementation object. Usage of this 483 parameter will override maxage and maxuses. 484 """ 485 self.app = app 486 self.gentoken = gentoken 487 if store is None: 488 self.noncestore = MemoryNonceStore(maxage, maxuses) 489 else: 490 assert hasattr(store, "newnonce") 491 assert hasattr(store, "checknonce") 492 self.noncestore = store
493
494 - def __call__(self, environ, start_response):
495 """wsgi interface""" 496 497 try: 498 auth = environ["HTTP_AUTHORIZATION"] # raises KeyError 499 method, rest = auth.split(' ', 1) # raises ValueError 500 501 if method.lower() != "digest": 502 raise AuthenticationRequired 503 credentials = parse_digest_response(rest) # raises ValueError 504 505 ### Check algorithm field 506 credentials["algorithm"] = credentials.get("algorithm", 507 "md5").lower() 508 if not credentials["algorithm"] in self.algorithms: 509 raise AuthenticationRequired 510 511 ### Check uri field 512 # Doing this by stripping known parts from the passed uri field 513 # until something trivial remains, as the uri cannot be 514 # reconstructed from the environment exactly. 515 uri = credentials["uri"] # raises KeyError 516 if "QUERY_STRING" in environ and environ["QUERY_STRING"]: 517 if not uri.endswith(environ["QUERY_STRING"]): 518 raise AuthenticationRequired 519 uri = uri[:-len(environ["QUERY_STRING"])] 520 if "SCRIPT_NAME" in environ: 521 if not uri.startswith(environ["SCRIPT_NAME"]): 522 raise AuthenticationRequired 523 uri = uri[len(environ["SCRIPT_NAME"]):] 524 if "PATH_INFO" in environ: 525 if not uri.startswith(environ["PATH_INFO"]): 526 raise AuthenticationRequired 527 uri = uri[len(environ["PATH_INFO"]):] 528 if uri not in ('', '?'): 529 raise AuthenticationRequired 530 del uri 531 532 if ("username" not in credentials or 533 "nonce" not in credentials or 534 "response" not in credentials or 535 "qop" in credentials and ( 536 credentials["qop"] != "auth" or 537 "nc" not in credentials or 538 credentials["nc"].lower().strip("0123456789abcdef") or 539 "cnonce" not in credentials)): 540 raise AuthenticationRequired 541 542 noncecount = 1 543 if credentials.get("qop") is not None: 544 # raises ValueError 545 noncecount = int(credentials["nc"], 16) 546 547 if not self.noncestore.checknonce(credentials["nonce"], noncecount): 548 return self.authorization_required(environ, start_response, 549 stale=True) # stale nonce! 550 551 # raises KeyError, ValueError 552 response = self.auth_response(credentials, 553 environ["REQUEST_METHOD"]) 554 555 if response != credentials["response"]: 556 raise AuthenticationRequired 557 558 except (KeyError, ValueError, AuthenticationRequired): 559 return self.authorization_required(environ, start_response) 560 else: 561 environ["REMOTE_USER"] = credentials["username"] 562 def modified_start_response(status, headers, exc_info=None): 563 digest = dict(nextnonce=self.noncestore.newnonce()) 564 if "qop" in credentials: 565 digest["qop"] = "auth" 566 digest["cnonce"] = credentials["cnonce"] # no KeyError 567 digest["rspauth"] = self.auth_response(credentials, "") 568 challenge = ", ".join(map('%s="%s"'.__mod__, digest.items())) 569 headers.append(("Authentication-Info", challenge)) 570 return start_response(status, headers, exc_info)
571 return self.app(environ, modified_start_response)
572
573 - def auth_response(self, credentials, reqmethod):
574 """internal method generating authentication tokens 575 @raise KeyError: 576 @raise ValueError: 577 """ 578 username = credentials["username"] 579 algo = credentials["algorithm"] 580 uri = credentials["uri"] 581 nonce = credentials["nonce"] 582 a1h = self.gentoken(username, algo) 583 if a1h is None: 584 raise ValueError 585 a2h = self.algorithms[algo]("%s:%s" % (reqmethod, uri)) 586 qop = credentials.get("qop", None) 587 if qop is None: 588 dig = ":".join((a1h, nonce, a2h)) 589 else: 590 if qop != "auth": 591 raise ValueError 592 # raises KeyError 593 dig = ":".join((a1h, nonce, credentials["nc"], 594 credentials["cnonce"], qop, a2h)) 595 return self.algorithms[algo](dig)
596
597 - def authorization_required(self, environ, start_response, stale=False):
598 """internal method implementing wsgi interface, serving 401 page""" 599 digest = dict(nonce=self.noncestore.newnonce(), 600 realm=self.gentoken.realm, 601 algorithm="md5", 602 qop="auth") 603 if stale: 604 digest["stale"] = "TRUE" 605 challenge = ", ".join(map('%s="%s"'.__mod__, digest.items())) 606 status = "401 Not authorized" 607 headers = [("Content-type", "text/html"), 608 ("WWW-Authenticate", "Digest %s" % challenge)] 609 data = "<html><head><title>401 Not authorized</title></head><body><h1>" 610 data += "401 Not authorized</h1></body></html>" 611 headers.append(("Content-length", str(len(data)))) 612 start_response(status, headers) 613 if environ["REQUEST_METHOD"] == "HEAD": 614 return [] 615 return [data]
616