Integrated my history into data
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
This package contains the modules related to this library's use of
|
||||
persistent storage.
|
||||
|
||||
@sort: interface, filestore, sqlstore, memstore
|
||||
"""
|
||||
|
||||
__all__ = ['interface', 'filestore', 'sqlstore', 'memstore', 'nonce']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
This module contains an C{L{OpenIDStore}} implementation backed by
|
||||
flat files.
|
||||
"""
|
||||
|
||||
import string
|
||||
import os
|
||||
import os.path
|
||||
import time
|
||||
import logging
|
||||
|
||||
from errno import EEXIST, ENOENT
|
||||
|
||||
from tempfile import mkstemp
|
||||
|
||||
from openid.association import Association
|
||||
from openid.store.interface import OpenIDStore
|
||||
from openid.store import nonce
|
||||
from openid import cryptutil, oidutil
|
||||
|
||||
_filename_allowed = string.ascii_letters + string.digits + '.'
|
||||
_isFilenameSafe = set(_filename_allowed).__contains__
|
||||
|
||||
|
||||
def _safe64(s):
|
||||
h64 = oidutil.toBase64(cryptutil.sha1(s))
|
||||
# to be able to manipulate it, make it a bytearray
|
||||
h64 = bytearray(h64)
|
||||
h64 = h64.replace(b'+', b'_')
|
||||
h64 = h64.replace(b'/', b'.')
|
||||
h64 = h64.replace(b'=', b'')
|
||||
return bytes(h64)
|
||||
|
||||
|
||||
def _filenameEscape(s):
|
||||
filename_chunks = []
|
||||
for c in s:
|
||||
if _isFilenameSafe(c):
|
||||
filename_chunks.append(c)
|
||||
else:
|
||||
filename_chunks.append('_%02X' % ord(c))
|
||||
return ''.join(filename_chunks)
|
||||
|
||||
|
||||
def _removeIfPresent(filename):
|
||||
"""Attempt to remove a file, returning whether the file existed at
|
||||
the time of the call.
|
||||
|
||||
str -> bool
|
||||
"""
|
||||
try:
|
||||
os.unlink(filename)
|
||||
except OSError as why:
|
||||
if why.errno == ENOENT:
|
||||
# Someone beat us to it, but it's gone, so that's OK
|
||||
return 0
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# File was present
|
||||
return 1
|
||||
|
||||
|
||||
def _ensureDir(dir_name):
|
||||
"""Create dir_name as a directory if it does not exist. If it
|
||||
exists, make sure that it is, in fact, a directory.
|
||||
|
||||
Can raise OSError
|
||||
|
||||
str -> NoneType
|
||||
"""
|
||||
try:
|
||||
os.makedirs(dir_name)
|
||||
except OSError as why:
|
||||
if why.errno != EEXIST or not os.path.isdir(dir_name):
|
||||
raise
|
||||
|
||||
|
||||
class FileOpenIDStore(OpenIDStore):
|
||||
"""
|
||||
This is a filesystem-based store for OpenID associations and
|
||||
nonces. This store should be safe for use in concurrent systems
|
||||
on both windows and unix (excluding NFS filesystems). There are a
|
||||
couple race conditions in the system, but those failure cases have
|
||||
been set up in such a way that the worst-case behavior is someone
|
||||
having to try to log in a second time.
|
||||
|
||||
Most of the methods of this class are implementation details.
|
||||
People wishing to just use this store need only pay attention to
|
||||
the C{L{__init__}} method.
|
||||
|
||||
Methods of this object can raise OSError if unexpected filesystem
|
||||
conditions, such as bad permissions or missing directories, occur.
|
||||
"""
|
||||
|
||||
def __init__(self, directory):
|
||||
"""
|
||||
Initializes a new FileOpenIDStore. This initializes the
|
||||
nonce and association directories, which are subdirectories of
|
||||
the directory passed in.
|
||||
|
||||
@param directory: This is the directory to put the store
|
||||
directories in.
|
||||
|
||||
@type directory: C{str}
|
||||
"""
|
||||
# Make absolute
|
||||
directory = os.path.normpath(os.path.abspath(directory))
|
||||
|
||||
self.nonce_dir = os.path.join(directory, 'nonces')
|
||||
|
||||
self.association_dir = os.path.join(directory, 'associations')
|
||||
|
||||
# Temp dir must be on the same filesystem as the assciations
|
||||
# directory
|
||||
self.temp_dir = os.path.join(directory, 'temp')
|
||||
|
||||
self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds
|
||||
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
"""Make sure that the directories in which we store our data
|
||||
exist.
|
||||
|
||||
() -> NoneType
|
||||
"""
|
||||
_ensureDir(self.nonce_dir)
|
||||
_ensureDir(self.association_dir)
|
||||
_ensureDir(self.temp_dir)
|
||||
|
||||
def _mktemp(self):
|
||||
"""Create a temporary file on the same filesystem as
|
||||
self.association_dir.
|
||||
|
||||
The temporary directory should not be cleaned if there are any
|
||||
processes using the store. If there is no active process using
|
||||
the store, it is safe to remove all of the files in the
|
||||
temporary directory.
|
||||
|
||||
() -> (file, str)
|
||||
"""
|
||||
fd, name = mkstemp(dir=self.temp_dir)
|
||||
try:
|
||||
file_obj = os.fdopen(fd, 'wb')
|
||||
return file_obj, name
|
||||
except:
|
||||
_removeIfPresent(name)
|
||||
raise
|
||||
|
||||
def getAssociationFilename(self, server_url, handle):
|
||||
"""Create a unique filename for a given server url and
|
||||
handle. This implementation does not assume anything about the
|
||||
format of the handle. The filename that is returned will
|
||||
contain the domain name from the server URL for ease of human
|
||||
inspection of the data directory.
|
||||
|
||||
(str, str) -> str
|
||||
"""
|
||||
if server_url.find('://') == -1:
|
||||
raise ValueError('Bad server URL: %r' % server_url)
|
||||
|
||||
proto, rest = server_url.split('://', 1)
|
||||
domain = _filenameEscape(rest.split('/', 1)[0])
|
||||
url_hash = _safe64(server_url)
|
||||
if handle:
|
||||
handle_hash = _safe64(handle)
|
||||
else:
|
||||
handle_hash = ''
|
||||
|
||||
filename = '%s-%s-%s-%s' % (proto, domain, url_hash, handle_hash)
|
||||
|
||||
return os.path.join(self.association_dir, filename)
|
||||
|
||||
def storeAssociation(self, server_url, association):
|
||||
"""Store an association in the association directory.
|
||||
|
||||
(str, Association) -> NoneType
|
||||
"""
|
||||
association_s = association.serialize() # NOTE: UTF-8 encoded bytes
|
||||
filename = self.getAssociationFilename(server_url, association.handle)
|
||||
tmp_file, tmp = self._mktemp()
|
||||
|
||||
try:
|
||||
try:
|
||||
tmp_file.write(association_s)
|
||||
os.fsync(tmp_file.fileno())
|
||||
finally:
|
||||
tmp_file.close()
|
||||
|
||||
try:
|
||||
os.rename(tmp, filename)
|
||||
except OSError as why:
|
||||
if why.errno != EEXIST:
|
||||
raise
|
||||
|
||||
# We only expect EEXIST to happen only on Windows. It's
|
||||
# possible that we will succeed in unlinking the existing
|
||||
# file, but not in putting the temporary file in place.
|
||||
try:
|
||||
os.unlink(filename)
|
||||
except OSError as why:
|
||||
if why.errno == ENOENT:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
# Now the target should not exist. Try renaming again,
|
||||
# giving up if it fails.
|
||||
os.rename(tmp, filename)
|
||||
except:
|
||||
# If there was an error, don't leave the temporary file
|
||||
# around.
|
||||
_removeIfPresent(tmp)
|
||||
raise
|
||||
|
||||
def getAssociation(self, server_url, handle=None):
|
||||
"""Retrieve an association. If no handle is specified, return
|
||||
the association with the latest expiration.
|
||||
|
||||
(str, str or NoneType) -> Association or NoneType
|
||||
"""
|
||||
if handle is None:
|
||||
handle = ''
|
||||
|
||||
# The filename with the empty handle is a prefix of all other
|
||||
# associations for the given server URL.
|
||||
filename = self.getAssociationFilename(server_url, handle)
|
||||
|
||||
if handle:
|
||||
return self._getAssociation(filename)
|
||||
else:
|
||||
association_files = os.listdir(self.association_dir)
|
||||
matching_files = []
|
||||
# strip off the path to do the comparison
|
||||
name = os.path.basename(filename)
|
||||
for association_file in association_files:
|
||||
if association_file.startswith(name):
|
||||
matching_files.append(association_file)
|
||||
|
||||
matching_associations = []
|
||||
# read the matching files and sort by time issued
|
||||
for name in matching_files:
|
||||
full_name = os.path.join(self.association_dir, name)
|
||||
association = self._getAssociation(full_name)
|
||||
if association is not None:
|
||||
matching_associations.append(
|
||||
(association.issued, association))
|
||||
|
||||
matching_associations.sort()
|
||||
|
||||
# return the most recently issued one.
|
||||
if matching_associations:
|
||||
(_, assoc) = matching_associations[-1]
|
||||
return assoc
|
||||
else:
|
||||
return None
|
||||
|
||||
def _getAssociation(self, filename):
|
||||
try:
|
||||
assoc_file = open(filename, 'rb')
|
||||
except IOError as why:
|
||||
if why.errno == ENOENT:
|
||||
# No association exists for that URL and handle
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
assoc_s = assoc_file.read()
|
||||
finally:
|
||||
assoc_file.close()
|
||||
|
||||
try:
|
||||
association = Association.deserialize(assoc_s)
|
||||
except ValueError:
|
||||
_removeIfPresent(filename)
|
||||
return None
|
||||
|
||||
# Clean up expired associations
|
||||
if association.expiresIn == 0:
|
||||
_removeIfPresent(filename)
|
||||
return None
|
||||
else:
|
||||
return association
|
||||
|
||||
def removeAssociation(self, server_url, handle):
|
||||
"""Remove an association if it exists. Do nothing if it does not.
|
||||
|
||||
(str, str) -> bool
|
||||
"""
|
||||
assoc = self.getAssociation(server_url, handle)
|
||||
if assoc is None:
|
||||
return 0
|
||||
else:
|
||||
filename = self.getAssociationFilename(server_url, handle)
|
||||
return _removeIfPresent(filename)
|
||||
|
||||
def useNonce(self, server_url, timestamp, salt):
|
||||
"""Return whether this nonce is valid.
|
||||
|
||||
str -> bool
|
||||
"""
|
||||
if abs(timestamp - time.time()) > nonce.SKEW:
|
||||
return False
|
||||
|
||||
if server_url:
|
||||
proto, rest = server_url.split('://', 1)
|
||||
else:
|
||||
# Create empty proto / rest values for empty server_url,
|
||||
# which is part of a consumer-generated nonce.
|
||||
proto, rest = '', ''
|
||||
|
||||
domain = _filenameEscape(rest.split('/', 1)[0])
|
||||
url_hash = _safe64(server_url)
|
||||
salt_hash = _safe64(salt)
|
||||
|
||||
filename = '%08x-%s-%s-%s-%s' % (timestamp, proto, domain, url_hash,
|
||||
salt_hash)
|
||||
|
||||
filename = os.path.join(self.nonce_dir, filename)
|
||||
try:
|
||||
fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o200)
|
||||
except OSError as why:
|
||||
if why.errno == EEXIST:
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
def _allAssocs(self):
|
||||
all_associations = []
|
||||
|
||||
association_filenames = [
|
||||
os.path.join(self.association_dir, filename)
|
||||
for filename in os.listdir(self.association_dir)
|
||||
]
|
||||
for association_filename in association_filenames:
|
||||
try:
|
||||
association_file = open(association_filename, 'rb')
|
||||
except IOError as why:
|
||||
if why.errno == ENOENT:
|
||||
logging.exception("%s disappeared during %s._allAssocs" % (
|
||||
association_filename, self.__class__.__name__))
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
assoc_s = association_file.read()
|
||||
finally:
|
||||
association_file.close()
|
||||
|
||||
# Remove expired or corrupted associations
|
||||
try:
|
||||
association = Association.deserialize(assoc_s)
|
||||
except ValueError:
|
||||
_removeIfPresent(association_filename)
|
||||
else:
|
||||
all_associations.append(
|
||||
(association_filename, association))
|
||||
|
||||
return all_associations
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove expired entries from the database. This is
|
||||
potentially expensive, so only run when it is acceptable to
|
||||
take time.
|
||||
|
||||
() -> NoneType
|
||||
"""
|
||||
self.cleanupAssociations()
|
||||
self.cleanupNonces()
|
||||
|
||||
def cleanupAssociations(self):
|
||||
removed = 0
|
||||
for assoc_filename, assoc in self._allAssocs():
|
||||
if assoc.expiresIn == 0:
|
||||
_removeIfPresent(assoc_filename)
|
||||
removed += 1
|
||||
return removed
|
||||
|
||||
def cleanupNonces(self):
|
||||
nonces = os.listdir(self.nonce_dir)
|
||||
now = time.time()
|
||||
|
||||
removed = 0
|
||||
# Check all nonces for expiry
|
||||
for nonce_fname in nonces:
|
||||
timestamp = nonce_fname.split('-', 1)[0]
|
||||
timestamp = int(timestamp, 16)
|
||||
if abs(timestamp - now) > nonce.SKEW:
|
||||
filename = os.path.join(self.nonce_dir, nonce_fname)
|
||||
_removeIfPresent(filename)
|
||||
removed += 1
|
||||
return removed
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
This module contains the definition of the C{L{OpenIDStore}}
|
||||
interface.
|
||||
"""
|
||||
|
||||
|
||||
class OpenIDStore(object):
|
||||
"""
|
||||
This is the interface for the store objects the OpenID library
|
||||
uses. It is a single class that provides all of the persistence
|
||||
mechanisms that the OpenID library needs, for both servers and
|
||||
consumers.
|
||||
|
||||
@change: Version 2.0 removed the C{storeNonce}, C{getAuthKey}, and C{isDumb}
|
||||
methods, and changed the behavior of the C{L{useNonce}} method
|
||||
to support one-way nonces. It added C{L{cleanupNonces}},
|
||||
C{L{cleanupAssociations}}, and C{L{cleanup}}.
|
||||
|
||||
@sort: storeAssociation, getAssociation, removeAssociation,
|
||||
useNonce
|
||||
"""
|
||||
|
||||
def storeAssociation(self, server_url, association):
|
||||
"""
|
||||
This method puts a C{L{Association
|
||||
<openid.association.Association>}} object into storage,
|
||||
retrievable by server URL and handle.
|
||||
|
||||
|
||||
@param server_url: The URL of the identity server that this
|
||||
association is with. Because of the way the server
|
||||
portion of the library uses this interface, don't assume
|
||||
there are any limitations on the character set of the
|
||||
input string. In particular, expect to see unescaped
|
||||
non-url-safe characters in the server_url field.
|
||||
|
||||
@type server_url: C{str}
|
||||
|
||||
|
||||
@param association: The C{L{Association
|
||||
<openid.association.Association>}} to store.
|
||||
|
||||
@type association: C{L{Association
|
||||
<openid.association.Association>}}
|
||||
|
||||
|
||||
@return: C{None}
|
||||
|
||||
@rtype: C{NoneType}
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def getAssociation(self, server_url, handle=None):
|
||||
"""
|
||||
This method returns an C{L{Association
|
||||
<openid.association.Association>}} object from storage that
|
||||
matches the server URL and, if specified, handle. It returns
|
||||
C{None} if no such association is found or if the matching
|
||||
association is expired.
|
||||
|
||||
If no handle is specified, the store may return any
|
||||
association which matches the server URL. If multiple
|
||||
associations are valid, the recommended return value for this
|
||||
method is the one most recently issued.
|
||||
|
||||
This method is allowed (and encouraged) to garbage collect
|
||||
expired associations when found. This method must not return
|
||||
expired associations.
|
||||
|
||||
|
||||
@param server_url: The URL of the identity server to get the
|
||||
association for. Because of the way the server portion of
|
||||
the library uses this interface, don't assume there are
|
||||
any limitations on the character set of the input string.
|
||||
In particular, expect to see unescaped non-url-safe
|
||||
characters in the server_url field.
|
||||
|
||||
@type server_url: C{str}
|
||||
|
||||
|
||||
@param handle: This optional parameter is the handle of the
|
||||
specific association to get. If no specific handle is
|
||||
provided, any valid association matching the server URL is
|
||||
returned.
|
||||
|
||||
@type handle: C{str} or C{NoneType}
|
||||
|
||||
|
||||
@return: The C{L{Association
|
||||
<openid.association.Association>}} for the given identity
|
||||
server.
|
||||
|
||||
@rtype: C{L{Association <openid.association.Association>}} or
|
||||
C{NoneType}
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def removeAssociation(self, server_url, handle):
|
||||
"""
|
||||
This method removes the matching association if it's found,
|
||||
and returns whether the association was removed or not.
|
||||
|
||||
|
||||
@param server_url: The URL of the identity server the
|
||||
association to remove belongs to. Because of the way the
|
||||
server portion of the library uses this interface, don't
|
||||
assume there are any limitations on the character set of
|
||||
the input string. In particular, expect to see unescaped
|
||||
non-url-safe characters in the server_url field.
|
||||
|
||||
@type server_url: C{str}
|
||||
|
||||
|
||||
@param handle: This is the handle of the association to
|
||||
remove. If there isn't an association found that matches
|
||||
both the given URL and handle, then there was no matching
|
||||
handle found.
|
||||
|
||||
@type handle: C{str}
|
||||
|
||||
|
||||
@return: Returns whether or not the given association existed.
|
||||
|
||||
@rtype: C{bool} or C{int}
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def useNonce(self, server_url, timestamp, salt):
|
||||
"""Called when using a nonce.
|
||||
|
||||
This method should return C{True} if the nonce has not been
|
||||
used before, and store it for a while to make sure nobody
|
||||
tries to use the same value again. If the nonce has already
|
||||
been used or the timestamp is not current, return C{False}.
|
||||
|
||||
You may use L{openid.store.nonce.SKEW} for your timestamp window.
|
||||
|
||||
@change: In earlier versions, round-trip nonces were used and
|
||||
a nonce was only valid if it had been previously stored
|
||||
with C{storeNonce}. Version 2.0 uses one-way nonces,
|
||||
requiring a different implementation here that does not
|
||||
depend on a C{storeNonce} call. (C{storeNonce} is no
|
||||
longer part of the interface.)
|
||||
|
||||
@param server_url: The URL of the server from which the nonce
|
||||
originated.
|
||||
|
||||
@type server_url: C{str}
|
||||
|
||||
@param timestamp: The time that the nonce was created (to the
|
||||
nearest second), in seconds since January 1 1970 UTC.
|
||||
@type timestamp: C{int}
|
||||
|
||||
@param salt: A random string that makes two nonces from the
|
||||
same server issued during the same second unique.
|
||||
@type salt: str
|
||||
|
||||
@return: Whether or not the nonce was valid.
|
||||
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def cleanupNonces(self):
|
||||
"""Remove expired nonces from the store.
|
||||
|
||||
Discards any nonce from storage that is old enough that its
|
||||
timestamp would not pass L{useNonce}.
|
||||
|
||||
This method is not called in the normal operation of the
|
||||
library. It provides a way for store admins to keep
|
||||
their storage from filling up with expired data.
|
||||
|
||||
@return: the number of nonces expired.
|
||||
@returntype: int
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def cleanupAssociations(self):
|
||||
"""Remove expired associations from the store.
|
||||
|
||||
This method is not called in the normal operation of the
|
||||
library. It provides a way for store admins to keep
|
||||
their storage from filling up with expired data.
|
||||
|
||||
@return: the number of associations expired.
|
||||
@returntype: int
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def cleanup(self):
|
||||
"""Shortcut for C{L{cleanupNonces}()}, C{L{cleanupAssociations}()}.
|
||||
|
||||
This method is not called in the normal operation of the
|
||||
library. It provides a way for store admins to keep
|
||||
their storage from filling up with expired data.
|
||||
"""
|
||||
return self.cleanupNonces(), self.cleanupAssociations()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""A simple store using only in-process memory."""
|
||||
|
||||
from openid.store import nonce
|
||||
|
||||
import copy
|
||||
import time
|
||||
|
||||
|
||||
class ServerAssocs(object):
|
||||
def __init__(self):
|
||||
self.assocs = {}
|
||||
|
||||
def set(self, assoc):
|
||||
self.assocs[assoc.handle] = assoc
|
||||
|
||||
def get(self, handle):
|
||||
return self.assocs.get(handle)
|
||||
|
||||
def remove(self, handle):
|
||||
try:
|
||||
del self.assocs[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def best(self):
|
||||
"""Returns association with the oldest issued date.
|
||||
|
||||
or None if there are no associations.
|
||||
"""
|
||||
best = None
|
||||
for assoc in list(self.assocs.values()):
|
||||
if best is None or best.issued < assoc.issued:
|
||||
best = assoc
|
||||
return best
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove expired associations.
|
||||
|
||||
@return: tuple of (removed associations, remaining associations)
|
||||
"""
|
||||
remove = []
|
||||
for handle, assoc in self.assocs.items():
|
||||
if assoc.expiresIn == 0:
|
||||
remove.append(handle)
|
||||
for handle in remove:
|
||||
del self.assocs[handle]
|
||||
return len(remove), len(self.assocs)
|
||||
|
||||
|
||||
class MemoryStore(object):
|
||||
"""In-process memory store.
|
||||
|
||||
Use for single long-running processes. No persistence supplied.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.server_assocs = {}
|
||||
self.nonces = {}
|
||||
|
||||
def _getServerAssocs(self, server_url):
|
||||
try:
|
||||
return self.server_assocs[server_url]
|
||||
except KeyError:
|
||||
assocs = self.server_assocs[server_url] = ServerAssocs()
|
||||
return assocs
|
||||
|
||||
def storeAssociation(self, server_url, assoc):
|
||||
assocs = self._getServerAssocs(server_url)
|
||||
assocs.set(copy.deepcopy(assoc))
|
||||
|
||||
def getAssociation(self, server_url, handle=None):
|
||||
assocs = self._getServerAssocs(server_url)
|
||||
if handle is None:
|
||||
return assocs.best()
|
||||
else:
|
||||
return assocs.get(handle)
|
||||
|
||||
def removeAssociation(self, server_url, handle):
|
||||
assocs = self._getServerAssocs(server_url)
|
||||
return assocs.remove(handle)
|
||||
|
||||
def useNonce(self, server_url, timestamp, salt):
|
||||
if abs(timestamp - time.time()) > nonce.SKEW:
|
||||
return False
|
||||
|
||||
anonce = (str(server_url), int(timestamp), str(salt))
|
||||
if anonce in self.nonces:
|
||||
return False
|
||||
else:
|
||||
self.nonces[anonce] = None
|
||||
return True
|
||||
|
||||
def cleanupNonces(self):
|
||||
now = time.time()
|
||||
expired = []
|
||||
for anonce in self.nonces.keys():
|
||||
if abs(anonce[1] - now) > nonce.SKEW:
|
||||
# removing items while iterating over the set could be bad.
|
||||
expired.append(anonce)
|
||||
|
||||
for anonce in expired:
|
||||
del self.nonces[anonce]
|
||||
return len(expired)
|
||||
|
||||
def cleanupAssociations(self):
|
||||
remove_urls = []
|
||||
removed_assocs = 0
|
||||
for server_url, assocs in self.server_assocs.items():
|
||||
removed, remaining = assocs.cleanup()
|
||||
removed_assocs += removed
|
||||
if not remaining:
|
||||
remove_urls.append(server_url)
|
||||
|
||||
# Remove entries from server_assocs that had none remaining.
|
||||
for server_url in remove_urls:
|
||||
del self.server_assocs[server_url]
|
||||
return removed_assocs
|
||||
|
||||
def __eq__(self, other):
|
||||
return ((self.server_assocs == other.server_assocs) and
|
||||
(self.nonces == other.nonces))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
@@ -0,0 +1,101 @@
|
||||
__all__ = [
|
||||
'split',
|
||||
'mkNonce',
|
||||
'checkTimestamp',
|
||||
]
|
||||
|
||||
from openid import cryptutil
|
||||
from time import strptime, strftime, gmtime, time
|
||||
from calendar import timegm
|
||||
import string
|
||||
|
||||
NONCE_CHARS = string.ascii_letters + string.digits
|
||||
|
||||
# Keep nonces for five hours (allow five hours for the combination of
|
||||
# request time and clock skew). This is probably way more than is
|
||||
# necessary, but there is not much overhead in storing nonces.
|
||||
SKEW = 60 * 60 * 5
|
||||
|
||||
time_fmt = '%Y-%m-%dT%H:%M:%SZ'
|
||||
time_str_len = len('0000-00-00T00:00:00Z')
|
||||
|
||||
|
||||
def split(nonce_string):
|
||||
"""Extract a timestamp from the given nonce string
|
||||
|
||||
@param nonce_string: the nonce from which to extract the timestamp
|
||||
@type nonce_string: str
|
||||
|
||||
@returns: A pair of a Unix timestamp and the salt characters
|
||||
@returntype: (int, str)
|
||||
|
||||
@raises ValueError: if the nonce does not start with a correctly
|
||||
formatted time string
|
||||
"""
|
||||
timestamp_str = nonce_string[:time_str_len]
|
||||
try:
|
||||
timestamp = timegm(strptime(timestamp_str, time_fmt))
|
||||
except AssertionError: # Python 2.2
|
||||
timestamp = -1
|
||||
if timestamp < 0:
|
||||
raise ValueError('time out of range')
|
||||
return timestamp, nonce_string[time_str_len:]
|
||||
|
||||
|
||||
def checkTimestamp(nonce_string, allowed_skew=SKEW, now=None):
|
||||
"""Is the timestamp that is part of the specified nonce string
|
||||
within the allowed clock-skew of the current time?
|
||||
|
||||
@param nonce_string: The nonce that is being checked
|
||||
@type nonce_string: str
|
||||
|
||||
@param allowed_skew: How many seconds should be allowed for
|
||||
completing the request, allowing for clock skew.
|
||||
@type allowed_skew: int
|
||||
|
||||
@param now: The current time, as a Unix timestamp
|
||||
@type now: int
|
||||
|
||||
@returntype: bool
|
||||
@returns: Whether the timestamp is correctly formatted and within
|
||||
the allowed skew of the current time.
|
||||
"""
|
||||
try:
|
||||
stamp, _ = split(nonce_string)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
if now is None:
|
||||
now = time()
|
||||
|
||||
# Time after which we should not use the nonce
|
||||
past = now - allowed_skew
|
||||
|
||||
# Time that is too far in the future for us to allow
|
||||
future = now + allowed_skew
|
||||
|
||||
# the stamp is not too far in the future and is not too far in
|
||||
# the past
|
||||
return past <= stamp <= future
|
||||
|
||||
|
||||
def mkNonce(when=None):
|
||||
"""Generate a nonce with the current timestamp
|
||||
|
||||
@param when: Unix timestamp representing the issue time of the
|
||||
nonce. Defaults to the current time.
|
||||
@type when: int
|
||||
|
||||
@returntype: str
|
||||
@returns: A string that should be usable as a one-way nonce
|
||||
|
||||
@see: time
|
||||
"""
|
||||
salt = cryptutil.randomString(6, NONCE_CHARS)
|
||||
if when is None:
|
||||
t = gmtime()
|
||||
else:
|
||||
t = gmtime(when)
|
||||
|
||||
time_str = strftime(time_fmt, t)
|
||||
return time_str + salt
|
||||
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
This module contains C{L{OpenIDStore}} implementations that use
|
||||
various SQL databases to back them.
|
||||
|
||||
Example of how to initialize a store database::
|
||||
|
||||
python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2;'
|
||||
'sqlstore.SQLiteStore(pysqlite2.dbapi2.connect("cstore.db")).createTables()'
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
from psycopg2cffi import compat
|
||||
compat.register()
|
||||
|
||||
from openid.association import Association
|
||||
from openid.store.interface import OpenIDStore
|
||||
from openid.store import nonce
|
||||
|
||||
|
||||
def _inTxn(func):
|
||||
def wrapped(self, *args, **kwargs):
|
||||
return self._callInTransaction(func, self, *args, **kwargs)
|
||||
|
||||
if hasattr(func, '__name__'):
|
||||
try:
|
||||
wrapped.__name__ = func.__name__[4:]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if hasattr(func, '__doc__'):
|
||||
wrapped.__doc__ = func.__doc__
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class SQLStore(OpenIDStore):
|
||||
"""
|
||||
This is the parent class for the SQL stores, which contains the
|
||||
logic common to all of the SQL stores.
|
||||
|
||||
The table names used are determined by the class variables
|
||||
C{L{associations_table}} and
|
||||
C{L{nonces_table}}. To change the name of the tables used, pass
|
||||
new table names into the constructor.
|
||||
|
||||
To create the tables with the proper schema, see the
|
||||
C{L{createTables}} method.
|
||||
|
||||
This class shouldn't be used directly. Use one of its subclasses
|
||||
instead, as those contain the code necessary to use a specific
|
||||
database.
|
||||
|
||||
All methods other than C{L{__init__}} and C{L{createTables}}
|
||||
should be considered implementation details.
|
||||
|
||||
|
||||
@cvar associations_table: This is the default name of the table to
|
||||
keep associations in
|
||||
|
||||
@cvar nonces_table: This is the default name of the table to keep
|
||||
nonces in.
|
||||
|
||||
|
||||
@sort: __init__, createTables
|
||||
"""
|
||||
|
||||
associations_table = 'oid_associations'
|
||||
nonces_table = 'oid_nonces'
|
||||
|
||||
def __init__(self, conn, associations_table=None, nonces_table=None):
|
||||
"""
|
||||
This creates a new SQLStore instance. It requires an
|
||||
established database connection be given to it, and it allows
|
||||
overriding the default table names.
|
||||
|
||||
|
||||
@param conn: This must be an established connection to a
|
||||
database of the correct type for the SQLStore subclass
|
||||
you're using.
|
||||
|
||||
@type conn: A python database API compatible connection
|
||||
object.
|
||||
|
||||
|
||||
@param associations_table: This is an optional parameter to
|
||||
specify the name of the table used for storing
|
||||
associations. The default value is specified in
|
||||
C{L{SQLStore.associations_table}}.
|
||||
|
||||
@type associations_table: C{str}
|
||||
|
||||
|
||||
@param nonces_table: This is an optional parameter to specify
|
||||
the name of the table used for storing nonces. The
|
||||
default value is specified in C{L{SQLStore.nonces_table}}.
|
||||
|
||||
@type nonces_table: C{str}
|
||||
"""
|
||||
self.conn = conn
|
||||
self.cur = None
|
||||
self._statement_cache = {}
|
||||
self._table_names = {
|
||||
'associations': associations_table or self.associations_table,
|
||||
'nonces': nonces_table or self.nonces_table,
|
||||
}
|
||||
self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds
|
||||
|
||||
# DB API extension: search for "Connection Attributes .Error,
|
||||
# .ProgrammingError, etc." in
|
||||
# http://www.python.org/dev/peps/pep-0249/
|
||||
if (hasattr(self.conn, 'IntegrityError') and
|
||||
hasattr(self.conn, 'OperationalError')):
|
||||
self.exceptions = self.conn
|
||||
|
||||
if not (hasattr(self.exceptions, 'IntegrityError') and
|
||||
hasattr(self.exceptions, 'OperationalError')):
|
||||
raise RuntimeError("Error using database connection module "
|
||||
"(Maybe it can't be imported?)")
|
||||
|
||||
def blobDecode(self, blob):
|
||||
"""Convert a blob as returned by the SQL engine into a str object.
|
||||
|
||||
str -> str"""
|
||||
return blob
|
||||
|
||||
def blobEncode(self, s):
|
||||
"""Convert a str object into the necessary object for storing
|
||||
in the database as a blob."""
|
||||
return s
|
||||
|
||||
def _getSQL(self, sql_name):
|
||||
try:
|
||||
return self._statement_cache[sql_name]
|
||||
except KeyError:
|
||||
sql = getattr(self, sql_name)
|
||||
sql %= self._table_names
|
||||
self._statement_cache[sql_name] = sql
|
||||
return sql
|
||||
|
||||
def _execSQL(self, sql_name, *args):
|
||||
sql = self._getSQL(sql_name)
|
||||
|
||||
# Kludge because we have reports of postgresql not quoting
|
||||
# arguments if they are passed in as unicode instead of str.
|
||||
# Currently the strings in our tables just have ascii in them,
|
||||
# so this ought to be safe.
|
||||
def unicode_to_str(arg):
|
||||
if isinstance(arg, str):
|
||||
return str(arg)
|
||||
else:
|
||||
return arg
|
||||
|
||||
str_args = list(map(unicode_to_str, args))
|
||||
self.cur.execute(sql, str_args)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# if the attribute starts with db_, use a default
|
||||
# implementation that looks up the appropriate SQL statement
|
||||
# as an attribute of this object and executes it.
|
||||
if attr[:3] == 'db_':
|
||||
sql_name = attr[3:] + '_sql'
|
||||
|
||||
def func(*args):
|
||||
return self._execSQL(sql_name, *args)
|
||||
|
||||
setattr(self, attr, func)
|
||||
return func
|
||||
else:
|
||||
raise AttributeError('Attribute %r not found' % (attr, ))
|
||||
|
||||
def _callInTransaction(self, func, *args, **kwargs):
|
||||
"""Execute the given function inside of a transaction, with an
|
||||
open cursor. If no exception is raised, the transaction is
|
||||
comitted, otherwise it is rolled back."""
|
||||
# No nesting of transactions
|
||||
self.conn.rollback()
|
||||
|
||||
try:
|
||||
self.cur = self.conn.cursor()
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
finally:
|
||||
self.cur.close()
|
||||
self.cur = None
|
||||
except:
|
||||
self.conn.rollback()
|
||||
raise
|
||||
else:
|
||||
self.conn.commit()
|
||||
|
||||
return ret
|
||||
|
||||
def txn_createTables(self):
|
||||
"""
|
||||
This method creates the database tables necessary for this
|
||||
store to work. It should not be called if the tables already
|
||||
exist.
|
||||
"""
|
||||
self.db_create_nonce()
|
||||
self.db_create_assoc()
|
||||
|
||||
createTables = _inTxn(txn_createTables)
|
||||
|
||||
def txn_storeAssociation(self, server_url, association):
|
||||
"""Set the association for the server URL.
|
||||
|
||||
Association -> NoneType
|
||||
"""
|
||||
a = association
|
||||
self.db_set_assoc(server_url, a.handle,
|
||||
self.blobEncode(a.secret), a.issued, a.lifetime,
|
||||
a.assoc_type)
|
||||
|
||||
storeAssociation = _inTxn(txn_storeAssociation)
|
||||
|
||||
def txn_getAssociation(self, server_url, handle=None):
|
||||
"""Get the most recent association that has been set for this
|
||||
server URL and handle.
|
||||
|
||||
str -> NoneType or Association
|
||||
"""
|
||||
if handle is not None:
|
||||
self.db_get_assoc(server_url, handle)
|
||||
else:
|
||||
self.db_get_assocs(server_url)
|
||||
|
||||
rows = self.cur.fetchall()
|
||||
if len(rows) == 0:
|
||||
return None
|
||||
else:
|
||||
associations = []
|
||||
for values in rows:
|
||||
values = list(values)
|
||||
values[1] = self.blobDecode(values[1])
|
||||
assoc = Association(*values)
|
||||
if assoc.expiresIn == 0:
|
||||
self.txn_removeAssociation(server_url, assoc.handle)
|
||||
else:
|
||||
associations.append((assoc.issued, assoc))
|
||||
|
||||
if associations:
|
||||
associations.sort()
|
||||
return associations[-1][1]
|
||||
else:
|
||||
return None
|
||||
|
||||
getAssociation = _inTxn(txn_getAssociation)
|
||||
|
||||
def txn_removeAssociation(self, server_url, handle):
|
||||
"""Remove the association for the given server URL and handle,
|
||||
returning whether the association existed at all.
|
||||
|
||||
(str, str) -> bool
|
||||
"""
|
||||
self.db_remove_assoc(server_url, handle)
|
||||
return self.cur.rowcount > 0 # -1 is undefined
|
||||
|
||||
removeAssociation = _inTxn(txn_removeAssociation)
|
||||
|
||||
def txn_useNonce(self, server_url, timestamp, salt):
|
||||
"""Return whether this nonce is present, and if it is, then
|
||||
remove it from the set.
|
||||
|
||||
str -> bool"""
|
||||
if abs(timestamp - time.time()) > nonce.SKEW:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.db_add_nonce(server_url, timestamp, salt)
|
||||
except self.exceptions.IntegrityError:
|
||||
# The key uniqueness check failed
|
||||
return False
|
||||
else:
|
||||
# The nonce was successfully added
|
||||
return True
|
||||
|
||||
useNonce = _inTxn(txn_useNonce)
|
||||
|
||||
def txn_cleanupNonces(self):
|
||||
self.db_clean_nonce(int(time.time()) - nonce.SKEW)
|
||||
return self.cur.rowcount
|
||||
|
||||
cleanupNonces = _inTxn(txn_cleanupNonces)
|
||||
|
||||
def txn_cleanupAssociations(self):
|
||||
self.db_clean_assoc(int(time.time()))
|
||||
return self.cur.rowcount
|
||||
|
||||
cleanupAssociations = _inTxn(txn_cleanupAssociations)
|
||||
|
||||
|
||||
class SQLiteStore(SQLStore):
|
||||
"""
|
||||
This is an SQLite-based specialization of C{L{SQLStore}}.
|
||||
|
||||
To create an instance, see C{L{SQLStore.__init__}}. To create the
|
||||
tables it will use, see C{L{SQLStore.createTables}}.
|
||||
|
||||
All other methods are implementation details.
|
||||
"""
|
||||
|
||||
create_nonce_sql = """
|
||||
CREATE TABLE %(nonces)s (
|
||||
server_url VARCHAR,
|
||||
timestamp INTEGER,
|
||||
salt CHAR(40),
|
||||
UNIQUE(server_url, timestamp, salt)
|
||||
);
|
||||
"""
|
||||
|
||||
create_assoc_sql = """
|
||||
CREATE TABLE %(associations)s
|
||||
(
|
||||
server_url VARCHAR(2047),
|
||||
handle VARCHAR(255),
|
||||
secret BLOB(128),
|
||||
issued INTEGER,
|
||||
lifetime INTEGER,
|
||||
assoc_type VARCHAR(64),
|
||||
PRIMARY KEY (server_url, handle)
|
||||
);
|
||||
"""
|
||||
|
||||
set_assoc_sql = ('INSERT OR REPLACE INTO %(associations)s '
|
||||
'(server_url, handle, secret, issued, '
|
||||
'lifetime, assoc_type) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?);')
|
||||
get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type '
|
||||
'FROM %(associations)s WHERE server_url = ?;')
|
||||
get_assoc_sql = (
|
||||
'SELECT handle, secret, issued, lifetime, assoc_type '
|
||||
'FROM %(associations)s WHERE server_url = ? AND handle = ?;')
|
||||
|
||||
get_expired_sql = ('SELECT server_url '
|
||||
'FROM %(associations)s WHERE issued + lifetime < ?;')
|
||||
|
||||
remove_assoc_sql = ('DELETE FROM %(associations)s '
|
||||
'WHERE server_url = ? AND handle = ?;')
|
||||
|
||||
clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < ?;'
|
||||
|
||||
add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (?, ?, ?);'
|
||||
|
||||
clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < ?;'
|
||||
|
||||
def blobEncode(self, s):
|
||||
return memoryview(s)
|
||||
|
||||
def useNonce(self, *args, **kwargs):
|
||||
# Older versions of the sqlite wrapper do not raise
|
||||
# IntegrityError as they should, so we have to detect the
|
||||
# message from the OperationalError.
|
||||
try:
|
||||
return super(SQLiteStore, self).useNonce(*args, **kwargs)
|
||||
except self.exceptions.OperationalError as why:
|
||||
if re.match('^columns .* are not unique$', str(why)):
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class MySQLStore(SQLStore):
|
||||
"""
|
||||
This is a MySQL-based specialization of C{L{SQLStore}}.
|
||||
|
||||
Uses InnoDB tables for transaction support.
|
||||
|
||||
To create an instance, see C{L{SQLStore.__init__}}. To create the
|
||||
tables it will use, see C{L{SQLStore.createTables}}.
|
||||
|
||||
All other methods are implementation details.
|
||||
"""
|
||||
|
||||
try:
|
||||
import MySQLdb as exceptions
|
||||
except ImportError:
|
||||
exceptions = None
|
||||
|
||||
create_nonce_sql = """
|
||||
CREATE TABLE %(nonces)s (
|
||||
server_url BLOB NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
salt CHAR(40) NOT NULL,
|
||||
PRIMARY KEY (server_url(255), timestamp, salt)
|
||||
)
|
||||
ENGINE=InnoDB;
|
||||
"""
|
||||
|
||||
create_assoc_sql = """
|
||||
CREATE TABLE %(associations)s
|
||||
(
|
||||
server_url BLOB NOT NULL,
|
||||
handle VARCHAR(255) NOT NULL,
|
||||
secret BLOB NOT NULL,
|
||||
issued INTEGER NOT NULL,
|
||||
lifetime INTEGER NOT NULL,
|
||||
assoc_type VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (server_url(255), handle)
|
||||
)
|
||||
ENGINE=InnoDB;
|
||||
"""
|
||||
|
||||
set_assoc_sql = ('REPLACE INTO %(associations)s '
|
||||
'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
|
||||
get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
|
||||
' FROM %(associations)s WHERE server_url = %%s;')
|
||||
get_expired_sql = ('SELECT server_url '
|
||||
'FROM %(associations)s WHERE issued + lifetime < %%s;')
|
||||
|
||||
get_assoc_sql = (
|
||||
'SELECT handle, secret, issued, lifetime, assoc_type'
|
||||
' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
|
||||
remove_assoc_sql = ('DELETE FROM %(associations)s '
|
||||
'WHERE server_url = %%s AND handle = %%s;')
|
||||
|
||||
clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
|
||||
|
||||
add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
|
||||
|
||||
clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
|
||||
|
||||
|
||||
class PostgreSQLStore(SQLStore):
|
||||
"""
|
||||
This is a PostgreSQL-based specialization of C{L{SQLStore}}.
|
||||
|
||||
To create an instance, see C{L{SQLStore.__init__}}. To create the
|
||||
tables it will use, see C{L{SQLStore.createTables}}.
|
||||
|
||||
All other methods are implementation details.
|
||||
"""
|
||||
exceptions = None
|
||||
|
||||
create_nonce_sql = """
|
||||
CREATE TABLE %(nonces)s (
|
||||
server_url VARCHAR(2047) NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
salt CHAR(40) NOT NULL,
|
||||
PRIMARY KEY (server_url, timestamp, salt)
|
||||
);
|
||||
"""
|
||||
|
||||
create_assoc_sql = """
|
||||
CREATE TABLE %(associations)s
|
||||
(
|
||||
server_url VARCHAR(2047) NOT NULL,
|
||||
handle VARCHAR(255) NOT NULL,
|
||||
secret BYTEA NOT NULL,
|
||||
issued INTEGER NOT NULL,
|
||||
lifetime INTEGER NOT NULL,
|
||||
assoc_type VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (server_url, handle),
|
||||
CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128)
|
||||
);
|
||||
"""
|
||||
|
||||
def db_set_assoc(self, server_url, handle, secret, issued, lifetime,
|
||||
assoc_type):
|
||||
"""
|
||||
Set an association. This is implemented as a method because
|
||||
REPLACE INTO is not supported by PostgreSQL (and is not
|
||||
standard SQL).
|
||||
"""
|
||||
result = self.db_get_assoc(server_url, handle)
|
||||
rows = self.cur.fetchall()
|
||||
if len(rows):
|
||||
# Update the table since this associations already exists.
|
||||
return self.db_update_assoc(secret, issued, lifetime, assoc_type,
|
||||
server_url, handle)
|
||||
else:
|
||||
# Insert a new record because this association wasn't
|
||||
# found.
|
||||
return self.db_new_assoc(server_url, handle, secret, issued,
|
||||
lifetime, assoc_type)
|
||||
|
||||
new_assoc_sql = ('INSERT INTO %(associations)s '
|
||||
'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
|
||||
update_assoc_sql = ('UPDATE %(associations)s SET '
|
||||
'secret = %%s, issued = %%s, '
|
||||
'lifetime = %%s, assoc_type = %%s '
|
||||
'WHERE server_url = %%s AND handle = %%s;')
|
||||
get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
|
||||
' FROM %(associations)s WHERE server_url = %%s;')
|
||||
get_expired_sql = ('SELECT server_url '
|
||||
'FROM %(associations)s WHERE issued + lifetime < %%s;')
|
||||
|
||||
get_assoc_sql = (
|
||||
'SELECT handle, secret, issued, lifetime, assoc_type'
|
||||
' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
|
||||
remove_assoc_sql = ('DELETE FROM %(associations)s '
|
||||
'WHERE server_url = %%s AND handle = %%s;')
|
||||
|
||||
clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
|
||||
|
||||
add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
|
||||
|
||||
clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
|
||||
|
||||
def blobEncode(self, blob):
|
||||
from psycopg2 import Binary
|
||||
|
||||
return Binary(blob)
|
||||
|
||||
def blobDecode(self, blob):
|
||||
return blob.tobytes()
|
||||
Reference in New Issue
Block a user