From 0b816a06770cf794bea50cfea5ea999f754630d4 Mon Sep 17 00:00:00 2001 From: Martin Zimmermann Date: Thu, 20 Mar 2014 15:44:05 +0100 Subject: [PATCH] store session-key in database (once generated on db creation), #74 Store a random session key used to sign and verify comment ownership once the database is initialized, not on every application startup. Currently fixed session keys in [general] session-key are migrated into the database on startup. The configuration parser will notice you about the change and suggest you to remove this option. --- docs/docs/configuration/server.rst | 9 +------- docs/docs/extras/deployment.rst | 18 --------------- docs/isso.example.cfg | 9 ++------ isso/__init__.py | 5 +---- isso/core.py | 7 +++--- isso/db/__init__.py | 29 +++++++++++++++++------- isso/db/preferences.py | 36 ++++++++++++++++++++++++++++++ 7 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 isso/db/preferences.py diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index 7d752f0..902bc4d 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -43,7 +43,6 @@ session key and hostname. Here are the default values for this section: name = host = http://localhost:8080/ max-age = 15m - session-key = ... ; python: binascii.b2a_hex(os.urandom(24)) notify = dbpath @@ -71,11 +70,6 @@ host This is useful, when your website is available on HTTP and HTTPS. -session-key - private session key to validate client cookies. If you restart the - application several times per hour for whatever reason, use a fixed - key. - max-age time range that allows users to edit/remove their own comments. See :ref:`Appendum: Timedelta ` for valid values. @@ -136,8 +130,7 @@ listen reload reload application, when the source code has changed. Useful for - development (don't forget to use a fixed `session-key`). Only works - when ``gevent`` and ``uwsgi`` are *not* available. + development. Only works with the internal webserver. profile show 10 most time consuming function in Isso after each request. Do diff --git a/docs/docs/extras/deployment.rst b/docs/docs/extras/deployment.rst index c39335d..cdd9881 100644 --- a/docs/docs/extras/deployment.rst +++ b/docs/docs/extras/deployment.rst @@ -121,15 +121,6 @@ Next, copy'n'paste to `/var/www/isso.wsgi`: application = make_app(Config.load("/path/to/isso.cfg")) -Also make sure, you set a static key because `mod_wsgi` generates a session -key per thread/process. This may result in random 403 errors when you edit or -delete comments. - -.. code-block:: ini - - [general] - ; cat /dev/urandom | strings | grep -o '[[:alnum:]]' | head -n 30 | tr -d '\n' - session-key = superrandomkey1 `mod_fastcgi `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -170,12 +161,3 @@ Next, copy'n'paste to `/var/www/isso.fcgi` (or whatever location you prefer): application = make_app(Config.load("/path/to/isso.cfg")) WSGIServer(application).run() - -Similar to mod_wsgi_, set a static session key if you are using more than one process -to avoid random errors. - -.. code-block:: ini - - [general] - ; cat /dev/urandom | strings | grep -o '[[:alnum:]]' | head -n 30 | tr -d '\n' - session-key = superrandomkey1 diff --git a/docs/isso.example.cfg b/docs/isso.example.cfg index afcfb8b..425c88c 100644 --- a/docs/isso.example.cfg +++ b/docs/isso.example.cfg @@ -24,10 +24,6 @@ host = http://localhost/ # 3h45m12s equals to 3 hours, 45 minutes and 12 seconds. max-age = 15m -# private session key to validate client cookies. If you restart the application -# several times per hour for whatever reason, use a fixed key. -session-key = ... ; python: binascii.b2a_hex(os.urandom(24)) - # Select notification backend for new comments. Currently, only SMTP is # available. notify = @@ -53,9 +49,8 @@ purge-after = 30d # for details). Does not apply for uWSGI. listen = http://localhost:8080 -# reload application, when the source code has changed. Useful for development -# (don't forget to use a fixed session-key). Only works when gevent and uwsgi -# are not available. +# reload application, when the source code has changed. Useful for development. +# Only works with the internal webserver. reload = off # show 10 most time consuming function in Isso after each request. Do not use in diff --git a/isso/__init__.py b/isso/__init__.py index a59311d..933081d 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -85,7 +85,7 @@ class Isso(object): self.conf = conf self.db = db.SQLite3(conf.get('general', 'dbpath'), conf) - self.signer = URLSafeTimedSerializer(conf.get('general', 'session-key')) + self.signer = URLSafeTimedSerializer(self.db.preferences.get("session-key")) self.markup = html.Markup(conf.section('markup')) super(Isso, self).__init__(conf) @@ -160,9 +160,6 @@ def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False): isso = App(conf) - # show session-key (to see that it changes randomely if unset) - logger.info("session-key = %s", isso.conf.get("general", "session-key")) - # check HTTP server connection for host in conf.getiter("general", "host"): with http.curl('HEAD', host, '/', 5) as resp: diff --git a/isso/core.py b/isso/core.py index 315a364..b47c465 100644 --- a/isso/core.py +++ b/isso/core.py @@ -3,10 +3,8 @@ from __future__ import print_function import io -import os import time import logging -import binascii import threading import multiprocessing @@ -115,7 +113,7 @@ class Config: default = [ "[general]", "name = ", - "dbpath = /tmp/isso.db", "session-key = %s" % binascii.b2a_hex(os.urandom(16)), + "dbpath = /tmp/isso.db", "host = http://localhost:8080/", "max-age = 15m", "notify = ", "[moderation]", @@ -164,6 +162,9 @@ class Config: logger.warn("use `listen = http://$host:$port` instead") if item == ("smtp", "ssl"): logger.warn("use `security = none | starttls | ssl` instead") + if item == ("general", "session-key"): + logger.info("Your `session-key` has been stored in the " + "database itself, this option is now unused") if rv.get("smtp", "username") and not rv.get("general", "notify"): logger.warn(("SMTP is no longer enabled by default, add " diff --git a/isso/db/__init__.py b/isso/db/__init__.py index ef34e1a..7a10a74 100644 --- a/isso/db/__init__.py +++ b/isso/db/__init__.py @@ -9,6 +9,7 @@ logger = logging.getLogger("isso") from isso.db.comments import Comments from isso.db.threads import Threads from isso.db.spam import Guard +from isso.db.preferences import Preferences class SQLite3: @@ -18,7 +19,7 @@ class SQLite3: a trigger for automated orphan removal. """ - MAX_VERSION = 1 + MAX_VERSION = 2 def __init__(self, path, conf): @@ -27,18 +28,19 @@ class SQLite3: rv = self.execute([ "SELECT name FROM sqlite_master" - " WHERE type='table' AND name IN ('threads', 'comments')"] - ).fetchall() - - if rv: - self.migrate(to=SQLite3.MAX_VERSION) - else: - self.execute("PRAGMA user_version = %i" % SQLite3.MAX_VERSION) + " WHERE type='table' AND name IN ('threads', 'comments', 'preferences')"] + ).fetchone() + self.preferences = Preferences(self) self.threads = Threads(self) self.comments = Comments(self) self.guard = Guard(self) + if rv is None: + self.execute("PRAGMA user_version = %i" % SQLite3.MAX_VERSION) + else: + self.migrate(to=SQLite3.MAX_VERSION) + self.execute([ 'CREATE TRIGGER IF NOT EXISTS remove_stale_threads', 'AFTER DELETE ON comments', @@ -76,3 +78,14 @@ class SQLite3: con.execute('UPDATE comments SET voters=?', (bf, )) con.execute('PRAGMA user_version = 1') logger.info("%i rows changed", con.total_changes) + + # move [general] session-key to database + if self.version == 1: + + with sqlite3.connect(self.path) as con: + if self.conf.has_option("general", "session-key"): + con.execute('UPDATE preferences SET value=? WHERE key=?', ( + self.conf.get("general", "session-key"), "session-key")) + + con.execute('PRAGMA user_version = 2') + logger.info("%i rows changed", con.total_changes) diff --git a/isso/db/preferences.py b/isso/db/preferences.py new file mode 100644 index 0000000..275819e --- /dev/null +++ b/isso/db/preferences.py @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- + +import os +import binascii + + +class Preferences: + + defaults = [ + ("session-key", binascii.b2a_hex(os.urandom(24))), + ] + + def __init__(self, db): + + self.db = db + self.db.execute([ + 'CREATE TABLE IF NOT EXISTS preferences (', + ' key VARCHAR PRIMARY KEY, value VARCHAR', + ');']) + + for (key, value) in Preferences.defaults: + if self.get(key) is None: + self.set(key, value) + + def get(self, key, default=None): + rv = self.db.execute( + 'SELECT value FROM preferences WHERE key=?', (key, )).fetchone() + + if rv is None: + return default + + return rv[0] + + def set(self, key, value): + self.db.execute( + 'INSERT INTO preferences (key, value) VALUES (?, ?)', (key, value))