codekingpro/portable-devtools
114k
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"""The main pgAdmin module. This handles the application initialisation tasks,11such as setup of logging, dynamic loading of modules etc."""12import logging13import os14import sys15import re16import ipaddress17import traceback18import shutil19 20from types import MethodType21from collections import defaultdict22from importlib import import_module23 24from flask import Flask, abort, request, current_app, session, url_for25from flask_socketio import SocketIO26from werkzeug.exceptions import HTTPException27from flask_babel import Babel, gettext28from flask_babel import gettext as _29from flask_login import user_logged_in, user_logged_out30from flask_mail import Mail31from flask_paranoid import Paranoid32from flask_security import Security, SQLAlchemyUserDatastore, current_user33from flask_security.utils import login_user, logout_user34from flask_migrate import Migrate35from werkzeug.datastructures import ImmutableDict36from werkzeug.local import LocalProxy37from werkzeug.utils import find_modules38from jinja2 import select_autoescape39 40from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \41 User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION42from pgadmin.utils import PgAdminModule, driver, KeyManager, heartbeat43from pgadmin.utils.preferences import Preferences44from pgadmin.utils.session import create_session_interface, pga_unauthorised45from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader46from datetime import timedelta, datetime47from pgadmin.setup import get_version, set_version, check_db_tables48from pgadmin.utils.ajax import internal_server_error, make_json_response49from pgadmin.utils.csrf import pgCSRFProtect50from pgadmin import authenticate51from pgadmin.utils.security_headers import SecurityHeaders52from pgadmin.utils.constants import KERBEROS, OAUTH2, INTERNAL, LDAP, WEBSERVER53from jsonformatter import JsonFormatter54 55# Explicitly set the mime-types so that a corrupted windows registry will not56# affect pgAdmin 4 to be load properly. This will avoid the issues that may57# occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".58import mimetypes59 60mimetypes.add_type('application/javascript', '.js')61mimetypes.add_type('text/css', '.css')62 63 64winreg = None65if os.name == 'nt':66 import winreg67 68socketio = SocketIO(manage_session=False, async_mode='threading',69 logger=False, engineio_logger=False, debug=False,70 ping_interval=25, ping_timeout=120)71 72_INDEX_PATH = 'browser.index'73 74 75class PgAdmin(Flask):76 def __init__(self, *args, **kwargs):77 # Set the template loader to a postgres-version-aware loader78 self.jinja_options = ImmutableDict(79 autoescape=select_autoescape(enabled_extensions=('html', 'xml')),80 loader=VersionedTemplateLoader(self)81 )82 self.logout_hooks = []83 self.before_app_start = []84 85 super().__init__(*args, **kwargs)86 87 def find_submodules(self, basemodule):88 try:89 for module_name in find_modules(basemodule, True):90 if module_name in self.config['MODULE_BLACKLIST']:91 self.logger.info(92 'Skipping blacklisted module: %s' % module_name93 )94 continue95 self.logger.info(96 'Examining potential module: %s' % module_name)97 module = import_module(module_name)98 for key in list(module.__dict__.keys()):99 if isinstance(module.__dict__[key], PgAdminModule):100 yield module.__dict__[key]101 except Exception:102 return []103 104 @property105 def submodules(self):106 for blueprint in self.blueprints.values():107 if isinstance(blueprint, PgAdminModule):108 yield blueprint109 110 @property111 def messages(self):112 messages = dict()113 for module in self.submodules:114 messages.update(getattr(module, "messages", dict()))115 return messages116 117 @property118 def exposed_endpoint_url_map(self):119 #############################################################120 # To handle WSGI paths121 # If user has setup application under WSGI alias122 # like 'localhost/pgadmin4' then we have to append '/pgadmin4'123 # into endpoints124 #############################################################125 wsgi_root_path = ''126 if url_for(_INDEX_PATH) != '/browser/':127 wsgi_root_path = url_for(_INDEX_PATH).replace(128 '/browser/', ''129 )130 131 def get_full_url_path(url):132 """133 Generate endpoint URL at per WSGI alias134 """135 return wsgi_root_path + url136 137 # Fetch all endpoints and their respective url138 for rule in current_app.url_map.iter_rules('static'):139 yield rule.endpoint, get_full_url_path(rule.rule)140 141 for module in self.submodules:142 for endpoint in module.exposed_endpoints:143 for rule in current_app.url_map.iter_rules(endpoint):144 yield rule.endpoint, get_full_url_path(rule.rule)145 146 yield 'pgadmin.root', wsgi_root_path147 148 @property149 def menu_items(self):150 from operator import attrgetter151 152 menu_items = defaultdict(list)153 for module in self.submodules:154 for key, value in module.menu_items.items():155 menu_items[key].extend(value)156 menu_items = dict((key, sorted(value, key=attrgetter('priority')))157 for key, value in menu_items.items())158 return menu_items159 160 def register_logout_hook(self, module):161 if hasattr(module, 'on_logout') and \162 isinstance(getattr(module, 'on_logout'), MethodType):163 self.logout_hooks.append(module)164 165 def register_before_app_start(self, callback):166 self.before_app_start.append(callback)167 168 def run_before_app_start(self):169 # call before app starts or is exported170 with self.app_context(), self.test_request_context():171 for callback in self.before_app_start:172 callback()173 174 175def _find_blueprint():176 if request.blueprint:177 return current_app.blueprints[request.blueprint]178 179 180current_blueprint = LocalProxy(_find_blueprint)181 182 183def create_app(app_name=None):184 # Configuration settings185 import config186 if not app_name:187 app_name = config.APP_NAME188 189 # Check if app is created for CLI operations or Web190 cli_mode = False191 if app_name.endswith('-cli'):192 cli_mode = True193 194 # Only enable password related functionality in server mode.195 if config.SERVER_MODE is True:196 # Some times we need to access these config params where application197 # context is not available (we can't use current_app.config in those198 # cases even with current_app.app_context())199 # So update these params in config itself.200 # And also these updated config values will picked up by application201 # since we are updating config before the application instance is202 # created.203 204 config.SECURITY_RECOVERABLE = True205 config.SECURITY_CHANGEABLE = True206 # Now we'll open change password page in dialog207 # we don't want it to redirect to main page after password208 # change operation so we will open the same password change page again.209 config.SECURITY_POST_CHANGE_VIEW = 'browser.change_password'210 211 """Create the Flask application, startup logging and dynamically load212 additional modules (blueprints) that are found in this directory."""213 app = PgAdmin(__name__, static_url_path='/static')214 # Removes unwanted whitespace from render_template function215 app.jinja_env.trim_blocks = True216 app.config.from_object(config)217 app.config.update(dict(PROPAGATE_EXCEPTIONS=True))218 219 config.SETTINGS_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION220 ##########################################################################221 # Setup logging and log the application startup222 ##########################################################################223 224 # We won't care about errors in the logging system, we are more225 # interested in application errors.226 logging.raiseExceptions = False227 228 # Add SQL level logging, and set the base logging level229 logging.addLevelName(25, 'SQL')230 app.logger.setLevel(logging.DEBUG)231 app.logger.handlers = []232 233 # We also need to update the handler on the webserver in order to see234 # request. Setting the level prevents werkzeug from setting up it's own235 # stream handler thus ensuring all the logging goes through the pgAdmin236 # logger.237 logger = logging.getLogger('werkzeug')238 logger.setLevel(config.CONSOLE_LOG_LEVEL)239 240 # Set SQLITE_PATH to TEST_SQLITE_PATH while running test cases241 if (242 'PGADMIN_TESTING_MODE' in os.environ and243 os.environ['PGADMIN_TESTING_MODE'] == '1'244 ):245 config.SQLITE_PATH = config.TEST_SQLITE_PATH246 config.MASTER_PASSWORD_REQUIRED = False247 config.UPGRADE_CHECK_ENABLED = False248 249 if not cli_mode:250 # Ensure the various working directories exist251 from pgadmin.setup import create_app_data_directory252 create_app_data_directory(config)253 254 # File logging255 from pgadmin.utils.enhanced_log_rotation import \256 EnhancedRotatingFileHandler257 fh = EnhancedRotatingFileHandler(config.LOG_FILE,258 config.LOG_ROTATION_SIZE,259 config.LOG_ROTATION_AGE,260 config.LOG_ROTATION_MAX_LOG_FILES)261 262 fh.setLevel(config.FILE_LOG_LEVEL)263 264 if config.JSON_LOGGER:265 json_formatter = JsonFormatter(config.FILE_LOG_FORMAT_JSON)266 fh.setFormatter(json_formatter)267 else:268 fh.setFormatter(logging.Formatter(config.FILE_LOG_FORMAT))269 270 app.logger.addHandler(fh)271 logger.addHandler(fh)272 273 # Console logging274 ch = logging.StreamHandler()275 ch.setLevel(config.CONSOLE_LOG_LEVEL)276 277 if config.JSON_LOGGER:278 json_formatter = JsonFormatter(config.CONSOLE_LOG_FORMAT_JSON)279 ch.setFormatter(json_formatter)280 else:281 ch.setFormatter(logging.Formatter(config.CONSOLE_LOG_FORMAT))282 283 app.logger.addHandler(ch)284 logger.addHandler(ch)285 286 # Log the startup287 app.logger.info('########################################################')288 app.logger.info('Starting %s v%s...', config.APP_NAME, config.APP_VERSION)289 app.logger.info('########################################################')290 app.logger.debug("Python syspath: %s", sys.path)291 292 ##########################################################################293 # Setup i18n294 ##########################################################################295 296 # Initialise i18n297 babel = Babel(app)298 299 def get_locale():300 """Get the language for the user."""301 language = 'en'302 if config.SERVER_MODE is False:303 # Get the user language preference from the miscellaneous module304 user_id = None305 if current_user and current_user.is_authenticated:306 user_id = current_user.id307 else:308 user = user_datastore.find_user(email=config.DESKTOP_USER)309 if user is not None:310 user_id = user.id311 user_language = Preferences.raw_value(312 'misc', 'user_language', 'user_language', user_id313 )314 if user_language is not None:315 language = user_language316 else:317 # If language is available in get request then return the same318 # otherwise check the session or cookie319 data = request.form320 if 'language' in data:321 language = data['language'] or language322 setattr(session, 'PGADMIN_LANGUAGE', language)323 elif hasattr(session, 'PGADMIN_LANGUAGE'):324 language = getattr(session, 'PGADMIN_LANGUAGE', language)325 elif hasattr(request.cookies, 'PGADMIN_LANGUAGE'):326 language = getattr(327 request.cookies, 'PGADMIN_LANGUAGE', language328 )329 330 return language331 332 babel.init_app(app, locale_selector=get_locale)333 ##########################################################################334 # Setup authentication335 ##########################################################################336 if config.CONFIG_DATABASE_URI is not None and \337 len(config.CONFIG_DATABASE_URI) > 0:338 app.config['SQLALCHEMY_DATABASE_URI'] = config.CONFIG_DATABASE_URI339 else:340 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{0}?timeout={1}' \341 .format(config.SQLITE_PATH.replace('\\', '/'),342 getattr(config, 'SQLITE_TIMEOUT', 500)343 )344 345 # Override USER_DOES_NOT_EXIST and INVALID_PASSWORD messages from flask.346 app.config['SECURITY_MSG_USER_DOES_NOT_EXIST'] = \347 app.config['SECURITY_MSG_INVALID_PASSWORD'] = \348 (gettext("Incorrect username or password."), "error")349 app.config['SECURITY_PASSWORD_LENGTH_MIN'] = config.PASSWORD_LENGTH_MIN350 351 # Create database connection object and mailer352 db.init_app(app)353 Migrate(app, db)354 355 ##########################################################################356 # Upgrade the schema (if required)357 ##########################################################################358 from config import SQLITE_PATH359 from pgadmin.setup import db_upgrade360 361 def backup_db_file():362 """363 Create a backup of the current database file364 and create new database file with default settings.365 """366 backup_file_name = "{0}.{1}".format(367 SQLITE_PATH, datetime.now().strftime('%Y%m%d%H%M%S'))368 os.rename(SQLITE_PATH, backup_file_name)369 app.logger.error('Exception in database migration.')370 app.logger.info('Creating new database file.')371 try:372 db_upgrade(app)373 os.environ[374 'CORRUPTED_DB_BACKUP_FILE'] = backup_file_name375 app.logger.info('Database migration completed.')376 except Exception:377 app.logger.error('Database migration failed')378 app.logger.error(traceback.format_exc())379 raise RuntimeError('Migration failed')380 381 def upgrade_db():382 """383 Execute the migrations.384 """385 try:386 db_upgrade(app)387 os.environ['CORRUPTED_DB_BACKUP_FILE'] = ''388 except Exception:389 app.logger.error('Database migration failed')390 app.logger.error(traceback.format_exc())391 backup_db_file()392 393 # check all tables are present in the db.394 is_db_error, invalid_tb_names = check_db_tables()395 if is_db_error:396 app.logger.error(397 'Table(s) {0} are missing in the'398 ' database'.format(invalid_tb_names))399 backup_db_file()400 401 def run_migration_for_sqlite():402 with app.app_context():403 # Run migration for the first time i.e. create database404 # If version not available, user must have aborted. Tables are not405 # created and so its an empty db406 if not os.path.exists(SQLITE_PATH) or get_version() == -1:407 # If running in cli mode then don't try to upgrade, just raise408 # the exception409 if not cli_mode:410 upgrade_db()411 else:412 if not os.path.exists(SQLITE_PATH):413 raise FileNotFoundError(414 'SQLite database file "' + SQLITE_PATH +415 '" does not exists.')416 raise RuntimeError(417 'The configuration database file is not valid.')418 else:419 schema_version = get_version()420 421 # Run migration if current schema version is greater than the422 # schema version stored in version table423 if CURRENT_SCHEMA_VERSION > schema_version:424 # Take a backup of the old database file.425 try:426 prev_database_file_name = \427 "{0}.prev.bak".format(SQLITE_PATH)428 shutil.copyfile(SQLITE_PATH, prev_database_file_name)429 except Exception as e:430 app.logger.error(e)431 432 upgrade_db()433 else:434 # check all tables are present in the db.435 is_db_error, invalid_tb_names = check_db_tables()436 if is_db_error:437 app.logger.error(438 'Table(s) {0} are missing in the'439 ' database'.format(invalid_tb_names))440 backup_db_file()441 442 # Update schema version to the latest443 if CURRENT_SCHEMA_VERSION > schema_version:444 set_version(CURRENT_SCHEMA_VERSION)445 db.session.commit()446 447 if os.name != 'nt':448 os.chmod(config.SQLITE_PATH, 0o600)449 450 def run_migration_for_others():451 with app.app_context():452 # Run migration for the first time i.e. create database453 # If version not available, user must have aborted. Tables are not454 # created and so its an empty db455 if get_version() == -1:456 db_upgrade(app)457 else:458 schema_version = get_version()459 460 # Run migration if current schema version is greater than461 # the schema version stored in version table.462 if CURRENT_SCHEMA_VERSION > schema_version:463 db_upgrade(app)464 # Update schema version to the latest465 set_version(CURRENT_SCHEMA_VERSION)466 db.session.commit()467 468 # Run the migration as per specified by the user.469 if config.CONFIG_DATABASE_URI is not None and \470 len(config.CONFIG_DATABASE_URI) > 0:471 run_migration_for_others()472 else:473 run_migration_for_sqlite()474 475 Mail(app)476 477 # Don't bother paths when running in cli mode478 if not cli_mode:479 from pgadmin.utils import paths480 paths.init_app()481 482 # Setup Flask-Security483 user_datastore = SQLAlchemyUserDatastore(db, User, Role)484 security = Security(None, user_datastore)485 486 ##########################################################################487 # Setup security488 ##########################################################################489 with app.app_context():490 config.CSRF_SESSION_KEY = Keys.query.filter_by(491 name='CSRF_SESSION_KEY').first().value492 config.SECRET_KEY = Keys.query.filter_by(493 name='SECRET_KEY').first().value494 config.SECURITY_PASSWORD_SALT = Keys.query.filter_by(495 name='SECURITY_PASSWORD_SALT').first().value496 497 # Update the app.config with proper security keyes for signing CSRF data,498 # signing cookies, and the SALT for hashing the passwords.499 app.config.update(dict({500 'CSRF_SESSION_KEY': config.CSRF_SESSION_KEY,501 'SECRET_KEY': config.SECRET_KEY,502 'SECURITY_PASSWORD_SALT': config.SECURITY_PASSWORD_SALT,503 'SESSION_COOKIE_DOMAIN': config.SESSION_COOKIE_DOMAIN,504 # CSRF Token expiration till session expires505 'WTF_CSRF_TIME_LIMIT': getattr(config, 'CSRF_TIME_LIMIT', None),506 'WTF_CSRF_METHODS': ['GET', 'POST', 'PUT', 'DELETE'],507 # Disable deliverable check for email addresss508 'SECURITY_EMAIL_VALIDATOR_ARGS': config.SECURITY_EMAIL_VALIDATOR_ARGS,509 # Disable CSRF for unauthenticated endpoints510 'SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS': True511 }))512 513 app.config.update(dict({514 'INTERNAL': INTERNAL,515 'LDAP': LDAP,516 'KERBEROS': KERBEROS,517 'OAUTH2': OAUTH2,518 'WEBSERVER': WEBSERVER519 }))520 521 security.init_app(app, user_datastore)522 523 # register custom unauthorised handler.524 if sys.version_info < (3, 8):525 app.login_manager.unauthorized_handler(pga_unauthorised)526 else:527 # Flask-Security-Too > 5.4.* requires custom unauth handeler528 # to be registeres with it.529 security.unauthn_handler(pga_unauthorised)530 531 # Set the permanent session lifetime to the specified value in config file.532 app.permanent_session_lifetime = timedelta(533 days=config.SESSION_EXPIRATION_TIME)534 535 if not cli_mode:536 app.session_interface = create_session_interface(537 app, config.SESSION_SKIP_PATHS538 )539 540 # Make the Session more secure against XSS & CSRF when running in web mode541 if config.SERVER_MODE and config.ENHANCED_COOKIE_PROTECTION:542 paranoid = Paranoid(app)543 paranoid.redirect_view = _INDEX_PATH544 545 ##########################################################################546 # Load all available server drivers547 ##########################################################################548 driver.init_app(app)549 authenticate.init_app(app)550 heartbeat.init_app(app)551 552 ##########################################################################553 # Register language to the preferences after login554 ##########################################################################555 @user_logged_in.connect_via(app)556 def register_language(sender, user):557 # After logged in, set the language in the preferences if we get from558 # the login page559 data = request.form560 if 'language' in data:561 language = data['language']562 563 # Set the user language preference564 misc_preference = Preferences.module('misc')565 user_languages = misc_preference.preference(566 'user_language'567 )568 569 if user_languages and language:570 language = user_languages.set(language)571 572 ##########################################################################573 # Register any local servers we can discover574 ##########################################################################575 @user_logged_in.connect_via(app)576 def on_user_logged_in(sender, user):577 578 # If Auto Discover servers is turned off then return from the579 # function.580 if not config.AUTO_DISCOVER_SERVERS:581 return582 583 # Keep hold of the user ID584 user_id = user.id585 586 # Get the first server group for the user587 servergroup_id = 1588 servergroups = ServerGroup.query.filter_by(589 user_id=user_id590 ).order_by("id")591 592 if int(servergroups.count()) > 0:593 servergroup = servergroups.first()594 servergroup_id = servergroup.id595 596 '''Add a server to the config database'''597 598 def add_server(user_id, servergroup_id, name, superuser, port,599 discovery_id, comment):600 # Create a server object if needed, and store it.601 servers = Server.query.filter_by(602 user_id=user_id,603 discovery_id=svr_discovery_id604 ).order_by("id")605 606 if int(servers.count()) > 0:607 return608 609 svr = Server(user_id=user_id,610 servergroup_id=servergroup_id,611 name=name,612 host='localhost',613 port=port,614 maintenance_db='postgres',615 username=superuser,616 connection_params={'sslmode': 'prefer',617 'connect_timeout': 10},618 comment=comment,619 discovery_id=discovery_id)620 621 db.session.add(svr)622 db.session.commit()623 624 # Figure out what servers are present625 if winreg is not None:626 arch_keys = set()627 proc_arch = os.environ['PROCESSOR_ARCHITECTURE'].lower()628 629 try:630 proc_arch64 = os.environ['PROCESSOR_ARCHITEW6432'].lower()631 except Exception:632 proc_arch64 = None633 634 if proc_arch == 'x86' and not proc_arch64:635 arch_keys.add(0)636 elif proc_arch == 'x86' or proc_arch == 'amd64':637 arch_keys.add(winreg.KEY_WOW64_32KEY)638 arch_keys.add(winreg.KEY_WOW64_64KEY)639 640 for arch_key in arch_keys:641 for server_type in ('PostgreSQL', 'EnterpriseDB'):642 try:643 root_key = winreg.OpenKey(644 winreg.HKEY_LOCAL_MACHINE,645 "SOFTWARE\\" + server_type + "\\Services", 0,646 winreg.KEY_READ | arch_key647 )648 for i in range(0, winreg.QueryInfoKey(root_key)[0]):649 inst_id = winreg.EnumKey(root_key, i)650 inst_key = winreg.OpenKey(root_key, inst_id)651 652 svr_name = winreg.QueryValueEx(653 inst_key, 'Display Name'654 )[0]655 svr_superuser = winreg.QueryValueEx(656 inst_key, 'Database Superuser'657 )[0]658 svr_port = winreg.QueryValueEx(inst_key, 'Port')[0]659 svr_discovery_id = inst_id660 svr_comment = gettext(661 "Auto-detected {0} installation with the data "662 "directory at {1}").format(663 winreg.QueryValueEx(664 inst_key, 'Display Name'665 )[0],666 winreg.QueryValueEx(667 inst_key, 'Data Directory'668 )[0])669 670 add_server(671 user_id, servergroup_id, svr_name,672 svr_superuser, svr_port,673 svr_discovery_id, svr_comment674 )675 676 inst_key.Close()677 except Exception:678 pass679 else:680 # We use the postgres-winreg.ini file on non-Windows681 from configparser import ConfigParser682 683 registry = ConfigParser()684 685 try:686 registry.read('/etc/postgres-reg.ini')687 sections = registry.sections()688 689 # Loop the sections, and get the data from any that are PG or PPAS690 for section in sections:691 if (692 section.startswith('PostgreSQL/') or693 section.startswith('EnterpriseDB/')694 ):695 svr_name = registry.get(section, 'Description')696 svr_superuser = registry.get(section, 'Superuser')697 698 # getint function throws exception if value is blank.699 # Ex: Port=700 # In such case we should handle the exception and continue701 # to read the next section of the config file.702 try:703 svr_port = registry.getint(section, 'Port')704 except ValueError:705 continue706 707 svr_discovery_id = section708 description = registry.get(section, 'Description')709 data_directory = registry.get(section, 'DataDirectory')710 svr_comment = gettext("Auto-detected {0} installation "711 "with the data directory at {1}"712 ).format(description, data_directory)713 add_server(user_id, servergroup_id, svr_name,714 svr_superuser, svr_port, svr_discovery_id,715 svr_comment)716 717 except Exception as e:718 print(str(e))719 db.session.rollback()720 721 @user_logged_in.connect_via(app)722 @user_logged_out.connect_via(app)723 def force_session_write(app, user):724 session.force_write = True725 726 @user_logged_in.connect_via(app)727 def store_crypt_key(app, user):728 # in desktop mode, master password is used to encrypt/decrypt729 # and is stored in the keyManager memory730 if config.SERVER_MODE and 'password' in request.form:731 current_app.keyManager.set(request.form['password'])732 733 @user_logged_out.connect_via(app)734 def current_user_cleanup(app, user):735 from config import PG_DEFAULT_DRIVER736 from pgadmin.utils.driver import get_driver737 from flask import current_app738 739 for mdl in current_app.logout_hooks:740 try:741 mdl.on_logout()742 except Exception as e:743 current_app.logger.exception(e)744 745 _driver = get_driver(PG_DEFAULT_DRIVER)746 _driver.gc_own()747 748 # remove key749 current_app.keyManager.reset()750 751 ##########################################################################752 # Load plugin modules753 ##########################################################################754 from .submodules import get_submodules755 for module in get_submodules():756 app.logger.info('Registering blueprint module: %s' % module)757 if app.blueprints.get(module.name) is None:758 app.register_blueprint(module)759 app.register_logout_hook(module)760 761 @app.before_request762 def limit_host_addr():763 """764 This function validate the hosts from ALLOWED_HOSTS before allowing765 HTTP request to avoid Host Header Injection attack766 :return: None/JSON response with 403 HTTP status code767 """768 client_host = str(request.host).split(':', maxsplit=1)[0]769 valid = True770 allowed_hosts = config.ALLOWED_HOSTS771 772 if len(allowed_hosts) != 0:773 regex = re.compile(774 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2}|)')775 # Create separate list for ip addresses and host names776 ip_set = list(filter(lambda ip: regex.match(ip), allowed_hosts))777 host_set = list(filter(lambda ip: not regex.match(ip),778 allowed_hosts))779 is_ip = regex.match(client_host)780 if is_ip:781 ip_address = []782 for ip in ip_set:783 ip_address.extend(list(ipaddress.ip_network(ip)))784 valid = ip_address.__contains__(785 ipaddress.ip_address(client_host)786 )787 else:788 valid = host_set.__contains__(client_host)789 790 if not valid:791 return make_json_response(792 status=403, success=0,793 errormsg=_("403 FORBIDDEN")794 )795 796 ##########################################################################797 # Handle the desktop login798 ##########################################################################799 800 @app.before_request801 def before_request():802 """Login the default user if running in desktop mode"""803 804 # Check the auth key is valid, if it's set, and we're not in server805 # mode, and it's not a help file request.806 807 if not config.SERVER_MODE and app.PGADMIN_INT_KEY != '' and ((808 'key' not in request.args or809 request.args['key'] != app.PGADMIN_INT_KEY) and810 request.cookies.get('PGADMIN_INT_KEY') != app.PGADMIN_INT_KEY and811 request.endpoint != 'help.static'812 ):813 abort(401)814 815 if not config.SERVER_MODE and not current_user.is_authenticated:816 user = user_datastore.find_user(email=config.DESKTOP_USER)817 # Throw an error if we failed to find the desktop user, to give818 # the sysadmin a hint. We'll continue to try to login anyway as819 # that'll through a nice 500 error for us.820 if user is None:821 app.logger.error(822 'The desktop user %s was not found in the configuration '823 'database.'824 % config.DESKTOP_USER825 )826 abort(401)827 login_user(user)828 elif config.SERVER_MODE and not current_user.is_authenticated and \829 request.endpoint in ('redirects.index', 'security.login') and \830 app.PGADMIN_EXTERNAL_AUTH_SOURCE in [KERBEROS, WEBSERVER]:831 return authenticate.login()832 # if the server is restarted the in memory key will be lost833 # but the user session may still be active. Logout the user834 # to get the key again when login835 if config.SERVER_MODE and current_user.is_authenticated and \836 app.PGADMIN_EXTERNAL_AUTH_SOURCE not in [837 KERBEROS, OAUTH2, WEBSERVER] and \838 current_app.keyManager.get() is None and \839 request.endpoint not in ('security.login', 'security.logout'):840 logout_user()841 842 @app.after_request843 def after_request(response):844 if 'key' in request.args:845 domain = dict()846 if config.COOKIE_DEFAULT_DOMAIN and \847 config.COOKIE_DEFAULT_DOMAIN != 'localhost':848 domain['domain'] = config.COOKIE_DEFAULT_DOMAIN849 response.set_cookie('PGADMIN_INT_KEY', value=request.args['key'],850 path=config.SESSION_COOKIE_PATH,851 secure=config.SESSION_COOKIE_SECURE,852 httponly=config.SESSION_COOKIE_HTTPONLY,853 samesite=config.SESSION_COOKIE_SAMESITE,854 **domain)855 856 SecurityHeaders.set_response_headers(response)857 return response858 859 ##########################################################################860 # Cache busting861 ##########################################################################862 863 # Version number to be added to all static file url requests864 # This is used by url_for function when generating urls865 # This will solve caching issues when application is upgrading866 # This is called - Cache Busting867 @app.url_defaults868 def add_internal_version(endpoint, values):869 extensions = config.APP_VERSION_EXTN870 871 # Add the internal version only if it is set872 if config.APP_VERSION_PARAM is not None and \873 config.APP_VERSION_PARAM != '':874 # If there is a filename, add the version875 if 'filename' in values \876 and values['filename'].endswith(extensions):877 values[config.APP_VERSION_PARAM] = config.APP_VERSION_INT878 else:879 # Sometimes there may be direct endpoint for some files880 # There will be only one rule for such endpoints881 urls = [url for url in app.url_map.iter_rules(endpoint)]882 if len(urls) == 1 and urls[0].rule.endswith(extensions):883 values[config.APP_VERSION_PARAM] = \884 config.APP_VERSION_INT885 886 # Strip away internal version param before sending further to app as it was887 # required for cache busting only888 @app.url_value_preprocessor889 def strip_version_number(endpoint, values):890 if values and config.APP_VERSION_PARAM in values:891 values.pop(config.APP_VERSION_PARAM)892 893 ##########################################################################894 # Minify output. Not required in desktop mode895 ##########################################################################896 if not config.DEBUG and config.SERVER_MODE:897 from flask_compress import Compress898 Compress(app)899 900 @app.context_processor901 def inject_blueprint():902 """903 Inject a reference to the current blueprint, if any.904 """905 906 return {907 'current_app': current_app,908 'current_blueprint': current_blueprint,909 }910 911 @app.errorhandler(Exception)912 def all_exception_handler(e):913 current_app.logger.error(e, exc_info=True)914 return internal_server_error(errormsg=str(e))915 916 # Exclude HTTPexception from above handler (all_exception_handler)917 # HTTPException are user defined exceptions and those should be returned918 # as is919 @app.errorhandler(HTTPException)920 def http_exception_handler(e):921 current_app.logger.error(e, exc_info=True)922 return e923 924 # Intialize the key manager925 app.keyManager = KeyManager()926 927 ##########################################################################928 # Protection against CSRF attacks929 ##########################################################################930 with app.app_context():931 pgCSRFProtect.init_app(app)932 933 ##########################################################################934 # All done!935 ##########################################################################936 socketio.init_app(app, cors_allowed_origins="*")937 return app938 