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