sessions.py 22 KB

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