Package openid :: Package store :: Module filestore
[frames] | no frames]

Source Code for Module openid.store.filestore

  1  """ 
  2  This module contains an C{L{OpenIDStore}} implementation backed by 
  3  flat files. 
  4  """ 
  5   
  6  import string 
  7  import os 
  8  import os.path 
  9  import sys 
 10  import time 
 11   
 12  from errno import EEXIST, ENOENT 
 13   
 14  try: 
 15      from tempfile import mkstemp 
 16  except ImportError: 
 17      # Python < 2.3 
 18      import tempfile 
 19      import warnings 
 20      warnings.filterwarnings("ignore", 
 21                              "tempnam is a potential security risk", 
 22                              RuntimeWarning, 
 23                              "openid.store.filestore") 
 24   
25 - def mkstemp(dir):
26 for _ in range(5): 27 name = os.tempnam(dir) 28 try: 29 fd = os.open(name, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0600) 30 except OSError, why: 31 if why[0] != EEXIST: 32 raise 33 else: 34 return fd, name 35 36 raise RuntimeError('Failed to get temp file after 5 attempts')
37 38 from openid.association import Association 39 from openid.store.interface import OpenIDStore 40 from openid import cryptutil, oidutil 41 42 _filename_allowed = string.ascii_letters + string.digits + '.' 43 try: 44 # 2.4 45 set 46 except NameError: 47 try: 48 # 2.3 49 import sets 50 except ImportError: 51 # Python < 2.2 52 d = {} 53 for c in _filename_allowed: 54 d[c] = None 55 _isFilenameSafe = d.has_key 56 del d 57 else: 58 _isFilenameSafe = sets.Set(_filename_allowed).__contains__ 59 else: 60 _isFilenameSafe = set(_filename_allowed).__contains__ 61
62 -def _safe64(s):
63 h64 = oidutil.toBase64(cryptutil.sha1(s)) 64 h64 = h64.replace('+', '_') 65 h64 = h64.replace('/', '.') 66 h64 = h64.replace('=', '') 67 return h64
68
69 -def _filenameEscape(s):
70 filename_chunks = [] 71 for c in s: 72 if _isFilenameSafe(c): 73 filename_chunks.append(c) 74 else: 75 filename_chunks.append('_%02X' % ord(c)) 76 return ''.join(filename_chunks)
77
78 -def _removeIfPresent(filename):
79 """Attempt to remove a file, returning whether the file existed at 80 the time of the call. 81 82 str -> bool 83 """ 84 try: 85 os.unlink(filename) 86 except OSError, why: 87 if why[0] == ENOENT: 88 # Someone beat us to it, but it's gone, so that's OK 89 return 0 90 else: 91 raise 92 else: 93 # File was present 94 return 1
95
96 -def _ensureDir(dir_name):
97 """Create dir_name as a directory if it does not exist. If it 98 exists, make sure that it is, in fact, a directory. 99 100 Can raise OSError 101 102 str -> NoneType 103 """ 104 try: 105 os.makedirs(dir_name) 106 except OSError, why: 107 if why[0] != EEXIST or not os.path.isdir(dir_name): 108 raise
109
110 -class FileOpenIDStore(OpenIDStore):
111 """ 112 This is a filesystem-based store for OpenID associations and 113 nonces. This store should be safe for use in concurrent systems 114 on both windows and unix (excluding NFS filesystems). There are a 115 couple race conditions in the system, but those failure cases have 116 been set up in such a way that the worst-case behavior is someone 117 having to try to log in a second time. 118 119 Most of the methods of this class are implementation details. 120 People wishing to just use this store need only pay attention to 121 the C{L{__init__}} method. 122 123 Methods of this object can raise OSError if unexpected filesystem 124 conditions, such as bad permissions or missing directories, occur. 125 """ 126
127 - def __init__(self, directory):
128 """ 129 Initializes a new FileOpenIDStore. This initializes the 130 nonce and association directories, which are subdirectories of 131 the directory passed in. 132 133 @param directory: This is the directory to put the store 134 directories in. 135 136 @type directory: C{str} 137 """ 138 # Make absolute 139 directory = os.path.normpath(os.path.abspath(directory)) 140 141 self.nonce_dir = os.path.join(directory, 'nonces') 142 143 self.association_dir = os.path.join(directory, 'associations') 144 145 # Temp dir must be on the same filesystem as the assciations 146 # directory and the directory containing the auth key file. 147 self.temp_dir = os.path.join(directory, 'temp') 148 149 self.auth_key_name = os.path.join(directory, 'auth_key') 150 151 self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds 152 153 self._setup()
154
155 - def _setup(self):
156 """Make sure that the directories in which we store our data 157 exist. 158 159 () -> NoneType 160 """ 161 _ensureDir(os.path.dirname(self.auth_key_name)) 162 _ensureDir(self.nonce_dir) 163 _ensureDir(self.association_dir) 164 _ensureDir(self.temp_dir)
165
166 - def _mktemp(self):
167 """Create a temporary file on the same filesystem as 168 self.auth_key_name and self.association_dir. 169 170 The temporary directory should not be cleaned if there are any 171 processes using the store. If there is no active process using 172 the store, it is safe to remove all of the files in the 173 temporary directory. 174 175 () -> (file, str) 176 """ 177 fd, name = mkstemp(dir=self.temp_dir) 178 try: 179 file_obj = os.fdopen(fd, 'wb') 180 return file_obj, name 181 except: 182 _removeIfPresent(name) 183 raise
184
185 - def readAuthKey(self):
186 """Read the auth key from the auth key file. Will return None 187 if there is currently no key. 188 189 () -> str or NoneType 190 """ 191 try: 192 auth_key_file = file(self.auth_key_name, 'rb') 193 except IOError, why: 194 if why[0] == ENOENT: 195 return None 196 else: 197 raise 198 199 try: 200 return auth_key_file.read() 201 finally: 202 auth_key_file.close()
203
204 - def createAuthKey(self):
205 """Generate a new random auth key and safely store it in the 206 location specified by self.auth_key_name. 207 208 () -> str""" 209 210 # Do the import here because this should only get called at 211 # most once from each process. Once the auth key file is 212 # created, this should not get called at all. 213 auth_key = cryptutil.randomString(self.AUTH_KEY_LEN) 214 215 file_obj, tmp = self._mktemp() 216 try: 217 file_obj.write(auth_key) 218 # Must close the file before linking or renaming it on win32. 219 file_obj.close() 220 221 try: 222 if hasattr(os, 'link') and sys.platform != 'cygwin': 223 # because os.link works in some cygwin environments, 224 # but returns errno 17 on others. Haven't figured out 225 # how to predict when it will do that yet. 226 os.link(tmp, self.auth_key_name) 227 else: 228 os.rename(tmp, self.auth_key_name) 229 except OSError, why: 230 if why[0] == EEXIST: 231 auth_key = self.readAuthKey() 232 if auth_key is None: 233 # This should only happen if someone deletes 234 # the auth key file out from under us. 235 raise 236 else: 237 raise 238 finally: 239 file_obj.close() 240 _removeIfPresent(tmp) 241 242 return auth_key
243
244 - def getAuthKey(self):
245 """Retrieve the auth key from the file specified by 246 self.auth_key_name, creating it if it does not exist. 247 248 () -> str 249 """ 250 auth_key = self.readAuthKey() 251 if auth_key is None: 252 auth_key = self.createAuthKey() 253 254 if len(auth_key) != self.AUTH_KEY_LEN: 255 fmt = ('Got an invalid auth key from %s. Expected %d byte ' 256 'string. Got: %r') 257 msg = fmt % (self.auth_key_name, self.AUTH_KEY_LEN, auth_key) 258 raise ValueError(msg) 259 260 return auth_key
261
262 - def getAssociationFilename(self, server_url, handle):
263 """Create a unique filename for a given server url and 264 handle. This implementation does not assume anything about the 265 format of the handle. The filename that is returned will 266 contain the domain name from the server URL for ease of human 267 inspection of the data directory. 268 269 (str, str) -> str 270 """ 271 if server_url.find('://') == -1: 272 raise ValueError('Bad server URL: %r' % server_url) 273 274 proto, rest = server_url.split('://', 1) 275 domain = _filenameEscape(rest.split('/', 1)[0]) 276 url_hash = _safe64(server_url) 277 if handle: 278 handle_hash = _safe64(handle) 279 else: 280 handle_hash = '' 281 282 filename = '%s-%s-%s-%s' % (proto, domain, url_hash, handle_hash) 283 284 return os.path.join(self.association_dir, filename)
285
286 - def storeAssociation(self, server_url, association):
287 """Store an association in the association directory. 288 289 (str, Association) -> NoneType 290 """ 291 association_s = association.serialize() 292 filename = self.getAssociationFilename(server_url, association.handle) 293 tmp_file, tmp = self._mktemp() 294 295 try: 296 try: 297 tmp_file.write(association_s) 298 os.fsync(tmp_file.fileno()) 299 finally: 300 tmp_file.close() 301 302 try: 303 os.rename(tmp, filename) 304 except OSError, why: 305 if why[0] != EEXIST: 306 raise 307 308 # We only expect EEXIST to happen only on Windows. It's 309 # possible that we will succeed in unlinking the existing 310 # file, but not in putting the temporary file in place. 311 try: 312 os.unlink(filename) 313 except OSError, why: 314 if why[0] == ENOENT: 315 pass 316 else: 317 raise 318 319 # Now the target should not exist. Try renaming again, 320 # giving up if it fails. 321 os.rename(tmp, filename) 322 except: 323 # If there was an error, don't leave the temporary file 324 # around. 325 _removeIfPresent(tmp) 326 raise
327
328 - def getAssociation(self, server_url, handle=None):
329 """Retrieve an association. If no handle is specified, return 330 the association with the latest expiration. 331 332 (str, str or NoneType) -> Association or NoneType 333 """ 334 if handle is None: 335 handle = '' 336 337 # The filename with the empty handle is a prefix of all other 338 # associations for the given server URL. 339 filename = self.getAssociationFilename(server_url, handle) 340 341 if handle: 342 return self._getAssociation(filename) 343 else: 344 association_files = os.listdir(self.association_dir) 345 matching_files = [] 346 # strip off the path to do the comparison 347 name = os.path.basename(filename) 348 for association_file in association_files: 349 if association_file.startswith(name): 350 matching_files.append(association_file) 351 352 matching_associations = [] 353 # read the matching files and sort by time issued 354 for name in matching_files: 355 full_name = os.path.join(self.association_dir, name) 356 association = self._getAssociation(full_name) 357 if association is not None: 358 matching_associations.append( 359 (association.issued, association)) 360 361 matching_associations.sort() 362 363 # return the most recently issued one. 364 if matching_associations: 365 (_, assoc) = matching_associations[-1] 366 return assoc 367 else: 368 return None
369
370 - def _getAssociation(self, filename):
371 try: 372 assoc_file = file(filename, 'rb') 373 except IOError, why: 374 if why[0] == ENOENT: 375 # No association exists for that URL and handle 376 return None 377 else: 378 raise 379 else: 380 try: 381 assoc_s = assoc_file.read() 382 finally: 383 assoc_file.close() 384 385 try: 386 association = Association.deserialize(assoc_s) 387 except ValueError: 388 _removeIfPresent(filename) 389 return None 390 391 # Clean up expired associations 392 if association.getExpiresIn() == 0: 393 _removeIfPresent(filename) 394 return None 395 else: 396 return association
397
398 - def removeAssociation(self, server_url, handle):
399 """Remove an association if it exists. Do nothing if it does not. 400 401 (str, str) -> bool 402 """ 403 assoc = self.getAssociation(server_url, handle) 404 if assoc is None: 405 return 0 406 else: 407 filename = self.getAssociationFilename(server_url, handle) 408 return _removeIfPresent(filename)
409
410 - def storeNonce(self, nonce):
411 """Mark this nonce as present. 412 413 str -> NoneType 414 """ 415 filename = os.path.join(self.nonce_dir, nonce) 416 nonce_file = file(filename, 'w') 417 nonce_file.close()
418
419 - def useNonce(self, nonce):
420 """Return whether this nonce is present. As a side effect, 421 mark it as no longer present. 422 423 str -> bool 424 """ 425 filename = os.path.join(self.nonce_dir, nonce) 426 try: 427 st = os.stat(filename) 428 except OSError, why: 429 if why[0] == ENOENT: 430 # File was not present, so nonce is no good 431 return 0 432 else: 433 raise 434 else: 435 # Either it is too old or we are using it. Either way, we 436 # must remove the file. 437 try: 438 os.unlink(filename) 439 except OSError, why: 440 if why[0] == ENOENT: 441 # someone beat us to it, so we cannot use this 442 # nonce anymore. 443 return 0 444 else: 445 raise 446 447 now = time.time() 448 nonce_age = now - st.st_mtime 449 450 # We can us it if the age of the file is less than the 451 # expiration time. 452 return nonce_age <= self.max_nonce_age
453
454 - def clean(self):
455 """Remove expired entries from the database. This is 456 potentially expensive, so only run when it is acceptable to 457 take time. 458 459 () -> NoneType 460 """ 461 nonces = os.listdir(self.nonce_dir) 462 now = time.time() 463 464 # Check all nonces for expiry 465 for nonce in nonces: 466 filename = os.path.join(self.nonce_dir, nonce) 467 try: 468 st = os.stat(filename) 469 except OSError, why: 470 if why[0] == ENOENT: 471 # The file did not exist by the time we tried to 472 # stat it. 473 pass 474 else: 475 raise 476 else: 477 # Remove the nonce if it has expired 478 nonce_age = now - st.st_mtime 479 if nonce_age > self.max_nonce_age: 480 _removeIfPresent(filename) 481 482 association_filenames = os.listdir(self.association_dir) 483 for association_filename in association_filenames: 484 try: 485 association_file = file(association_filename, 'rb') 486 except IOError, why: 487 if why[0] == ENOENT: 488 pass 489 else: 490 raise 491 else: 492 try: 493 assoc_s = association_file.read() 494 finally: 495 association_file.close() 496 497 # Remove expired or corrupted associations 498 try: 499 association = Association.deserialize(assoc_s) 500 except ValueError: 501 _removeIfPresent(association_filename) 502 else: 503 if association.getExpiresIn() == 0: 504 _removeIfPresent(association_filename)
505