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.
This commit is contained in:
Martin Zimmermann 2014-03-20 15:44:05 +01:00
parent 8f293ad435
commit 0b816a0677
7 changed files with 65 additions and 48 deletions

View File

@ -43,7 +43,6 @@ session key and hostname. Here are the default values for this section:
name = name =
host = http://localhost:8080/ host = http://localhost:8080/
max-age = 15m max-age = 15m
session-key = ... ; python: binascii.b2a_hex(os.urandom(24))
notify = notify =
dbpath dbpath
@ -71,11 +70,6 @@ host
This is useful, when your website is available on HTTP and HTTPS. 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 max-age
time range that allows users to edit/remove their own comments. See time range that allows users to edit/remove their own comments. See
:ref:`Appendum: Timedelta <appendum-timedelta>` for valid values. :ref:`Appendum: Timedelta <appendum-timedelta>` for valid values.
@ -136,8 +130,7 @@ listen
reload reload
reload application, when the source code has changed. Useful for reload application, when the source code has changed. Useful for
development (don't forget to use a fixed `session-key`). Only works development. Only works with the internal webserver.
when ``gevent`` and ``uwsgi`` are *not* available.
profile profile
show 10 most time consuming function in Isso after each request. Do show 10 most time consuming function in Isso after each request. Do

View File

@ -121,15 +121,6 @@ Next, copy'n'paste to `/var/www/isso.wsgi`:
application = make_app(Config.load("/path/to/isso.cfg")) 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 <http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html>`__ `mod_fastcgi <http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -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")) application = make_app(Config.load("/path/to/isso.cfg"))
WSGIServer(application).run() 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

View File

@ -24,10 +24,6 @@ host = http://localhost/
# 3h45m12s equals to 3 hours, 45 minutes and 12 seconds. # 3h45m12s equals to 3 hours, 45 minutes and 12 seconds.
max-age = 15m 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 # Select notification backend for new comments. Currently, only SMTP is
# available. # available.
notify = notify =
@ -53,9 +49,8 @@ purge-after = 30d
# for details). Does not apply for uWSGI. # for details). Does not apply for uWSGI.
listen = http://localhost:8080 listen = http://localhost:8080
# reload application, when the source code has changed. Useful for development # 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 # Only works with the internal webserver.
# are not available.
reload = off reload = off
# show 10 most time consuming function in Isso after each request. Do not use in # show 10 most time consuming function in Isso after each request. Do not use in

View File

@ -85,7 +85,7 @@ class Isso(object):
self.conf = conf self.conf = conf
self.db = db.SQLite3(conf.get('general', 'dbpath'), 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')) self.markup = html.Markup(conf.section('markup'))
super(Isso, self).__init__(conf) super(Isso, self).__init__(conf)
@ -160,9 +160,6 @@ def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False):
isso = App(conf) 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 # check HTTP server connection
for host in conf.getiter("general", "host"): for host in conf.getiter("general", "host"):
with http.curl('HEAD', host, '/', 5) as resp: with http.curl('HEAD', host, '/', 5) as resp:

View File

@ -3,10 +3,8 @@
from __future__ import print_function from __future__ import print_function
import io import io
import os
import time import time
import logging import logging
import binascii
import threading import threading
import multiprocessing import multiprocessing
@ -115,7 +113,7 @@ class Config:
default = [ default = [
"[general]", "[general]",
"name = ", "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", "host = http://localhost:8080/", "max-age = 15m",
"notify = ", "notify = ",
"[moderation]", "[moderation]",
@ -164,6 +162,9 @@ class Config:
logger.warn("use `listen = http://$host:$port` instead") logger.warn("use `listen = http://$host:$port` instead")
if item == ("smtp", "ssl"): if item == ("smtp", "ssl"):
logger.warn("use `security = none | starttls | ssl` instead") 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"): if rv.get("smtp", "username") and not rv.get("general", "notify"):
logger.warn(("SMTP is no longer enabled by default, add " logger.warn(("SMTP is no longer enabled by default, add "

View File

@ -9,6 +9,7 @@ logger = logging.getLogger("isso")
from isso.db.comments import Comments from isso.db.comments import Comments
from isso.db.threads import Threads from isso.db.threads import Threads
from isso.db.spam import Guard from isso.db.spam import Guard
from isso.db.preferences import Preferences
class SQLite3: class SQLite3:
@ -18,7 +19,7 @@ class SQLite3:
a trigger for automated orphan removal. a trigger for automated orphan removal.
""" """
MAX_VERSION = 1 MAX_VERSION = 2
def __init__(self, path, conf): def __init__(self, path, conf):
@ -27,18 +28,19 @@ class SQLite3:
rv = self.execute([ rv = self.execute([
"SELECT name FROM sqlite_master" "SELECT name FROM sqlite_master"
" WHERE type='table' AND name IN ('threads', 'comments')"] " WHERE type='table' AND name IN ('threads', 'comments', 'preferences')"]
).fetchall() ).fetchone()
if rv:
self.migrate(to=SQLite3.MAX_VERSION)
else:
self.execute("PRAGMA user_version = %i" % SQLite3.MAX_VERSION)
self.preferences = Preferences(self)
self.threads = Threads(self) self.threads = Threads(self)
self.comments = Comments(self) self.comments = Comments(self)
self.guard = Guard(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([ self.execute([
'CREATE TRIGGER IF NOT EXISTS remove_stale_threads', 'CREATE TRIGGER IF NOT EXISTS remove_stale_threads',
'AFTER DELETE ON comments', 'AFTER DELETE ON comments',
@ -76,3 +78,14 @@ class SQLite3:
con.execute('UPDATE comments SET voters=?', (bf, )) con.execute('UPDATE comments SET voters=?', (bf, ))
con.execute('PRAGMA user_version = 1') con.execute('PRAGMA user_version = 1')
logger.info("%i rows changed", con.total_changes) 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)

36
isso/db/preferences.py Normal file
View File

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