This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -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']

View File

@@ -0,0 +1,399 @@
"""
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
logger = logging.getLogger(__name__)
_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:
logger.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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -0,0 +1,510 @@
"""
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
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.
"""
try:
import psycopg2
except ImportError:
from psycopg2cffi import compat
compat.register()
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()