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
18 import tempfile
19 import warnings
20 warnings.filterwarnings("ignore",
21 "tempnam is a potential security risk",
22 RuntimeWarning,
23 "openid.store.filestore")
24
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
45 set
46 except NameError:
47 try:
48
49 import sets
50 except ImportError:
51
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
63 h64 = oidutil.toBase64(cryptutil.sha1(s))
64 h64 = h64.replace('+', '_')
65 h64 = h64.replace('/', '.')
66 h64 = h64.replace('=', '')
67 return h64
68
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
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
89 return 0
90 else:
91 raise
92 else:
93
94 return 1
95
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
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
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
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
146
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
152
153 self._setup()
154
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
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
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
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
211
212
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
219 file_obj.close()
220
221 try:
222 if hasattr(os, 'link') and sys.platform != 'cygwin':
223
224
225
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
234
235 raise
236 else:
237 raise
238 finally:
239 file_obj.close()
240 _removeIfPresent(tmp)
241
242 return auth_key
243
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
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
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
309
310
311 try:
312 os.unlink(filename)
313 except OSError, why:
314 if why[0] == ENOENT:
315 pass
316 else:
317 raise
318
319
320
321 os.rename(tmp, filename)
322 except:
323
324
325 _removeIfPresent(tmp)
326 raise
327
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
338
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
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
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
364 if matching_associations:
365 (_, assoc) = matching_associations[-1]
366 return assoc
367 else:
368 return None
369
371 try:
372 assoc_file = file(filename, 'rb')
373 except IOError, why:
374 if why[0] == ENOENT:
375
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
392 if association.getExpiresIn() == 0:
393 _removeIfPresent(filename)
394 return None
395 else:
396 return association
397
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
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
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
431 return 0
432 else:
433 raise
434 else:
435
436
437 try:
438 os.unlink(filename)
439 except OSError, why:
440 if why[0] == ENOENT:
441
442
443 return 0
444 else:
445 raise
446
447 now = time.time()
448 nonce_age = now - st.st_mtime
449
450
451
452 return nonce_age <= self.max_nonce_age
453
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
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
472
473 pass
474 else:
475 raise
476 else:
477
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
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