123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- # -*- coding: utf-8 -*-
- """
- flask_session.sessions
- ~~~~~~~~~~~~~~~~~~~~~~
- Server-side Sessions and SessionInterfaces.
- :copyright: (c) 2014 by Shipeng Feng.
- :license: BSD, see LICENSE for more details.
- """
- import sys
- import time
- from datetime import datetime
- from uuid import uuid4
- try:
- import cPickle as pickle
- except ImportError:
- import pickle
- from flask.sessions import SessionInterface as FlaskSessionInterface
- from flask.sessions import SessionMixin
- from werkzeug.datastructures import CallbackDict
- from itsdangerous import Signer, BadSignature, want_bytes
- PY2 = sys.version_info[0] == 2
- if not PY2:
- text_type = str
- else:
- text_type = unicode
- def total_seconds(td):
- return td.days * 60 * 60 * 24 + td.seconds
- class ServerSideSession(CallbackDict, SessionMixin):
- """Baseclass for server-side based sessions."""
- def __init__(self, initial=None, sid=None, permanent=None):
- def on_update(self):
- self.modified = True
- CallbackDict.__init__(self, initial, on_update)
- self.sid = sid
- if permanent:
- self.permanent = permanent
- self.modified = False
- class RedisSession(ServerSideSession):
- pass
- class MemcachedSession(ServerSideSession):
- pass
- class FileSystemSession(ServerSideSession):
- pass
- class MongoDBSession(ServerSideSession):
- pass
- class SqlAlchemySession(ServerSideSession):
- pass
- class SessionInterface(FlaskSessionInterface):
- def _generate_sid(self):
- return str(uuid4())
- def _get_signer(self, app):
- if not app.secret_key:
- return None
- return Signer(app.secret_key, salt='flask-session',
- key_derivation='hmac')
- class NullSessionInterface(SessionInterface):
- """Used to open a :class:`flask.sessions.NullSession` instance.
- """
- def open_session(self, app, request):
- return None
- class RedisSessionInterface(SessionInterface):
- """Uses the Redis key-value store as a session backend.
- .. versionadded:: 0.2
- The `use_signer` parameter was added.
- :param redis: A ``redis.Redis`` instance.
- :param key_prefix: A prefix that is added to all Redis store keys.
- :param use_signer: Whether to sign the session id cookie or not.
- :param permanent: Whether to use permanent session or not.
- """
- serializer = pickle
- session_class = RedisSession
- def __init__(self, redis, key_prefix, use_signer=False, permanent=True):
- if redis is None:
- from redis import Redis
- redis = Redis()
- self.redis = redis
- self.key_prefix = key_prefix
- self.use_signer = use_signer
- self.permanent = permanent
- self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
- def open_session(self, app, request):
- sid = request.cookies.get(app.session_cookie_name)
- if not sid:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- if self.use_signer:
- signer = self._get_signer(app)
- if signer is None:
- return None
- try:
- sid_as_bytes = signer.unsign(sid)
- sid = sid_as_bytes.decode()
- except BadSignature:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- if not PY2 and not isinstance(sid, text_type):
- sid = sid.decode('utf-8', 'strict')
- val = self.redis.get(self.key_prefix + sid)
- if val is not None:
- try:
- data = self.serializer.loads(val)
- return self.session_class(data, sid=sid)
- except:
- return self.session_class(sid=sid, permanent=self.permanent)
- return self.session_class(sid=sid, permanent=self.permanent)
- def save_session(self, app, session, response):
- domain = self.get_cookie_domain(app)
- path = self.get_cookie_path(app)
- if not session:
- if session.modified:
- self.redis.delete(self.key_prefix + session.sid)
- response.delete_cookie(app.session_cookie_name,
- domain=domain, path=path)
- return
- # Modification case. There are upsides and downsides to
- # emitting a set-cookie header each request. The behavior
- # is controlled by the :meth:`should_set_cookie` method
- # which performs a quick check to figure out if the cookie
- # should be set or not. This is controlled by the
- # SESSION_REFRESH_EACH_REQUEST config flag as well as
- # the permanent flag on the session itself.
- # if not self.should_set_cookie(app, session):
- # return
- conditional_cookie_kwargs = {}
- httponly = self.get_cookie_httponly(app)
- secure = self.get_cookie_secure(app)
- if self.has_same_site_capability:
- conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
- expires = self.get_expiration_time(app, session)
- val = self.serializer.dumps(dict(session))
- self.redis.setex(name=self.key_prefix + session.sid, value=val,
- time=total_seconds(app.permanent_session_lifetime))
- if self.use_signer:
- session_id = self._get_signer(app).sign(want_bytes(session.sid))
- else:
- session_id = session.sid
- response.set_cookie(app.session_cookie_name, session_id,
- expires=expires, httponly=httponly,
- domain=domain, path=path, secure=secure,
- **conditional_cookie_kwargs)
- class MemcachedSessionInterface(SessionInterface):
- """A Session interface that uses memcached as backend.
- .. versionadded:: 0.2
- The `use_signer` parameter was added.
- :param client: A ``memcache.Client`` instance.
- :param key_prefix: A prefix that is added to all Memcached store keys.
- :param use_signer: Whether to sign the session id cookie or not.
- :param permanent: Whether to use permanent session or not.
- """
- serializer = pickle
- session_class = MemcachedSession
- def __init__(self, client, key_prefix, use_signer=False, permanent=True):
- if client is None:
- client = self._get_preferred_memcache_client()
- if client is None:
- raise RuntimeError('no memcache module found')
- self.client = client
- self.key_prefix = key_prefix
- self.use_signer = use_signer
- self.permanent = permanent
- self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
- def _get_preferred_memcache_client(self):
- servers = ['127.0.0.1:11211']
- try:
- import pylibmc
- except ImportError:
- pass
- else:
- return pylibmc.Client(servers)
- try:
- import memcache
- except ImportError:
- pass
- else:
- return memcache.Client(servers)
- def _get_memcache_timeout(self, timeout):
- """
- Memcached deals with long (> 30 days) timeouts in a special
- way. Call this function to obtain a safe value for your timeout.
- """
- if timeout > 2592000: # 60*60*24*30, 30 days
- # See http://code.google.com/p/memcached/wiki/FAQ
- # "You can set expire times up to 30 days in the future. After that
- # memcached interprets it as a date, and will expire the item after
- # said date. This is a simple (but obscure) mechanic."
- #
- # This means that we have to switch to absolute timestamps.
- timeout += int(time.time())
- return timeout
- def open_session(self, app, request):
- sid = request.cookies.get(app.session_cookie_name)
- if not sid:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- if self.use_signer:
- signer = self._get_signer(app)
- if signer is None:
- return None
- try:
- sid_as_bytes = signer.unsign(sid)
- sid = sid_as_bytes.decode()
- except BadSignature:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- full_session_key = self.key_prefix + sid
- if PY2 and isinstance(full_session_key, unicode):
- full_session_key = full_session_key.encode('utf-8')
- val = self.client.get(full_session_key)
- if val is not None:
- try:
- if not PY2:
- val = want_bytes(val)
- data = self.serializer.loads(val)
- return self.session_class(data, sid=sid)
- except:
- return self.session_class(sid=sid, permanent=self.permanent)
- return self.session_class(sid=sid, permanent=self.permanent)
- def save_session(self, app, session, response):
- domain = self.get_cookie_domain(app)
- path = self.get_cookie_path(app)
- full_session_key = self.key_prefix + session.sid
- if PY2 and isinstance(full_session_key, unicode):
- full_session_key = full_session_key.encode('utf-8')
- if not session:
- if session.modified:
- self.client.delete(full_session_key)
- response.delete_cookie(app.session_cookie_name,
- domain=domain, path=path)
- return
- conditional_cookie_kwargs = {}
- httponly = self.get_cookie_httponly(app)
- secure = self.get_cookie_secure(app)
- if self.has_same_site_capability:
- conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
- expires = self.get_expiration_time(app, session)
- if not PY2:
- val = self.serializer.dumps(dict(session), 0)
- else:
- val = self.serializer.dumps(dict(session))
- self.client.set(full_session_key, val, self._get_memcache_timeout(
- total_seconds(app.permanent_session_lifetime)))
- if self.use_signer:
- session_id = self._get_signer(app).sign(want_bytes(session.sid))
- else:
- session_id = session.sid
- response.set_cookie(app.session_cookie_name, session_id,
- expires=expires, httponly=httponly,
- domain=domain, path=path, secure=secure,
- **conditional_cookie_kwargs)
- class FileSystemSessionInterface(SessionInterface):
- """Uses the :class:`cachelib.file.FileSystemCache` as a session backend.
- .. versionadded:: 0.2
- The `use_signer` parameter was added.
- :param cache_dir: the directory where session files are stored.
- :param threshold: the maximum number of items the session stores before it
- starts deleting some.
- :param mode: the file mode wanted for the session files, default 0600
- :param key_prefix: A prefix that is added to FileSystemCache store keys.
- :param use_signer: Whether to sign the session id cookie or not.
- :param permanent: Whether to use permanent session or not.
- """
- session_class = FileSystemSession
- def __init__(self, cache_dir, threshold, mode, key_prefix,
- use_signer=False, permanent=True):
- from cachelib.file import FileSystemCache
- self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode)
- self.key_prefix = key_prefix
- self.use_signer = use_signer
- self.permanent = permanent
- self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
- def open_session(self, app, request):
- sid = request.cookies.get(app.session_cookie_name)
- if not sid:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- if self.use_signer:
- signer = self._get_signer(app)
- if signer is None:
- return None
- try:
- sid_as_bytes = signer.unsign(sid)
- sid = sid_as_bytes.decode()
- except BadSignature:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- data = self.cache.get(self.key_prefix + sid)
- if data is not None:
- return self.session_class(data, sid=sid)
- return self.session_class(sid=sid, permanent=self.permanent)
- def save_session(self, app, session, response):
- domain = self.get_cookie_domain(app)
- path = self.get_cookie_path(app)
- if not session:
- if session.modified:
- self.cache.delete(self.key_prefix + session.sid)
- response.delete_cookie(app.session_cookie_name,
- domain=domain, path=path)
- return
- conditional_cookie_kwargs = {}
- httponly = self.get_cookie_httponly(app)
- secure = self.get_cookie_secure(app)
- if self.has_same_site_capability:
- conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
- expires = self.get_expiration_time(app, session)
- data = dict(session)
- self.cache.set(self.key_prefix + session.sid, data,
- total_seconds(app.permanent_session_lifetime))
- if self.use_signer:
- session_id = self._get_signer(app).sign(want_bytes(session.sid))
- else:
- session_id = session.sid
- response.set_cookie(app.session_cookie_name, session_id,
- expires=expires, httponly=httponly,
- domain=domain, path=path, secure=secure,
- **conditional_cookie_kwargs)
- class MongoDBSessionInterface(SessionInterface):
- """A Session interface that uses mongodb as backend.
- .. versionadded:: 0.2
- The `use_signer` parameter was added.
- :param client: A ``pymongo.MongoClient`` instance.
- :param db: The database you want to use.
- :param collection: The collection you want to use.
- :param key_prefix: A prefix that is added to all MongoDB store keys.
- :param use_signer: Whether to sign the session id cookie or not.
- :param permanent: Whether to use permanent session or not.
- """
- serializer = pickle
- session_class = MongoDBSession
- def __init__(self, client, db, collection, key_prefix, use_signer=False,
- permanent=True):
- if client is None:
- from pymongo import MongoClient
- client = MongoClient()
- self.client = client
- self.store = client[db][collection]
- self.key_prefix = key_prefix
- self.use_signer = use_signer
- self.permanent = permanent
- self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
- def open_session(self, app, request):
- sid = request.cookies.get(app.session_cookie_name)
- if not sid:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- if self.use_signer:
- signer = self._get_signer(app)
- if signer is None:
- return None
- try:
- sid_as_bytes = signer.unsign(sid)
- sid = sid_as_bytes.decode()
- except BadSignature:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- store_id = self.key_prefix + sid
- document = self.store.find_one({'id': store_id})
- if document and document.get('expiration') <= datetime.utcnow():
- # Delete expired session
- self.store.remove({'id': store_id})
- document = None
- if document is not None:
- try:
- val = document['val']
- data = self.serializer.loads(want_bytes(val))
- return self.session_class(data, sid=sid)
- except:
- return self.session_class(sid=sid, permanent=self.permanent)
- return self.session_class(sid=sid, permanent=self.permanent)
- def save_session(self, app, session, response):
- domain = self.get_cookie_domain(app)
- path = self.get_cookie_path(app)
- store_id = self.key_prefix + session.sid
- if not session:
- if session.modified:
- self.store.remove({'id': store_id})
- response.delete_cookie(app.session_cookie_name,
- domain=domain, path=path)
- return
- conditional_cookie_kwargs = {}
- httponly = self.get_cookie_httponly(app)
- secure = self.get_cookie_secure(app)
- if self.has_same_site_capability:
- conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
- expires = self.get_expiration_time(app, session)
- val = self.serializer.dumps(dict(session))
- self.store.update({'id': store_id},
- {'id': store_id,
- 'val': val,
- 'expiration': expires}, True)
- if self.use_signer:
- session_id = self._get_signer(app).sign(want_bytes(session.sid))
- else:
- session_id = session.sid
- response.set_cookie(app.session_cookie_name, session_id,
- expires=expires, httponly=httponly,
- domain=domain, path=path, secure=secure,
- **conditional_cookie_kwargs)
- class SqlAlchemySessionInterface(SessionInterface):
- """Uses the Flask-SQLAlchemy from a flask app as a session backend.
- .. versionadded:: 0.2
- :param app: A Flask app instance.
- :param db: A Flask-SQLAlchemy instance.
- :param table: The table name you want to use.
- :param key_prefix: A prefix that is added to all store keys.
- :param use_signer: Whether to sign the session id cookie or not.
- :param permanent: Whether to use permanent session or not.
- """
- serializer = pickle
- session_class = SqlAlchemySession
- def __init__(self, app, db, table, key_prefix, use_signer=False,
- permanent=True):
- if db is None:
- from flask_sqlalchemy import SQLAlchemy
- db = SQLAlchemy(app)
- self.db = db
- self.key_prefix = key_prefix
- self.use_signer = use_signer
- self.permanent = permanent
- self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
- class Session(self.db.Model):
- __tablename__ = table
- id = self.db.Column(self.db.Integer, primary_key=True)
- session_id = self.db.Column(self.db.String(255), unique=True)
- data = self.db.Column(self.db.LargeBinary)
- expiry = self.db.Column(self.db.DateTime)
- def __init__(self, session_id, data, expiry):
- self.session_id = session_id
- self.data = data
- self.expiry = expiry
- def __repr__(self):
- return '<Session data %s>' % self.data
- # self.db.create_all()
- self.sql_session_model = Session
- def open_session(self, app, request):
- sid = request.cookies.get(app.session_cookie_name)
- if not sid:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- if self.use_signer:
- signer = self._get_signer(app)
- if signer is None:
- return None
- try:
- sid_as_bytes = signer.unsign(sid)
- sid = sid_as_bytes.decode()
- except BadSignature:
- sid = self._generate_sid()
- return self.session_class(sid=sid, permanent=self.permanent)
- store_id = self.key_prefix + sid
- saved_session = self.sql_session_model.query.filter_by(
- session_id=store_id).first()
- if saved_session and saved_session.expiry <= datetime.utcnow():
- # Delete expired session
- self.db.session.delete(saved_session)
- self.db.session.commit()
- saved_session = None
- if saved_session:
- try:
- val = saved_session.data
- data = self.serializer.loads(want_bytes(val))
- return self.session_class(data, sid=sid)
- except:
- return self.session_class(sid=sid, permanent=self.permanent)
- return self.session_class(sid=sid, permanent=self.permanent)
- def save_session(self, app, session, response):
- domain = self.get_cookie_domain(app)
- path = self.get_cookie_path(app)
- store_id = self.key_prefix + session.sid
- saved_session = self.sql_session_model.query.filter_by(
- session_id=store_id).first()
- if not session:
- if session.modified:
- if saved_session:
- self.db.session.delete(saved_session)
- self.db.session.commit()
- response.delete_cookie(app.session_cookie_name,
- domain=domain, path=path)
- return
- conditional_cookie_kwargs = {}
- httponly = self.get_cookie_httponly(app)
- secure = self.get_cookie_secure(app)
- if self.has_same_site_capability:
- conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
- expires = self.get_expiration_time(app, session)
- val = self.serializer.dumps(dict(session))
- if saved_session:
- saved_session.data = val
- saved_session.expiry = expires
- self.db.session.commit()
- else:
- new_session = self.sql_session_model(store_id, val, expires)
- self.db.session.add(new_session)
- self.db.session.commit()
- if self.use_signer:
- session_id = self._get_signer(app).sign(want_bytes(session.sid))
- else:
- session_id = session.sid
- response.set_cookie(app.session_cookie_name, session_id,
- expires=expires, httponly=httponly,
- domain=domain, path=path, secure=secure,
- **conditional_cookie_kwargs)
|