# vim: set sw=4 expandtab :
#
# Copyright (C) 2000, 2001, 2013 Gregory Trubetskoy
# Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Apache Software Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you
# may not use this file except in compliance with the License. You
# may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing
# permissions and limitations under the License.
#
# Originally developed by Gregory Trubetskoy.
#
import sys
PY2 = sys.version[0] == '2'
if PY2:
import apache, Cookie
import md5
import anydbm as dbm
from whichdb import whichdb
from cPickle import load, loads, dump, dumps
from cStringIO import StringIO
else:
from . import apache, Cookie
from hashlib import md5
import dbm
from dbm import whichdb
from io import StringIO
from pickle import load, loads, dump, dumps
import _apache
import os
import stat
import time
import random
import tempfile
import traceback
import re
COOKIE_NAME="pysid"
DFT_TIMEOUT=30*60 # 30 min
DFT_LOCK = True
CLEANUP_CHANCE=1000 # cleanups have 1 in CLEANUP_CHANCE chance
tempdir = tempfile.gettempdir()
def md5_hash(s):
if PY2:
return md5.new(s).hexdigest()
else:
if isinstance(s, str):
s = s.encode('latin1')
return md5(s).hexdigest()
def _init_rnd():
""" initialize random number generators
this is key in multithreaded env, see
python docs for random """
# query max number of threads
if _apache.mpm_query(apache.AP_MPMQ_IS_THREADED):
gennum = _apache.mpm_query(apache.AP_MPMQ_MAX_SPARE_THREADS)
else:
gennum = 10
# make generators
# this bit is from Python lib reference
g = random.Random(time.time())
result = [g]
for i in range(gennum - 1):
laststate = g.getstate()
g = random.Random()
g.setstate(laststate)
result.append(g)
return result
rnd_gens = _init_rnd()
rnd_iter = iter(rnd_gens)
def _get_generator():
# get rnd_iter.next(), or start over
# if we reached the end of it
global rnd_iter
try:
return next(rnd_iter)
except StopIteration:
# the small potential for two threads doing this
# seems does not warrant use of a lock
rnd_iter = iter(rnd_gens)
return next(rnd_iter)
def _new_sid(req):
# Make a number based on current time, pid, remote ip
# and two random ints, then hash with md5. This should
# be fairly unique and very difficult to guess.
#
# WARNING
# The current implementation of _new_sid returns an
# md5 hexdigest string. To avoid a possible directory traversal
# attack in FileSession the sid is validated using
# the _check_sid() method and the compiled regex
# validate_sid_re. The sid will be accepted only if len(sid) == 32
# and it only contains the characters 0-9 and a-f.
#
# If you change this implementation of _new_sid, make sure to also
# change the validation scheme, as well as the test_Session_illegal_sid()
# unit test in test/test.py.
# /WARNING
t = int(time.time()*10000)
pid = os.getpid()
g = _get_generator()
rnd1 = g.randint(0, 999999999)
rnd2 = g.randint(0, 999999999)
ip = req.connection.remote_ip
return md5_hash("%d%d%d%d%s" % (t, pid, rnd1, rnd2, ip))
validate_sid_re = re.compile('[0-9a-f]{32}$')
def _check_sid(sid):
## Check the validity of the session id
# # The sid must be 32 characters long, and consisting of the characters
# 0-9 and a-f.
#
# The sid may be passed in a cookie from the client and as such
# should not be trusted. This is particularly important in
# FileSession, where the session filename is derived from the sid.
# A sid containing '/' or '.' characters could result in a directory
# traversal attack
return not not validate_sid_re.match(sid)
class BaseSession(dict):
def __init__(self, req, sid=None, secret=None, lock=1,
timeout=0):
self._req, self._sid, self._secret = req, sid, secret
self._lock = lock
self._new = 1
self._created = 0
self._accessed = 0
self._timeout = 0
self._locked = 0
self._invalid = 0
dict.__init__(self)
config = req.get_options()
if "mod_python.session.cookie_name" in config:
session_cookie_name = config.get("mod_python.session.cookie_name", COOKIE_NAME)
else:
# For backwards compatability with versions
# of mod_python prior to 3.3.
session_cookie_name = config.get("session_cookie_name", COOKIE_NAME)
if not self._sid:
# check to see if cookie exists
if secret:
cookie = Cookie.get_cookie(req, session_cookie_name,
Class=Cookie.SignedCookie,
secret=self._secret,
mismatch=Cookie.Cookie.IGNORE)
else:
cookie = Cookie.get_cookie(req, session_cookie_name)
if cookie:
self._sid = cookie.value
if self._sid:
if not _check_sid(self._sid):
if sid:
# Supplied explicitly by user of the class,
# raise an exception and make the user code
# deal with it.
raise ValueError("Invalid Session ID: sid=%s" % sid)
else:
# Derived from the cookie sent by browser,
# wipe it out so it gets replaced with a
# correct value.
self._sid = None
self.init_lock()
if self._sid:
# attempt to load ourselves
self.lock()
if self.load():
self._new = 0
if self._new:
# make a new session
if self._sid: self.unlock() # unlock old sid
self._sid = _new_sid(self._req)
self.lock() # lock new sid
Cookie.add_cookie(self._req, self.make_cookie())
self._created = time.time()
if timeout:
self._timeout = timeout
else:
self._timeout = DFT_TIMEOUT
self._accessed = time.time()
# need cleanup?
if random.randint(1, CLEANUP_CHANCE) == 1:
self.cleanup()
def make_cookie(self):
config = self._req.get_options()
if "mod_python.session.cookie_name" in config:
session_cookie_name = config.get("mod_python.session.cookie_name", COOKIE_NAME)
else:
# For backwards compatability with versions
# of mod_python prior to 3.3.
session_cookie_name = config.get("session_cookie_name", COOKIE_NAME)
if self._secret:
c = Cookie.SignedCookie(session_cookie_name, self._sid,
secret=self._secret)
else:
c = Cookie.Cookie(session_cookie_name, self._sid)
if "mod_python.session.application_domain" in config:
c.domain = config["mod_python.session.application_domain"]
if "mod_python.session.application_path" in config:
c.path = config["mod_python.session.application_path"]
elif "ApplicationPath" in config:
# For backwards compatability with versions
# of mod_python prior to 3.3.
c.path = config["ApplicationPath"]
else:
# the path where *Handler directive was specified
dirpath = self._req.hlist.directory
if dirpath:
docroot = self._req.document_root()
c.path = dirpath[len(docroot):]
else:
c.path = '/'
# Sometimes there is no path, e.g. when Location
# is used. When Alias or UserDir are used, then
# the path wouldn't match the URI. In those cases
# just default to '/'
if not c.path or not self._req.uri.startswith(c.path):
c.path = '/'
return c
def invalidate(self):
c = self.make_cookie()
c.expires = 0
Cookie.add_cookie(self._req, c)
self.delete()
self._invalid = 1
def load(self):
dict = self.do_load()
if dict == None:
return 0
if (time.time() - dict["_accessed"]) > dict["_timeout"]:
return 0
self._created = dict["_created"]
self._accessed = dict["_accessed"]
self._timeout = dict["_timeout"]
self.update(dict["_data"])
return 1
def save(self):
if not self._invalid:
dict = {"_data" : self.copy(),
"_created" : self._created,
"_accessed": self._accessed,
"_timeout" : self._timeout}
self.do_save(dict)
def delete(self):
self.do_delete()
self.clear()
def init_lock(self):
pass
def lock(self):
if self._lock:
_apache._global_lock(self._req.server, self._sid)
self._locked = 1
self._req.register_cleanup(unlock_session_cleanup, self)
def unlock(self):
if self._lock and self._locked:
_apache._global_unlock(self._req.server, self._sid)
self._locked = 0
def is_new(self):
return not not self._new
def id(self):
return self._sid
def created(self):
return self._created
def last_accessed(self):
return self._accessed
def timeout(self):
return self._timeout
def set_timeout(self, secs):
self._timeout = secs
def cleanup(self):
self.do_cleanup()
def __del__(self):
self.unlock()
def unlock_session_cleanup(sess):
sess.unlock()
###########################################################################
## DbmSession
def dbm_cleanup(data):
dbm, server = data
_apache._global_lock(server, None, 0)
db = dbm.open(dbm, 'c')
try:
old = []
s = db.first()
while 1:
key, val = s
dict = loads(val)
try:
if (time.time() - dict["_accessed"]) > dict["_timeout"]:
old.append(key)
except KeyError:
old.append(key)
try:
s = next(db)
except KeyError: break
for key in old:
try:
del db[key]
except: pass
finally:
db.close()
_apache._global_unlock(server, None, 0)
class DbmSession(BaseSession):
def __init__(self, req, dbm=None, sid=0, secret=None, dbmtype=dbm,
timeout=0, lock=1):
if not dbm:
opts = req.get_options()
if "mod_python.dbm_session.database_filename" in opts:
dbm = opts["mod_python.dbm_session.database_filename"]
elif "session_dbm" in opts:
# For backwards compatability with versions
# of mod_python prior to 3.3.
dbm = opts["session_dbm"]
elif "mod_python.dbm_session.database_directory" in opts:
dbm = os.path.join(opts.get('mod_python.dbm_session.database_directory', tempdir), 'mp_sess.dbm')
elif "mod_python.session.database_directory" in opts:
dbm = os.path.join(opts.get('mod_python.session.database_directory', tempdir), 'mp_sess.dbm')
else:
# For backwards compatability with versions
# of mod_python prior to 3.3.
dbm = os.path.join(opts.get('session_directory', tempdir), 'mp_sess.dbm')
self._dbmfile = dbm
self._dbmtype = dbmtype
BaseSession.__init__(self, req, sid=sid, secret=secret,
timeout=timeout, lock=lock)
def _set_dbm_type(self):
module = whichdb(self._dbmfile)
if module:
self._dbmtype = __import__(module)
def _get_dbm(self):
result = self._dbmtype.open(self._dbmfile, 'c', stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
if self._dbmtype is dbm:
self._set_dbm_type()
return result
def do_cleanup(self):
data = [self._dbmfile, self._req.server]
self._req.register_cleanup(dbm_cleanup, data)
self._req.log_error("DbmSession: registered database cleanup.",
apache.APLOG_NOTICE)
def do_load(self):
_apache._global_lock(self._req.server, None, 0)
dbm = self._get_dbm()
try:
if self._sid.encode() in dbm:
return loads(dbm[self._sid.encode()])
else:
return None
finally:
dbm.close()
_apache._global_unlock(self._req.server, None, 0)
def do_save(self, dict):
_apache._global_lock(self._req.server, None, 0)
dbm = self._get_dbm()
try:
dbm[self._sid.encode()] = dumps(dict)
finally:
dbm.close()
_apache._global_unlock(self._req.server, None, 0)
def do_delete(self):
_apache._global_lock(self._req.server, None, 0)
dbm = self._get_dbm()
try:
try:
del dbm[self._sid.encode()]
except KeyError: pass
finally:
dbm.close()
_apache._global_unlock(self._req.server, None, 0)
###########################################################################
## FileSession
DFT_FAST_CLEANUP = True
DFT_VERIFY_CLEANUP = True
DFT_GRACE_PERIOD = 240
DFT_CLEANUP_TIME_LIMIT = 2
# Credits : this was initially contributed by dharana