sessions.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import sys
  2. import time
  3. from datetime import datetime
  4. from uuid import uuid4
  5. try:
  6. import cPickle as pickle
  7. except ImportError:
  8. import pickle
  9. from flask.sessions import SessionInterface as FlaskSessionInterface
  10. from flask.sessions import SessionMixin
  11. from werkzeug.datastructures import CallbackDict
  12. from itsdangerous import Signer, BadSignature, want_bytes
  13. PY2 = sys.version_info[0] == 2
  14. if not PY2:
  15. text_type = str
  16. else:
  17. text_type = unicode
  18. def total_seconds(td):
  19. return td.days * 60 * 60 * 24 + td.seconds
  20. class ServerSideSession(CallbackDict, SessionMixin):
  21. """Baseclass for server-side based sessions."""
  22. def __init__(self, initial=None, sid=None, permanent=None):
  23. def on_update(self):
  24. self.modified = True
  25. CallbackDict.__init__(self, initial, on_update)
  26. self.sid = sid
  27. if permanent:
  28. self.permanent = permanent
  29. self.modified = False
  30. class RedisSession(ServerSideSession):
  31. pass
  32. class MemcachedSession(ServerSideSession):
  33. pass
  34. class FileSystemSession(ServerSideSession):
  35. pass
  36. class MongoDBSession(ServerSideSession):
  37. pass
  38. class SqlAlchemySession(ServerSideSession):
  39. pass
  40. class SessionInterface(FlaskSessionInterface):
  41. def _generate_sid(self):
  42. return str(uuid4())
  43. def _get_signer(self, app):
  44. if not app.secret_key:
  45. return None
  46. return Signer(app.secret_key, salt='flask-session',
  47. key_derivation='hmac')
  48. class NullSessionInterface(SessionInterface):
  49. """Used to open a :class:`flask.sessions.NullSession` instance.
  50. """
  51. def open_session(self, app, request):
  52. return None
  53. class RedisSessionInterface(SessionInterface):
  54. """Uses the Redis key-value store as a session backend.
  55. .. versionadded:: 0.2
  56. The `use_signer` parameter was added.
  57. :param redis: A ``redis.Redis`` instance.
  58. :param key_prefix: A prefix that is added to all Redis store keys.
  59. :param use_signer: Whether to sign the session id cookie or not.
  60. :param permanent: Whether to use permanent session or not.
  61. """
  62. serializer = pickle
  63. session_class = RedisSession
  64. def __init__(self, redis, key_prefix, use_signer=False, permanent=True):
  65. if redis is None:
  66. from redis import Redis
  67. redis = Redis()
  68. self.redis = redis
  69. self.key_prefix = key_prefix
  70. self.use_signer = use_signer
  71. self.permanent = permanent
  72. self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
  73. def open_session(self, app, request):
  74. sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
  75. if not sid:
  76. sid = self._generate_sid()
  77. return self.session_class(sid=sid, permanent=self.permanent)
  78. if self.use_signer:
  79. signer = self._get_signer(app)
  80. if signer is None:
  81. return None
  82. try:
  83. sid_as_bytes = signer.unsign(sid)
  84. sid = sid_as_bytes.decode()
  85. except BadSignature:
  86. sid = self._generate_sid()
  87. return self.session_class(sid=sid, permanent=self.permanent)
  88. if not PY2 and not isinstance(sid, text_type):
  89. sid = sid.decode('utf-8', 'strict')
  90. val = self.redis.get(self.key_prefix + sid)
  91. if val is not None:
  92. try:
  93. data = self.serializer.loads(val)
  94. return self.session_class(data, sid=sid)
  95. except:
  96. return self.session_class(sid=sid, permanent=self.permanent)
  97. return self.session_class(sid=sid, permanent=self.permanent)
  98. def save_session(self, app, session, response):
  99. domain = self.get_cookie_domain(app)
  100. path = self.get_cookie_path(app)
  101. if not session:
  102. if session.modified:
  103. self.redis.delete(self.key_prefix + session.sid)
  104. response.delete_cookie(app.config["SESSION_COOKIE_NAME"],
  105. domain=domain, path=path)
  106. return
  107. # Modification case. There are upsides and downsides to
  108. # emitting a set-cookie header each request. The behavior
  109. # is controlled by the :meth:`should_set_cookie` method
  110. # which performs a quick check to figure out if the cookie
  111. # should be set or not. This is controlled by the
  112. # SESSION_REFRESH_EACH_REQUEST config flag as well as
  113. # the permanent flag on the session itself.
  114. # if not self.should_set_cookie(app, session):
  115. # return
  116. conditional_cookie_kwargs = {}
  117. httponly = self.get_cookie_httponly(app)
  118. secure = self.get_cookie_secure(app)
  119. if self.has_same_site_capability:
  120. conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
  121. expires = self.get_expiration_time(app, session)
  122. val = self.serializer.dumps(dict(session))
  123. self.redis.setex(name=self.key_prefix + session.sid, value=val,
  124. time=total_seconds(app.permanent_session_lifetime))
  125. if self.use_signer:
  126. session_id = self._get_signer(app).sign(want_bytes(session.sid))
  127. else:
  128. session_id = session.sid
  129. response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id,
  130. expires=expires, httponly=httponly,
  131. domain=domain, path=path, secure=secure,
  132. **conditional_cookie_kwargs)
  133. class MemcachedSessionInterface(SessionInterface):
  134. """A Session interface that uses memcached as backend.
  135. .. versionadded:: 0.2
  136. The `use_signer` parameter was added.
  137. :param client: A ``memcache.Client`` instance.
  138. :param key_prefix: A prefix that is added to all Memcached store keys.
  139. :param use_signer: Whether to sign the session id cookie or not.
  140. :param permanent: Whether to use permanent session or not.
  141. """
  142. serializer = pickle
  143. session_class = MemcachedSession
  144. def __init__(self, client, key_prefix, use_signer=False, permanent=True):
  145. if client is None:
  146. client = self._get_preferred_memcache_client()
  147. if client is None:
  148. raise RuntimeError('no memcache module found')
  149. self.client = client
  150. self.key_prefix = key_prefix
  151. self.use_signer = use_signer
  152. self.permanent = permanent
  153. self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
  154. def _get_preferred_memcache_client(self):
  155. servers = ['127.0.0.1:11211']
  156. try:
  157. import pylibmc
  158. except ImportError:
  159. pass
  160. else:
  161. return pylibmc.Client(servers)
  162. try:
  163. import memcache
  164. except ImportError:
  165. pass
  166. else:
  167. return memcache.Client(servers)
  168. def _get_memcache_timeout(self, timeout):
  169. """
  170. Memcached deals with long (> 30 days) timeouts in a special
  171. way. Call this function to obtain a safe value for your timeout.
  172. """
  173. if timeout > 2592000: # 60*60*24*30, 30 days
  174. # See http://code.google.com/p/memcached/wiki/FAQ
  175. # "You can set expire times up to 30 days in the future. After that
  176. # memcached interprets it as a date, and will expire the item after
  177. # said date. This is a simple (but obscure) mechanic."
  178. #
  179. # This means that we have to switch to absolute timestamps.
  180. timeout += int(time.time())
  181. return timeout
  182. def open_session(self, app, request):
  183. sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
  184. if not sid:
  185. sid = self._generate_sid()
  186. return self.session_class(sid=sid, permanent=self.permanent)
  187. if self.use_signer:
  188. signer = self._get_signer(app)
  189. if signer is None:
  190. return None
  191. try:
  192. sid_as_bytes = signer.unsign(sid)
  193. sid = sid_as_bytes.decode()
  194. except BadSignature:
  195. sid = self._generate_sid()
  196. return self.session_class(sid=sid, permanent=self.permanent)
  197. full_session_key = self.key_prefix + sid
  198. if PY2 and isinstance(full_session_key, unicode):
  199. full_session_key = full_session_key.encode('utf-8')
  200. val = self.client.get(full_session_key)
  201. if val is not None:
  202. try:
  203. if not PY2:
  204. val = want_bytes(val)
  205. data = self.serializer.loads(val)
  206. return self.session_class(data, sid=sid)
  207. except:
  208. return self.session_class(sid=sid, permanent=self.permanent)
  209. return self.session_class(sid=sid, permanent=self.permanent)
  210. def save_session(self, app, session, response):
  211. domain = self.get_cookie_domain(app)
  212. path = self.get_cookie_path(app)
  213. full_session_key = self.key_prefix + session.sid
  214. if PY2 and isinstance(full_session_key, unicode):
  215. full_session_key = full_session_key.encode('utf-8')
  216. if not session:
  217. if session.modified:
  218. self.client.delete(full_session_key)
  219. response.delete_cookie(app.config["SESSION_COOKIE_NAME"],
  220. domain=domain, path=path)
  221. return
  222. conditional_cookie_kwargs = {}
  223. httponly = self.get_cookie_httponly(app)
  224. secure = self.get_cookie_secure(app)
  225. if self.has_same_site_capability:
  226. conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
  227. expires = self.get_expiration_time(app, session)
  228. if not PY2:
  229. val = self.serializer.dumps(dict(session), 0)
  230. else:
  231. val = self.serializer.dumps(dict(session))
  232. self.client.set(full_session_key, val, self._get_memcache_timeout(
  233. total_seconds(app.permanent_session_lifetime)))
  234. if self.use_signer:
  235. session_id = self._get_signer(app).sign(want_bytes(session.sid))
  236. else:
  237. session_id = session.sid
  238. response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id,
  239. expires=expires, httponly=httponly,
  240. domain=domain, path=path, secure=secure,
  241. **conditional_cookie_kwargs)
  242. class FileSystemSessionInterface(SessionInterface):
  243. """Uses the :class:`cachelib.file.FileSystemCache` as a session backend.
  244. .. versionadded:: 0.2
  245. The `use_signer` parameter was added.
  246. :param cache_dir: the directory where session files are stored.
  247. :param threshold: the maximum number of items the session stores before it
  248. starts deleting some.
  249. :param mode: the file mode wanted for the session files, default 0600
  250. :param key_prefix: A prefix that is added to FileSystemCache store keys.
  251. :param use_signer: Whether to sign the session id cookie or not.
  252. :param permanent: Whether to use permanent session or not.
  253. """
  254. session_class = FileSystemSession
  255. def __init__(self, cache_dir, threshold, mode, key_prefix,
  256. use_signer=False, permanent=True):
  257. from cachelib.file import FileSystemCache
  258. self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode)
  259. self.key_prefix = key_prefix
  260. self.use_signer = use_signer
  261. self.permanent = permanent
  262. self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
  263. def open_session(self, app, request):
  264. sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
  265. if not sid:
  266. sid = self._generate_sid()
  267. return self.session_class(sid=sid, permanent=self.permanent)
  268. if self.use_signer:
  269. signer = self._get_signer(app)
  270. if signer is None:
  271. return None
  272. try:
  273. sid_as_bytes = signer.unsign(sid)
  274. sid = sid_as_bytes.decode()
  275. except BadSignature:
  276. sid = self._generate_sid()
  277. return self.session_class(sid=sid, permanent=self.permanent)
  278. data = self.cache.get(self.key_prefix + sid)
  279. if data is not None:
  280. return self.session_class(data, sid=sid)
  281. return self.session_class(sid=sid, permanent=self.permanent)
  282. def save_session(self, app, session, response):
  283. domain = self.get_cookie_domain(app)
  284. path = self.get_cookie_path(app)
  285. if not session:
  286. if session.modified:
  287. self.cache.delete(self.key_prefix + session.sid)
  288. response.delete_cookie(app.config["SESSION_COOKIE_NAME"],
  289. domain=domain, path=path)
  290. return
  291. conditional_cookie_kwargs = {}
  292. httponly = self.get_cookie_httponly(app)
  293. secure = self.get_cookie_secure(app)
  294. if self.has_same_site_capability:
  295. conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
  296. expires = self.get_expiration_time(app, session)
  297. data = dict(session)
  298. self.cache.set(self.key_prefix + session.sid, data,
  299. total_seconds(app.permanent_session_lifetime))
  300. if self.use_signer:
  301. session_id = self._get_signer(app).sign(want_bytes(session.sid))
  302. else:
  303. session_id = session.sid
  304. response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id,
  305. expires=expires, httponly=httponly,
  306. domain=domain, path=path, secure=secure,
  307. **conditional_cookie_kwargs)
  308. class MongoDBSessionInterface(SessionInterface):
  309. """A Session interface that uses mongodb as backend.
  310. .. versionadded:: 0.2
  311. The `use_signer` parameter was added.
  312. :param client: A ``pymongo.MongoClient`` instance.
  313. :param db: The database you want to use.
  314. :param collection: The collection you want to use.
  315. :param key_prefix: A prefix that is added to all MongoDB store keys.
  316. :param use_signer: Whether to sign the session id cookie or not.
  317. :param permanent: Whether to use permanent session or not.
  318. """
  319. serializer = pickle
  320. session_class = MongoDBSession
  321. def __init__(self, client, db, collection, key_prefix, use_signer=False,
  322. permanent=True):
  323. if client is None:
  324. from pymongo import MongoClient
  325. client = MongoClient()
  326. self.client = client
  327. self.store = client[db][collection]
  328. self.key_prefix = key_prefix
  329. self.use_signer = use_signer
  330. self.permanent = permanent
  331. self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
  332. def open_session(self, app, request):
  333. sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
  334. if not sid:
  335. sid = self._generate_sid()
  336. return self.session_class(sid=sid, permanent=self.permanent)
  337. if self.use_signer:
  338. signer = self._get_signer(app)
  339. if signer is None:
  340. return None
  341. try:
  342. sid_as_bytes = signer.unsign(sid)
  343. sid = sid_as_bytes.decode()
  344. except BadSignature:
  345. sid = self._generate_sid()
  346. return self.session_class(sid=sid, permanent=self.permanent)
  347. store_id = self.key_prefix + sid
  348. document = self.store.find_one({'id': store_id})
  349. if document and document.get('expiration') <= datetime.utcnow():
  350. # Delete expired session
  351. self.store.remove({'id': store_id})
  352. document = None
  353. if document is not None:
  354. try:
  355. val = document['val']
  356. data = self.serializer.loads(want_bytes(val))
  357. return self.session_class(data, sid=sid)
  358. except:
  359. return self.session_class(sid=sid, permanent=self.permanent)
  360. return self.session_class(sid=sid, permanent=self.permanent)
  361. def save_session(self, app, session, response):
  362. domain = self.get_cookie_domain(app)
  363. path = self.get_cookie_path(app)
  364. store_id = self.key_prefix + session.sid
  365. if not session:
  366. if session.modified:
  367. self.store.remove({'id': store_id})
  368. response.delete_cookie(app.config["SESSION_COOKIE_NAME"],
  369. domain=domain, path=path)
  370. return
  371. conditional_cookie_kwargs = {}
  372. httponly = self.get_cookie_httponly(app)
  373. secure = self.get_cookie_secure(app)
  374. if self.has_same_site_capability:
  375. conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
  376. expires = self.get_expiration_time(app, session)
  377. val = self.serializer.dumps(dict(session))
  378. self.store.update({'id': store_id},
  379. {'id': store_id,
  380. 'val': val,
  381. 'expiration': expires}, True)
  382. if self.use_signer:
  383. session_id = self._get_signer(app).sign(want_bytes(session.sid))
  384. else:
  385. session_id = session.sid
  386. response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id,
  387. expires=expires, httponly=httponly,
  388. domain=domain, path=path, secure=secure,
  389. **conditional_cookie_kwargs)
  390. class SqlAlchemySessionInterface(SessionInterface):
  391. """Uses the Flask-SQLAlchemy from a flask app as a session backend.
  392. .. versionadded:: 0.2
  393. :param app: A Flask app instance.
  394. :param db: A Flask-SQLAlchemy instance.
  395. :param table: The table name you want to use.
  396. :param key_prefix: A prefix that is added to all store keys.
  397. :param use_signer: Whether to sign the session id cookie or not.
  398. :param permanent: Whether to use permanent session or not.
  399. """
  400. serializer = pickle
  401. session_class = SqlAlchemySession
  402. def __init__(self, app, db, table, key_prefix, use_signer=False,
  403. permanent=True):
  404. if db is None:
  405. from flask_sqlalchemy import SQLAlchemy
  406. db = SQLAlchemy(app)
  407. self.db = db
  408. self.key_prefix = key_prefix
  409. self.use_signer = use_signer
  410. self.permanent = permanent
  411. self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
  412. class Session(self.db.Model):
  413. __tablename__ = table
  414. id = self.db.Column(self.db.Integer, primary_key=True)
  415. session_id = self.db.Column(self.db.String(255), unique=True)
  416. data = self.db.Column(self.db.LargeBinary)
  417. expiry = self.db.Column(self.db.DateTime)
  418. def __init__(self, session_id, data, expiry):
  419. self.session_id = session_id
  420. self.data = data
  421. self.expiry = expiry
  422. def __repr__(self):
  423. return '<Session data %s>' % self.data
  424. # self.db.create_all()
  425. self.sql_session_model = Session
  426. def open_session(self, app, request):
  427. sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
  428. if not sid:
  429. sid = self._generate_sid()
  430. return self.session_class(sid=sid, permanent=self.permanent)
  431. if self.use_signer:
  432. signer = self._get_signer(app)
  433. if signer is None:
  434. return None
  435. try:
  436. sid_as_bytes = signer.unsign(sid)
  437. sid = sid_as_bytes.decode()
  438. except BadSignature:
  439. sid = self._generate_sid()
  440. return self.session_class(sid=sid, permanent=self.permanent)
  441. store_id = self.key_prefix + sid
  442. saved_session = self.sql_session_model.query.filter_by(
  443. session_id=store_id).first()
  444. if saved_session and saved_session.expiry <= datetime.utcnow():
  445. # Delete expired session
  446. self.db.session.delete(saved_session)
  447. self.db.session.commit()
  448. saved_session = None
  449. if saved_session:
  450. try:
  451. val = saved_session.data
  452. data = self.serializer.loads(want_bytes(val))
  453. return self.session_class(data, sid=sid)
  454. except:
  455. return self.session_class(sid=sid, permanent=self.permanent)
  456. return self.session_class(sid=sid, permanent=self.permanent)
  457. def save_session(self, app, session, response):
  458. domain = self.get_cookie_domain(app)
  459. path = self.get_cookie_path(app)
  460. store_id = self.key_prefix + session.sid
  461. saved_session = self.sql_session_model.query.filter_by(
  462. session_id=store_id).first()
  463. if not session:
  464. if session.modified:
  465. if saved_session:
  466. self.db.session.delete(saved_session)
  467. self.db.session.commit()
  468. response.delete_cookie(app.config["SESSION_COOKIE_NAME"],
  469. domain=domain, path=path)
  470. return
  471. conditional_cookie_kwargs = {}
  472. httponly = self.get_cookie_httponly(app)
  473. secure = self.get_cookie_secure(app)
  474. if self.has_same_site_capability:
  475. conditional_cookie_kwargs["samesite"] = self.get_cookie_samesite(app)
  476. expires = self.get_expiration_time(app, session)
  477. val = self.serializer.dumps(dict(session))
  478. if saved_session:
  479. saved_session.data = val
  480. saved_session.expiry = expires
  481. self.db.session.commit()
  482. else:
  483. new_session = self.sql_session_model(store_id, val, expires)
  484. self.db.session.add(new_session)
  485. self.db.session.commit()
  486. if self.use_signer:
  487. session_id = self._get_signer(app).sign(want_bytes(session.sid))
  488. else:
  489. session_id = session.sid
  490. response.set_cookie(app.config["SESSION_COOKIE_NAME"], session_id,
  491. expires=expires, httponly=httponly,
  492. domain=domain, path=path, secure=secure,
  493. **conditional_cookie_kwargs)