codekingpro/portable-devtools
115k
1##########################################################################2#3# pgAdmin 4 - PostgreSQL Tools4#5# Copyright (C) 2013 - 2024, The pgAdmin Development Team6# This software is released under the PostgreSQL Licence7#8##########################################################################9 10"""11Implements the server-side session management.12 13Credit/Reference: http://flask.pocoo.org/snippets/109/14 15Modified to support both Python 2.6+ & Python 3.x16"""17 18import base6419import datetime20import hmac21import hashlib22import os23import secrets24import string25import time26import config27from uuid import uuid428from threading import Lock29from flask import current_app, request, flash, redirect30from flask_login import login_url31 32from pickle import dump, load33from collections import OrderedDict34 35from flask.sessions import SessionInterface, SessionMixin36from werkzeug.datastructures import CallbackDict37from werkzeug.security import safe_join38from werkzeug.exceptions import InternalServerError39 40from pgadmin.utils.ajax import make_json_response41 42 43def _calc_hmac(body, secret):44 return base64.b64encode(45 hmac.new(46 secret.encode(), body.encode(), hashlib.sha25647 ).digest()48 ).decode()49 50 51sess_lock = Lock()52LAST_CHECK_SESSION_FILES = None53 54 55class ManagedSession(CallbackDict, SessionMixin):56 def __init__(self, initial=None, sid=None, new=False, randval=None,57 hmac_digest=None):58 def on_update(self):59 self.modified = True60 61 CallbackDict.__init__(self, initial, on_update)62 self.sid = sid63 self.new = new64 self.modified = False65 self.randval = randval66 self.last_write = None67 self.force_write = False68 self.hmac_digest = hmac_digest69 self.permanent = True70 71 def sign(self, secret):72 if not self.hmac_digest:73 population = string.ascii_lowercase + string.digits74 75 self.randval = ''.join(76 secrets.choice(population) for i in range(20))77 self.hmac_digest = _calc_hmac(78 '%s:%s' % (self.sid, self.randval), secret)79 80 81class SessionManager():82 def new_session(self):83 'Create a new session'84 raise NotImplementedError85 86 def exists(self, sid):87 'Does the given session-id exist?'88 raise NotImplementedError89 90 def remove(self, sid):91 'Remove the session'92 raise NotImplementedError93 94 def get(self, sid, digest):95 'Retrieve a managed session by session-id, checking the HMAC digest'96 raise NotImplementedError97 98 def put(self, session):99 'Store a managed session'100 raise NotImplementedError101 102 103class CachingSessionManager(SessionManager):104 def __init__(self, parent, num_to_store, skip_paths=None):105 self.parent = parent106 self.num_to_store = num_to_store107 self._cache = OrderedDict()108 self.skip_paths = [] if skip_paths is None else skip_paths109 110 def _normalize(self):111 if len(self._cache) > self.num_to_store:112 # Flush 20% of the cache113 with sess_lock:114 while len(self._cache) > (self.num_to_store * 0.8):115 self._cache.popitem(False)116 117 def new_session(self):118 session = self.parent.new_session()119 120 # Do not store the session if skip paths121 for sp in self.skip_paths:122 if request.path.startswith(sp):123 return session124 125 with sess_lock:126 self._cache[session.sid] = session127 self._normalize()128 129 return session130 131 def remove(self, sid):132 with sess_lock:133 self.parent.remove(sid)134 if sid in self._cache:135 del self._cache[sid]136 137 def exists(self, sid):138 with sess_lock:139 if sid in self._cache:140 return True141 return self.parent.exists(sid)142 143 def get(self, sid, digest):144 session = None145 with sess_lock:146 if sid in self._cache:147 session = self._cache[sid]148 if session and session.hmac_digest != digest:149 session = None150 151 # reset order in Dict152 del self._cache[sid]153 154 if not session:155 session = self.parent.get(sid, digest)156 157 # Do not store the session if skip paths158 for sp in self.skip_paths:159 if request.path.startswith(sp):160 return session161 162 self._cache[sid] = session163 self._normalize()164 165 return session166 167 def put(self, session):168 with sess_lock:169 self.parent.put(session)170 171 # Do not store the session if skip paths172 for sp in self.skip_paths:173 if request.path.startswith(sp):174 return175 176 if session.sid in self._cache:177 try:178 del self._cache[session.sid]179 except Exception:180 pass181 182 self._cache[session.sid] = session183 self._normalize()184 185 186class FileBackedSessionManager(SessionManager):187 188 def __init__(self, path, secret, disk_write_delay, skip_paths=None):189 self.path = path190 self.secret = secret191 self.disk_write_delay = disk_write_delay192 if not os.path.exists(self.path):193 os.makedirs(self.path)194 self.skip_paths = [] if skip_paths is None else skip_paths195 196 def exists(self, sid):197 fname = safe_join(self.path, sid)198 return fname is not None and os.path.exists(fname)199 200 def remove(self, sid):201 fname = safe_join(self.path, sid)202 if fname is not None and os.path.exists(fname):203 os.unlink(fname)204 205 def new_session(self):206 sid = str(uuid4())207 fname = safe_join(self.path, sid)208 209 while fname is not None and os.path.exists(fname):210 sid = str(uuid4())211 fname = safe_join(self.path, sid)212 213 # Do not store the session if skip paths214 for sp in self.skip_paths:215 if request.path.startswith(sp):216 return ManagedSession(sid=sid)217 218 if fname is None:219 raise InternalServerError('Failed to create new session')220 221 # touch the file222 with open(fname, 'wb'):223 return ManagedSession(sid=sid)224 225 return ManagedSession(sid=sid)226 227 def get(self, sid, digest):228 'Retrieve a managed session by session-id, checking the HMAC digest'229 230 fname = safe_join(self.path, sid)231 data = None232 hmac_digest = None233 randval = None234 235 if fname is not None and os.path.exists(fname):236 try:237 with open(fname, 'rb') as f:238 randval, hmac_digest, data = load(f)239 except Exception:240 pass241 242 if not data:243 return self.new_session()244 245 # This assumes the file is correct, if you really want to246 # make sure the session is good from the server side, you247 # can re-calculate the hmac248 249 if hmac_digest != digest:250 return self.new_session()251 252 return ManagedSession(253 data, sid=sid, randval=randval, hmac_digest=hmac_digest254 )255 256 def put(self, session):257 """Store a managed session"""258 current_time = time.time()259 if not session.hmac_digest:260 session.sign(self.secret)261 elif not session.force_write and session.last_write is not None and \262 (current_time - float(session.last_write)) < \263 self.disk_write_delay:264 return265 266 session.last_write = current_time267 session.force_write = False268 269 # Do not store the session if skip paths270 for sp in self.skip_paths:271 if request.path.startswith(sp):272 return273 274 fname = safe_join(self.path, session.sid)275 276 if fname is None:277 raise InternalServerError('Failed to update the session')278 279 with open(fname, 'wb') as f:280 dump(281 (session.randval, session.hmac_digest, dict(session)),282 f283 )284 285 286class ManagedSessionInterface(SessionInterface):287 def __init__(self, manager):288 self.manager = manager289 290 def open_session(self, app, request):291 cookie_val = request.cookies.get(app.config['SESSION_COOKIE_NAME'])292 293 if not cookie_val or '!' not in cookie_val:294 return self.manager.new_session()295 296 sid, digest = cookie_val.split('!', 1)297 298 if self.manager.exists(sid):299 return self.manager.get(sid, digest)300 301 return self.manager.new_session()302 303 def save_session(self, app, session, response):304 domain = self.get_cookie_domain(app)305 if not session:306 self.manager.remove(session.sid)307 if session.modified:308 response.delete_cookie(app.config['SESSION_COOKIE_NAME'],309 domain=domain)310 return311 312 if not session.modified:313 # No need to save an unaltered session314 # TODO: put logic here to test if the cookie is older than N days,315 # if so, update the expiration date316 return317 318 self.manager.put(session)319 session.modified = False320 321 cookie_exp = self.get_expiration_time(app, session)322 response.set_cookie(323 app.config['SESSION_COOKIE_NAME'],324 '%s!%s' % (session.sid, session.hmac_digest),325 expires=cookie_exp,326 path=config.SESSION_COOKIE_PATH,327 secure=config.SESSION_COOKIE_SECURE,328 httponly=config.SESSION_COOKIE_HTTPONLY,329 samesite=config.SESSION_COOKIE_SAMESITE,330 domain=domain331 )332 333 334def create_session_interface(app, skip_paths=[]):335 return ManagedSessionInterface(336 CachingSessionManager(337 FileBackedSessionManager(338 app.config['SESSION_DB_PATH'],339 app.config['SECRET_KEY'],340 app.config.get('PGADMIN_SESSION_DISK_WRITE_DELAY', 10),341 skip_paths342 ),343 1000,344 skip_paths345 ))346 347 348def pga_unauthorised():349 350 lm = current_app.login_manager351 login_message = None352 353 if lm.login_message:354 if lm.localize_callback is not None:355 login_message = lm.localize_callback(lm.login_message)356 else:357 login_message = lm.login_message358 359 if not lm.login_view:360 # Only 401 is not enough to distinguish pgAdmin login is required.361 # There are other cases when we return 401. For eg. wrong password362 # supplied while connecting to server.363 # So send additional 'info' message.364 return make_json_response(365 status=401,366 success=0,367 errormsg=login_message,368 info='PGADMIN_LOGIN_REQUIRED'369 )370 371 # flash messages are only required if the request was from a372 # security page, otherwise it will be redirected to login page373 # anyway374 if login_message and 'security' in request.endpoint:375 flash(login_message, category=lm.login_message_category)376 377 return redirect(login_url(lm.login_view, request.url))378 379 380def cleanup_session_files():381 """382 This function will iterate through session directory and check the last383 modified time, if it older than (session expiration time + 1) days then384 delete that file.385 """386 iterate_session_files = False387 388 global LAST_CHECK_SESSION_FILES389 if LAST_CHECK_SESSION_FILES is None or \390 datetime.datetime.now() >= LAST_CHECK_SESSION_FILES + \391 datetime.timedelta(hours=config.CHECK_SESSION_FILES_INTERVAL):392 iterate_session_files = True393 LAST_CHECK_SESSION_FILES = datetime.datetime.now()394 395 if iterate_session_files:396 for root, dirs, files in os.walk(397 current_app.config['SESSION_DB_PATH']):398 for file_name in files:399 absolute_file_name = os.path.join(root, file_name)400 st = os.stat(absolute_file_name)401 402 # Get the last modified time of the session file403 last_modified_time = \404 datetime.datetime.fromtimestamp(st.st_mtime)405 406 # Calculate session file expiry time.407 file_expiration_time = \408 last_modified_time + \409 current_app.permanent_session_lifetime + \410 datetime.timedelta(days=1)411 412 if file_expiration_time <= datetime.datetime.now() and \413 os.path.exists(absolute_file_name):414 os.unlink(absolute_file_name)415 