pax_global_header00006660000000000000000000000064144272322540014517gustar00rootroot0000000000000052 comment=0baf8cb2b5df62fbc71fa15684919ffd4fa974a9 flask-session-0.5.0/000077500000000000000000000000001442723225400143025ustar00rootroot00000000000000flask-session-0.5.0/.editorconfig000066400000000000000000000002501442723225400167540ustar00rootroot00000000000000root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 max_line_length = 88 flask-session-0.5.0/.github/000077500000000000000000000000001442723225400156425ustar00rootroot00000000000000flask-session-0.5.0/.github/workflows/000077500000000000000000000000001442723225400176775ustar00rootroot00000000000000flask-session-0.5.0/.github/workflows/publish.yaml000066400000000000000000000043051442723225400222330ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b with: python-version: '3.x' - run: pip install build # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - run: python -m build # Generate hashes used for provenance. - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce with: path: ./dist provenance: needs: ['build'] permissions: actions: read id-token: write contents: write # Can't pin with hash due to how this workflow works. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0 with: base64-subjects: ${{ needs.build.outputs.hash }} create-release: # Upload the sdist, wheels, and provenance to a GitHub release. They remain # available as build artifacts for a while as well. needs: ['provenance'] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - name: create release run: > gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} *.intoto.jsonl/* artifact/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: ['provenance'] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: 'publish' runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - uses: pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d with: packages-dir: artifact/ flask-session-0.5.0/.gitignore000066400000000000000000000002171442723225400162720ustar00rootroot00000000000000/.idea/ /.vscode/ /.venv/ __pycache__/ /dist/ *.egg-info/ /.pytest_cache/ /.coverage /.coverage.* /htmlcov/ /.mypy_cache/ /.tox/ /docs/_build/ flask-session-0.5.0/.readthedocs.yaml000066400000000000000000000003161442723225400175310ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" python: install: - requirements: requirements/docs.txt - method: pip path: . sphinx: builder: html fail_on_warning: true flask-session-0.5.0/CHANGES.rst000066400000000000000000000030711442723225400161050ustar00rootroot00000000000000Version 0.5.0 ------------- Released 2023-05-11 - Drop support for Python < 3.7. - Switch to ``pyproject.toml`` and Flit for packaging. - Move to Pallets Community Ecosystem for community-driven maintenance. - Replace use of ``session_cookie_name`` for Flask 2.3 compatibility. Version 0.4.1 ------------- - Temporarily pin Flask < 2.3. Version 0.4.0 ------------- - Added support for ``SESSION_COOKIE_SAMESITE``. Version 0.3.2 ------------- - Changed ``werkzeug.contrib.cache`` to ``cachelib``. Version 0.3.1 ------------- - ``SqlAlchemySessionInterface`` is using ``VARCHAR(255)`` to store session id now. - ``SqlAlchemySessionInterface`` won't run `db.create_all` anymore. Version 0.3 ----------- - ``SqlAlchemySessionInterface`` is using ``LargeBinary`` type to store data now. - Fixed ``MongoDBSessionInterface`` ``delete`` method not found. - Fixed ``TypeError`` when getting ``store_id`` using a signer. Version 0.2.3 ------------- - Fixed signing failure in Python 3. - Fixed ``MongoDBSessionInterface`` failure in Python 3. - Fixed ``SqlAlchemySessionInterface`` failure in Python 3. - Fixed ``StrictRedis`` support. Version 0.2.2 ------------- - Added support for non-permanent session. Version 0.2.1 ------------- - Fixed signing failure. Version 0.2 ----------- - Added ``SqlAlchemySessionInterface``. - Added support for cookie session id signing. - Various bugfixes. Version 0.1.1 ------------- Fixed MongoDB backend ``InvalidDocument`` error. Version 0.1 ----------- - First public preview release. flask-session-0.5.0/LICENSE.rst000066400000000000000000000027271442723225400161260ustar00rootroot00000000000000Copyright 2014 Pallets Community Ecosystem Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-session-0.5.0/README.rst000066400000000000000000000002051442723225400157660ustar00rootroot00000000000000Flask-Session ============= Flask-Session is an extension for Flask that adds support for server-side sessions to your application. flask-session-0.5.0/docs/000077500000000000000000000000001442723225400152325ustar00rootroot00000000000000flask-session-0.5.0/docs/Makefile000066400000000000000000000011721442723225400166730ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) flask-session-0.5.0/docs/api.rst000066400000000000000000000010231442723225400165310ustar00rootroot00000000000000API --- .. module:: flask_session .. autoclass:: Session :members: init_app .. autoclass:: flask_session.sessions.ServerSideSession .. attribute:: sid Session id, internally we use :func:`uuid.uuid4` to generate one session id. You can access it with ``session.sid``. .. autoclass:: NullSessionInterface .. autoclass:: RedisSessionInterface .. autoclass:: MemcachedSessionInterface .. autoclass:: FileSystemSessionInterface .. autoclass:: MongoDBSessionInterface .. autoclass:: SqlAlchemySessionInterface flask-session-0.5.0/docs/changes.rst000066400000000000000000000000551442723225400173740ustar00rootroot00000000000000Changes ======= .. include:: ../CHANGES.rst flask-session-0.5.0/docs/conf.py000066400000000000000000000012441442723225400165320ustar00rootroot00000000000000import importlib.metadata project = "Flask-Session" author = "Pallets Community Ecosystem" copyright = f"2014, {author}" release = importlib.metadata.version("Flask-Session") extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", ] intersphinx_mapping = { "python": ("http://docs.python.org/", None), "flask": ("http://flask.palletsprojects.com/", None), "werkzeug": ("http://werkzeug.palletsprojects.com/", None), "flask-sqlalchemy": ("http://flask-sqlalchemy.palletsprojects.com/", None), } html_theme = "alabaster" html_theme_options = { "github_button": True, "github_user": "pallets-eco", "github_repo": "flask-session", } flask-session-0.5.0/docs/config.rst000066400000000000000000000126451442723225400172410ustar00rootroot00000000000000Configuration ============= The following configuration values exist for Flask-Session. Flask-Session loads these values from your Flask application config, so you should configure your app first before you pass it to Flask-Session. Note that these values cannot be modified after the ``init_app`` was applyed so make sure to not modify them at runtime. We are not supplying something like ``SESSION_REDIS_HOST`` and ``SESSION_REDIS_PORT``, if you want to use the ``RedisSessionInterface``, you should configure ``SESSION_REDIS`` to your own ``redis.Redis`` instance. This gives you more flexibility, like maybe you want to use the same ``redis.Redis`` instance for cache purpose too, then you do not need to keep two ``redis.Redis`` instance in the same process. The following configuration values are builtin configuration values within Flask itself that are related to session. **They are all understood by Flask-Session, for example, you should use PERMANENT_SESSION_LIFETIME to control your session lifetime.** ================================= ========================================= ``SESSION_COOKIE_NAME`` the name of the session cookie ``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If this is not set, the cookie will be valid for all subdomains of ``SERVER_NAME``. ``SESSION_COOKIE_PATH`` the path for the session cookie. If this is not set the cookie will be valid for all of ``APPLICATION_ROOT`` or if that is not set for ``'/'``. ``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set with the httponly flag. Defaults to `True`. ``SESSION_COOKIE_SECURE`` controls if the cookie should be set with the secure flag. Defaults to `False`. ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. Starting with Flask 0.8 this can also be an integer representing seconds. ================================= ========================================= A list of configuration keys also understood by the extension: ============================= ============================================== ``SESSION_TYPE`` Specifies which type of session interface to use. Built-in session types: - **null**: NullSessionInterface (default) - **redis**: RedisSessionInterface - **memcached**: MemcachedSessionInterface - **filesystem**: FileSystemSessionInterface - **mongodb**: MongoDBSessionInterface - **sqlalchemy**: SqlAlchemySessionInterface ``SESSION_PERMANENT`` Whether use permanent session or not, default to be ``True`` ``SESSION_USE_SIGNER`` Whether sign the session cookie sid or not, if set to ``True``, you have to set :attr:`flask.Flask.secret_key`, default to be ``False`` ``SESSION_KEY_PREFIX`` A prefix that is added before all session keys. This makes it possible to use the same backend storage server for different apps, default "session:" ``SESSION_REDIS`` A ``redis.Redis`` instance, default connect to ``127.0.0.1:6379`` ``SESSION_MEMCACHED`` A ``memcache.Client`` instance, default connect to ``127.0.0.1:11211`` ``SESSION_FILE_DIR`` The directory where session files are stored. Default to use `flask_session` directory under current working directory. ``SESSION_FILE_THRESHOLD`` The maximum number of items the session stores before it starts deleting some, default 500 ``SESSION_FILE_MODE`` The file mode wanted for the session files, default 0600 ``SESSION_MONGODB`` A ``pymongo.MongoClient`` instance, default connect to ``127.0.0.1:27017`` ``SESSION_MONGODB_DB`` The MongoDB database you want to use, default "flask_session" ``SESSION_MONGODB_COLLECT`` The MongoDB collection you want to use, default "sessions" ``SESSION_SQLALCHEMY`` A ``flask_sqlalchemy.SQLAlchemy`` instance whose database connection URI is configured using the ``SQLALCHEMY_DATABASE_URI`` parameter ``SESSION_SQLALCHEMY_TABLE`` The name of the SQL table you want to use, default "sessions" ============================= ============================================== Basically you only need to configure ``SESSION_TYPE``. .. note:: By default, all non-null sessions in Flask-Session are permanent. .. versionadded:: 0.2 ``SESSION_TYPE``: **sqlalchemy**, ``SESSION_USE_SIGNER`` flask-session-0.5.0/docs/index.rst000066400000000000000000000007131442723225400170740ustar00rootroot00000000000000Flask-Session ============= Flask-Session is an extension for `Flask`_ that adds support for server-side sessions to your application. .. _Flask: http://flask.palletsprojects.com/ Installation ------------ Install from PyPI using an installer such as pip: .. code-block:: text $ pip install Flask-Session Table of Contents ----------------- .. toctree:: :maxdepth: 2 quickstart interfaces config api license changes flask-session-0.5.0/docs/interfaces.rst000066400000000000000000000031121442723225400201040ustar00rootroot00000000000000Built-in Session Interfaces =========================== .. currentmodule:: flask_session :class:`NullSessionInterface` ----------------------------- If you do not configure a different ``SESSION_TYPE``, this will be used to generate nicer error messages. Will allow read-only access to the empty session but fail on setting. :class:`RedisSessionInterface` ------------------------------ Uses the Redis key-value store as a session backend. (`redis-py`_ required) Relevant configuration values: - SESSION_REDIS :class:`MemcachedSessionInterface` ---------------------------------- Uses the Memcached as a session backend. (`pylibmc`_ or `memcache`_ required) - SESSION_MEMCACHED :class:`FileSystemSessionInterface` ----------------------------------- Uses the :class:`cachelib.file.FileSystemCache` as a session backend. - SESSION_FILE_DIR - SESSION_FILE_THRESHOLD - SESSION_FILE_MODE :class:`MongoDBSessionInterface` -------------------------------- Uses the MongoDB as a session backend. (`pymongo`_ required) - SESSION_MONGODB - SESSION_MONGODB_DB - SESSION_MONGODB_COLLECT .. _redis-py: https://github.com/andymccurdy/redis-py .. _pylibmc: http://sendapatch.se/projects/pylibmc/ .. _memcache: https://github.com/linsomniac/python-memcached .. _pymongo: http://api.mongodb.org/python/current/index.html :class:`SqlAlchemySessionInterface` ----------------------------------- .. versionadded:: 0.2 Uses SQLAlchemy as a session backend. (`Flask-SQLAlchemy`_ required) - SESSION_SQLALCHEMY - SESSION_SQLALCHEMY_TABLE .. _Flask-SQLAlchemy: https://pythonhosted.org/Flask-SQLAlchemy/ flask-session-0.5.0/docs/license.rst000066400000000000000000000001071442723225400174040ustar00rootroot00000000000000BSD-3-Clause License ==================== .. include:: ../LICENSE.rst flask-session-0.5.0/docs/quickstart.rst000066400000000000000000000015751442723225400201660ustar00rootroot00000000000000Quick Start =========== .. currentmodule:: flask_session Create your Flask application, load the configuration of choice, and then create the :class:`Session` object by passing it the application. The ``Session`` instance is not used for direct access, you should always use :class:`flask.session`. .. code-block:: python from flask import Flask, session from flask_session import Session app = Flask(__name__) # Check Configuration section for more details SESSION_TYPE = 'redis' app.config.from_object(__name__) Session(app) @app.route('/set/') def set(): session['key'] = 'value' return 'ok' @app.route('/get/') def get(): return session.get('key', 'not set') You may also set up your application later using :meth:`~Session.init_app` method. .. code-block:: python sess = Session() sess.init_app(app) flask-session-0.5.0/examples/000077500000000000000000000000001442723225400161205ustar00rootroot00000000000000flask-session-0.5.0/examples/hello.py000066400000000000000000000005571442723225400176040ustar00rootroot00000000000000from flask import Flask, session from flask_session import Session SESSION_TYPE = 'redis' app = Flask(__name__) app.config.from_object(__name__) Session(app) @app.route('/set/') def set(): session['key'] = 'value' return 'ok' @app.route('/get/') def get(): return session.get('key', 'not set') if __name__ == "__main__": app.run(debug=True) flask-session-0.5.0/pyproject.toml000066400000000000000000000027321442723225400172220ustar00rootroot00000000000000[project] name = "Flask-Session" description = "Server-side session support for Flask" readme = "README.rst" license = {text = "BSD-3-Clause"} maintainers = [{name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com"}] authors = [{name = "Shipeng Feng", email = "fsp261@gmail.com"}] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Session", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", ] requires-python = ">=3.7" dependencies = [ "flask>=2.2", "cachelib", ] dynamic = ["version"] [project.urls] Documentation = "https://flasksession.readthedocs.io" Changes = "https://flasksession.readthedocs.io/changes.html" "Source Code" = "https://github.com/pallets-eco/flask-session/" "Issue Tracker" = "https://github.com/pallets-eco/flask-session/issues/" Chat = "https://discord.gg/pallets" [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flask_session" [tool.flit.sdist] include = [ "docs/", "requirements/", "CHANGES.rst", "LICENSE.rst", "test_session.py", ] exclude = ["docs/_build/"] flask-session-0.5.0/requirements/000077500000000000000000000000001442723225400170255ustar00rootroot00000000000000flask-session-0.5.0/requirements/docs.in000066400000000000000000000000071442723225400203020ustar00rootroot00000000000000sphinx flask-session-0.5.0/requirements/docs.txt000066400000000000000000000017021442723225400205160ustar00rootroot00000000000000# SHA1:b9aaf35e80441f415c3a3d3c53695d0efded116a # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # alabaster==0.7.13 # via sphinx babel==2.12.1 # via sphinx certifi==2023.5.7 # via requests charset-normalizer==3.1.0 # via requests docutils==0.19 # via sphinx idna==3.4 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.2 # via sphinx markupsafe==2.1.2 # via jinja2 packaging==23.1 # via sphinx pygments==2.15.1 # via sphinx requests==2.30.0 # via sphinx snowballstemmer==2.2.0 # via sphinx sphinx==7.0.0 # via -r requirements/docs.in sphinxcontrib-applehelp==1.0.4 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx urllib3==2.0.2 # via requests flask-session-0.5.0/src/000077500000000000000000000000001442723225400150715ustar00rootroot00000000000000flask-session-0.5.0/src/flask_session/000077500000000000000000000000001442723225400177345ustar00rootroot00000000000000flask-session-0.5.0/src/flask_session/__init__.py000066400000000000000000000076501442723225400220550ustar00rootroot00000000000000import os from .sessions import NullSessionInterface, RedisSessionInterface, \ MemcachedSessionInterface, FileSystemSessionInterface, \ MongoDBSessionInterface, SqlAlchemySessionInterface __version__ = '0.5.0' class Session(object): """This class is used to add Server-side Session to one or more Flask applications. There are two usage modes. One is initialize the instance with a very specific Flask application:: app = Flask(__name__) Session(app) The second possibility is to create the object once and configure the application later:: sess = Session() def create_app(): app = Flask(__name__) sess.init_app(app) return app By default Flask-Session will use :class:`NullSessionInterface`, you really should configurate your app to use a different SessionInterface. .. note:: You can not use ``Session`` instance directly, what ``Session`` does is just change the :attr:`~flask.Flask.session_interface` attribute on your Flask applications. """ def __init__(self, app=None): self.app = app if app is not None: self.init_app(app) def init_app(self, app): """This is used to set up session for your app object. :param app: the Flask app object with proper configuration. """ app.session_interface = self._get_interface(app) def _get_interface(self, app): config = app.config.copy() config.setdefault('SESSION_TYPE', 'null') config.setdefault('SESSION_PERMANENT', True) config.setdefault('SESSION_USE_SIGNER', False) config.setdefault('SESSION_KEY_PREFIX', 'session:') config.setdefault('SESSION_REDIS', None) config.setdefault('SESSION_MEMCACHED', None) config.setdefault('SESSION_FILE_DIR', os.path.join(os.getcwd(), 'flask_session')) config.setdefault('SESSION_FILE_THRESHOLD', 500) config.setdefault('SESSION_FILE_MODE', 384) config.setdefault('SESSION_MONGODB', None) config.setdefault('SESSION_MONGODB_DB', 'flask_session') config.setdefault('SESSION_MONGODB_COLLECT', 'sessions') config.setdefault('SESSION_SQLALCHEMY', None) config.setdefault('SESSION_SQLALCHEMY_TABLE', 'sessions') if config['SESSION_TYPE'] == 'redis': session_interface = RedisSessionInterface( config['SESSION_REDIS'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) elif config['SESSION_TYPE'] == 'memcached': session_interface = MemcachedSessionInterface( config['SESSION_MEMCACHED'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) elif config['SESSION_TYPE'] == 'filesystem': session_interface = FileSystemSessionInterface( config['SESSION_FILE_DIR'], config['SESSION_FILE_THRESHOLD'], config['SESSION_FILE_MODE'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) elif config['SESSION_TYPE'] == 'mongodb': session_interface = MongoDBSessionInterface( config['SESSION_MONGODB'], config['SESSION_MONGODB_DB'], config['SESSION_MONGODB_COLLECT'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) elif config['SESSION_TYPE'] == 'sqlalchemy': session_interface = SqlAlchemySessionInterface( app, config['SESSION_SQLALCHEMY'], config['SESSION_SQLALCHEMY_TABLE'], config['SESSION_KEY_PREFIX'], config['SESSION_USE_SIGNER'], config['SESSION_PERMANENT']) else: session_interface = NullSessionInterface() return session_interface flask-session-0.5.0/src/flask_session/sessions.py000066400000000000000000000535221442723225400221630ustar00rootroot00000000000000import 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.config["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.config["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.config["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.config["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.config["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.config["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.config["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.config["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.config["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.config["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.config["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.config["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 '' % self.data # self.db.create_all() self.sql_session_model = Session def open_session(self, app, request): sid = request.cookies.get(app.config["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.config["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.config["SESSION_COOKIE_NAME"], session_id, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure, **conditional_cookie_kwargs) flask-session-0.5.0/test_session.py000066400000000000000000000143351442723225400174040ustar00rootroot00000000000000import unittest import tempfile import flask from flask_session import Session class FlaskSessionTestCase(unittest.TestCase): def test_null_session(self): app = flask.Flask(__name__) Session(app) def expect_exception(f, *args, **kwargs): try: f(*args, **kwargs) except RuntimeError as e: self.assertTrue(e.args and 'session is unavailable' in e.args[0]) else: self.assertTrue(False, 'expected exception') with app.test_request_context(): self.assertTrue(flask.session.get('missing_key') is None) expect_exception(flask.session.__setitem__, 'foo', 42) expect_exception(flask.session.pop, 'foo') def test_redis_session(self): app = flask.Flask(__name__) app.config['SESSION_TYPE'] = 'redis' Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] @app.route('/delete', methods=['POST']) def delete(): del flask.session['value'] return 'value deleted' c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') self.assertEqual(c.get('/get').data, b'42') c.post('/delete') def test_memcached_session(self): app = flask.Flask(__name__) app.config['SESSION_TYPE'] = 'memcached' Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] @app.route('/delete', methods=['POST']) def delete(): del flask.session['value'] return 'value deleted' c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') self.assertEqual(c.get('/get').data, b'42') c.post('/delete') def test_filesystem_session(self): app = flask.Flask(__name__) app.config['SESSION_TYPE'] = 'filesystem' app.config['SESSION_FILE_DIR'] = tempfile.gettempdir() Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] @app.route('/delete', methods=['POST']) def delete(): del flask.session['value'] return 'value deleted' c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') self.assertEqual(c.get('/get').data, b'42') c.post('/delete') def test_mongodb_session(self): app = flask.Flask(__name__) app.testing = True app.config['SESSION_TYPE'] = 'mongodb' Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] @app.route('/delete', methods=['POST']) def delete(): del flask.session['value'] return 'value deleted' c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') self.assertEqual(c.get('/get').data, b'42') c.post('/delete') def test_flasksqlalchemy_session(self): app = flask.Flask(__name__) app.debug = True app.config['SESSION_TYPE'] = 'sqlalchemy' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] @app.route('/delete', methods=['POST']) def delete(): del flask.session['value'] return 'value deleted' c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value ' b'set') self.assertEqual(c.get('/get').data, b'42') c.post('/delete') def test_flasksqlalchemy_session_with_signer(self): app = flask.Flask(__name__) app.debug = True app.secret_key = 'test_secret_key' app.config['SESSION_TYPE'] = 'sqlalchemy' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' app.config['SESSION_USE_SIGNER'] = True session = Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] @app.route('/delete', methods=['POST']) def delete(): del flask.session['value'] return 'value deleted' c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value ' b'set') self.assertEqual(c.get('/get').data, b'42') c.post('/delete') def test_session_use_signer(self): app = flask.Flask(__name__) app.secret_key = 'test_secret_key' app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_USE_SIGNER'] = True Session(app) @app.route('/set', methods=['POST']) def set(): flask.session['value'] = flask.request.form['value'] return 'value set' @app.route('/get') def get(): return flask.session['value'] c = app.test_client() self.assertEqual(c.post('/set', data={'value': '42'}).data, b'value set') self.assertEqual(c.get('/get').data, b'42') if __name__ == "__main__": unittest.main()