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 time
10
11 from errno import EEXIST, ENOENT
12
13 try:
14 from tempfile import mkstemp
15 except ImportError:
16
17 import warnings
18 warnings.filterwarnings("ignore",
19 "tempnam is a potential security risk",
20 RuntimeWarning,
21 "openid.store.filestore")
22
24 for _ in range(5):
25 name = os.tempnam(dir)
26 try:
27 fd = os.open(name, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0600)
28 except OSError, why:
29 if why.errno != EEXIST:
30 raise
31 else:
32 return fd, name
33
34 raise RuntimeError('Failed to get temp file after 5 attempts')
35
36 from openid.association import Association
37 from openid.store.interface import OpenIDStore
38 from openid.store import nonce
39 from openid import cryptutil, oidutil
40
41 _filename_allowed = string.ascii_letters + string.digits + '.'
42 try:
43
44 set
45 except NameError:
46 try:
47
48 import sets
49 except ImportError:
50
51 d = {}
52 for c in _filename_allowed:
53 d[c] = None
54 _isFilenameSafe = d.has_key
55 del d
56 else:
57 _isFilenameSafe = sets.Set(_filename_allowed).__contains__
58 else:
59 _isFilenameSafe = set(_filename_allowed).__contains__
60
62 h64 = oidutil.toBase64(cryptutil.sha1(s))
63 h64 = h64.replace('+', '_')
64 h64 = h64.replace('/', '.')
65 h64 = h64.replace('=', '')
66 return h64
67
69 filename_chunks = []
70 for c in s:
71 if _isFilenameSafe(c):
72 filename_chunks.append(c)
73 else:
74 filename_chunks.append('_%02X' % ord(c))
75 return ''.join(filename_chunks)
76
78 """Attempt to remove a file, returning whether the file existed at
79 the time of the call.
80
81 str -> bool
82 """
83 try:
84 os.unlink(filename)
85 except OSError, why:
86 if why.errno == ENOENT:
87
88 return 0
89 else:
90 raise
91 else:
92
93 return 1
94
96 """Create dir_name as a directory if it does not exist. If it
97 exists, make sure that it is, in fact, a directory.
98
99 Can raise OSError
100
101 str -> NoneType
102 """
103 try:
104 os.makedirs(dir_name)
105 except OSError, why:
106 if why.errno != EEXIST or not os.path.isdir(dir_name):
107 raise
108
110 """
111 This is a filesystem-based store for OpenID associations and
112 nonces. This store should be safe for use in concurrent systems
113 on both windows and unix (excluding NFS filesystems). There are a
114 couple race conditions in the system, but those failure cases have
115 been set up in such a way that the worst-case behavior is someone
116 having to try to log in a second time.
117
118 Most of the methods of this class are implementation details.
119 People wishing to just use this store need only pay attention to
120 the C{L{__init__}} method.
121
122 Methods of this object can raise OSError if unexpected filesystem
123 conditions, such as bad permissions or missing directories, occur.
124 """
125
127 """
128 Initializes a new FileOpenIDStore. This initializes the
129 nonce and association directories, which are subdirectories of
130 the directory passed in.
131
132 @param directory: This is the directory to put the store
133 directories in.
134
135 @type directory: C{str}
136 """
137
138 directory = os.path.normpath(os.path.abspath(directory))
139
140 self.nonce_dir = os.path.join(directory, 'nonces')
141
142 self.association_dir = os.path.join(directory, 'associations')
143
144
145
146 self.temp_dir = os.path.join(directory, 'temp')
147
148 self.max_nonce_age = 6 * 60 * 60
149
150 self._setup()
151
153 """Make sure that the directories in which we store our data
154 exist.
155
156 () -> NoneType
157 """
158 _ensureDir(self.nonce_dir)
159 _ensureDir(self.association_dir)
160 _ensureDir(self.temp_dir)
161
163 """Create a temporary file on the same filesystem as
164 self.association_dir.
165
166 The temporary directory should not be cleaned if there are any
167 processes using the store. If there is no active process using
168 the store, it is safe to remove all of the files in the
169 temporary directory.
170
171 () -> (file, str)
172 """
173 fd, name = mkstemp(dir=self.temp_dir)
174 try:
175 file_obj = os.fdopen(fd, 'wb')
176 return file_obj, name
177 except:
178 _removeIfPresent(name)
179 raise
180
182 """Create a unique filename for a given server url and
183 handle. This implementation does not assume anything about the
184 format of the handle. The filename that is returned will
185 contain the domain name from the server URL for ease of human
186 inspection of the data directory.
187
188 (str, str) -> str
189 """
190 if server_url.find('://') == -1:
191 raise ValueError('Bad server URL: %r' % server_url)
192
193 proto, rest = server_url.split('://', 1)
194 domain = _filenameEscape(rest.split('/', 1)[0])
195 url_hash = _safe64(server_url)
196 if handle:
197 handle_hash = _safe64(handle)
198 else:
199 handle_hash = ''
200
201 filename = '%s-%s-%s-%s' % (proto, domain, url_hash, handle_hash)
202
203 return os.path.join(self.association_dir, filename)
204
206 """Store an association in the association directory.
207
208 (str, Association) -> NoneType
209 """
210 association_s = association.serialize()
211 filename = self.getAssociationFilename(server_url, association.handle)
212 tmp_file, tmp = self._mktemp()
213
214 try:
215 try:
216 tmp_file.write(association_s)
217 os.fsync(tmp_file.fileno())
218 finally:
219 tmp_file.close()
220
221 try:
222 os.rename(tmp, filename)
223 except OSError, why:
224 if why.errno != EEXIST:
225 raise
226
227
228
229
230 try:
231 os.unlink(filename)
232 except OSError, why:
233 if why.errno == ENOENT:
234 pass
235 else:
236 raise
237
238
239
240 os.rename(tmp, filename)
241 except:
242
243
244 _removeIfPresent(tmp)
245 raise
246
248 """Retrieve an association. If no handle is specified, return
249 the association with the latest expiration.
250
251 (str, str or NoneType) -> Association or NoneType
252 """
253 if handle is None:
254 handle = ''
255
256
257
258 filename = self.getAssociationFilename(server_url, handle)
259
260 if handle:
261 return self._getAssociation(filename)
262 else:
263 association_files = os.listdir(self.association_dir)
264 matching_files = []
265
266 name = os.path.basename(filename)
267 for association_file in association_files:
268 if association_file.startswith(name):
269 matching_files.append(association_file)
270
271 matching_associations = []
272
273 for name in matching_files:
274 full_name = os.path.join(self.association_dir, name)
275 association = self._getAssociation(full_name)
276 if association is not None:
277 matching_associations.append(
278 (association.issued, association))
279
280 matching_associations.sort()
281
282
283 if matching_associations:
284 (_, assoc) = matching_associations[-1]
285 return assoc
286 else:
287 return None
288
290 try:
291 assoc_file = file(filename, 'rb')
292 except IOError, why:
293 if why.errno == ENOENT:
294
295 return None
296 else:
297 raise
298 else:
299 try:
300 assoc_s = assoc_file.read()
301 finally:
302 assoc_file.close()
303
304 try:
305 association = Association.deserialize(assoc_s)
306 except ValueError:
307 _removeIfPresent(filename)
308 return None
309
310
311 if association.getExpiresIn() == 0:
312 _removeIfPresent(filename)
313 return None
314 else:
315 return association
316
328
329 - def useNonce(self, server_url, timestamp, salt):
330 """Return whether this nonce is valid.
331
332 str -> bool
333 """
334 if abs(timestamp - time.time()) > nonce.SKEW:
335 return False
336
337 if server_url:
338 proto, rest = server_url.split('://', 1)
339 else:
340
341
342 proto, rest = '', ''
343
344 domain = _filenameEscape(rest.split('/', 1)[0])
345 url_hash = _safe64(server_url)
346 salt_hash = _safe64(salt)
347
348 filename = '%08x-%s-%s-%s-%s' % (timestamp, proto, domain,
349 url_hash, salt_hash)
350
351 filename = os.path.join(self.nonce_dir, filename)
352 try:
353 fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0200)
354 except OSError, why:
355 if why.errno == EEXIST:
356 return False
357 else:
358 raise
359 else:
360 os.close(fd)
361 return True
362
364 all_associations = []
365
366 association_filenames = map(
367 lambda filename: os.path.join(self.association_dir, filename),
368 os.listdir(self.association_dir))
369 for association_filename in association_filenames:
370 try:
371 association_file = file(association_filename, 'rb')
372 except IOError, why:
373 if why.errno == ENOENT:
374 oidutil.log("%s disappeared during %s._allAssocs" % (
375 association_filename, self.__class__.__name__))
376 else:
377 raise
378 else:
379 try:
380 assoc_s = association_file.read()
381 finally:
382 association_file.close()
383
384
385 try:
386 association = Association.deserialize(assoc_s)
387 except ValueError:
388 _removeIfPresent(association_filename)
389 else:
390 all_associations.append(
391 (association_filename, association))
392
393 return all_associations
394
396 """Remove expired entries from the database. This is
397 potentially expensive, so only run when it is acceptable to
398 take time.
399
400 () -> NoneType
401 """
402 self.cleanupAssociations()
403 self.cleanupNonces()
404
406 removed = 0
407 for assoc_filename, assoc in self._allAssocs():
408 if assoc.getExpiresIn() == 0:
409 _removeIfPresent(assoc_filename)
410 removed += 1
411 return removed
412
414 nonces = os.listdir(self.nonce_dir)
415 now = time.time()
416
417 removed = 0
418
419 for nonce_fname in nonces:
420 timestamp = nonce_fname.split('-', 1)[0]
421 timestamp = int(timestamp, 16)
422 if abs(timestamp - now) > nonce.SKEW:
423 filename = os.path.join(self.nonce_dir, nonce_fname)
424 _removeIfPresent(filename)
425 removed += 1
426 return removed
427