Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
server_manager.py699 linesDownload Raw Back to psycopg3
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"""11Implementation of ServerManager12"""13import os14import datetime15import config16import logging17from flask import current_app, session18from flask_security import current_user19from flask_babel import gettext20from werkzeug.exceptions import InternalServerError21 22from pgadmin.utils import get_complete_file_path23from pgadmin.utils.crypto import decrypt24from pgadmin.utils.master_password import process_masterpass_disabled25from .connection import Connection26from pgadmin.model import Server, User27from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\28    CryptKeyMissing29from pgadmin.utils.master_password import get_crypt_key30from pgadmin.utils.exception import ObjectGone31from pgadmin.utils.passexec import PasswordExec32from psycopg.conninfo import make_conninfo33 34if config.SUPPORT_SSH_TUNNEL:35    from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError36 37CONN_STRING = 'CONN:{0}'38DB_STRING = 'DB:{0}'39 40 41class ServerManager(object):42    """43    class ServerManager44 45    This class contains the information about the given server.46    And, acts as connection manager for that particular session.47    """48    _INFORMATION_MSG = gettext("Information is not available.")49 50    def __init__(self, server):51        self.connections = dict()52        self.local_bind_host = '127.0.0.1'53        self.local_bind_port = None54        self.tunnel_object = None55        self.tunnel_created = False56        self.display_connection_string = ''57 58        self.update(server)59 60    def update(self, server):61        assert (server is not None)62        assert (isinstance(server, Server))63 64        self.ver = None65        self.sversion = None66        self.server_type = None67        self.server_cls = None68        self.password = None69        self.tunnel_password = None70 71        self.sid = server.id72        self.host = server.host73        self.port = server.port74        self.db = server.maintenance_db75        self.shared = server.shared76        self.did = None77        self.user = server.username78        self.password = server.password79        self.role = server.role80        self.pinged = datetime.datetime.now()81        self.db_info = dict()82        self.server_types = None83        self.db_res = server.db_res84        self.name = server.name85        self.passexec = \86            PasswordExec(server.passexec_cmd, server.passexec_expiration) \87            if server.passexec_cmd else None88        self.service = server.service89 90        if config.SUPPORT_SSH_TUNNEL:91            self.use_ssh_tunnel = server.use_ssh_tunnel92            self.tunnel_host = server.tunnel_host93            self.tunnel_port = \94                22 if server.tunnel_port is None else server.tunnel_port95            self.tunnel_username = server.tunnel_username96            self.tunnel_authentication = 0 \97                if server.tunnel_authentication is None \98                else server.tunnel_authentication99            self.tunnel_identity_file = server.tunnel_identity_file100            self.tunnel_password = server.tunnel_password101            self.tunnel_keep_alive = server.tunnel_keep_alive102        else:103            self.use_ssh_tunnel = 0104            self.tunnel_host = None105            self.tunnel_port = 22106            self.tunnel_username = None107            self.tunnel_authentication = None108            self.tunnel_identity_file = None109            self.tunnel_password = None110            self.tunnel_keep_alive = 0111 112        self.kerberos_conn = server.kerberos_conn113        self.gss_authenticated = False114        self.gss_encrypted = False115        self.connection_params = server.connection_params116        self.create_connection_string(self.db, self.user)117        self.prepare_threshold = server.prepare_threshold118 119        for con in self.connections:120            self.connections[con]._release()121 122        self.update_session()123 124        self.connections = dict()125 126    def _set_password(self, res):127        """128        Set password for server manager object.129        :param res: response dict.130        :return:131        """132        if hasattr(self, 'password') and self.password:133            if hasattr(self.password, 'decode'):134                res['password'] = self.password.decode('utf-8')135            else:136                res['password'] = str(self.password)137        else:138            res['password'] = self.password139 140    def as_dict(self):141        """142        Returns a dictionary object representing the server manager.143        """144        if self.ver is None or len(self.connections) == 0:145            return None146 147        res = dict()148        res['sid'] = self.sid149        res['ver'] = self.ver150        res['sversion'] = self.sversion151 152        self._set_password(res)153 154        if self.use_ssh_tunnel:155            if hasattr(self, 'tunnel_password') and self.tunnel_password:156                if hasattr(self.tunnel_password, 'decode'):157                    res['tunnel_password'] = \158                        self.tunnel_password.decode('utf-8')159                else:160                    res['tunnel_password'] = str(self.tunnel_password)161            else:162                res['tunnel_password'] = self.tunnel_password163 164        connections = res['connections'] = dict()165 166        for conn_id in self.connections:167            conn = self.connections[conn_id].as_dict()168 169            if conn is not None:170                connections[conn_id] = conn171 172        return res173 174    def server_version(self):175        return self.ver176 177    @property178    def version(self):179        return self.sversion180 181    def major_version(self):182        if self.sversion is not None:183            return int(self.sversion / 10000)184        raise InternalServerError(self._INFORMATION_MSG)185 186    def minor_version(self):187        if self.sversion:188            return int(int(self.sversion / 100) % 100)189        raise InternalServerError(self._INFORMATION_MSG)190 191    def patch_version(self):192        if self.sversion:193            return int(int(self.sversion / 100) / 100)194        raise InternalServerError(self._INFORMATION_MSG)195 196    def connection(self, **kwargs):197        database = kwargs.get('database', None)198        conn_id = kwargs.get('conn_id', None)199        auto_reconnect = kwargs.get('auto_reconnect', True)200        did = kwargs.get('did', None)201        async_ = kwargs.get('async_', None)202        use_binary_placeholder = kwargs.get('use_binary_placeholder', False)203        array_to_string = kwargs.get('array_to_string', False)204 205        if database is not None:206            if did is not None and did in self.db_info:207                self.db_info[did]['datname'] = database208        else:209            conn_str = CONN_STRING.format(conn_id)210            if did is None:211                database = self.db212            elif did in self.db_info:213                database = self.db_info[did]['datname']214            elif conn_id and conn_str in self.connections:215                database = self.connections[conn_str].db216            else:217                maintenance_db_id = DB_STRING.format(self.db)218                if maintenance_db_id in self.connections:219                    conn = self.connections[maintenance_db_id]220                    # try to connect maintenance db if not connected221                    if not conn.connected():222                        conn.connect()223 224                    if conn.connected():225                        status, res = conn.execute_dict("""226SELECT227    db.oid as did, db.datname, db.datallowconn,228    pg_catalog.pg_encoding_to_char(db.encoding) AS serverencoding,229    pg_catalog.has_database_privilege(db.oid, 'CREATE') as cancreate,230    datistemplate231FROM232    pg_catalog.pg_database db233WHERE db.oid = {0}""".format(did))234 235                        if status and len(res['rows']) > 0:236                            for row in res['rows']:237                                self.db_info[did] = row238                                database = self.db_info[did]['datname']239 240                        if did not in self.db_info:241                            raise ObjectGone(gettext(242                                "Could not find the specified database."243                            ))244 245        if not get_crypt_key()[0] and (246                config.SERVER_MODE or config.DISABLED_LOCAL_PASSWORD_STORAGE):247            # the reason its not connected might be missing key248            raise CryptKeyMissing()249 250        if database is None:251            # Check SSH Tunnel is alive or not.252            if self.use_ssh_tunnel == 1:253                self.check_ssh_tunnel_alive()254            else:255                raise ConnectionLost(self.sid, None, None)256 257        my_id = (CONN_STRING.format(conn_id)) if conn_id is not None else \258            (DB_STRING.format(database))259 260        self.pinged = datetime.datetime.now()261 262        if my_id in self.connections:263            return self.connections[my_id]264        else:265            if async_ is None:266                async_ = 1 if conn_id is not None else 0267            else:268                async_ = 1 if async_ is True else 0269            self.connections[my_id] = Connection(270                self, my_id, database, auto_reconnect=auto_reconnect,271                async_=async_,272                use_binary_placeholder=use_binary_placeholder,273                array_to_string=array_to_string274            )275 276            return self.connections[my_id]277 278    @staticmethod279    def _get_password_to_conn(data, masterpass_processed):280        """281        Get password for connect to server with simple and ssh connection.282        :param data: Data.283        :param masterpass_processed:284        :return:285        """286        # The data variable is a copy so is not automatically synced287        # update here288        if masterpass_processed and 'password' in data:289            data['password'] = None290        if masterpass_processed and 'tunnel_password' in data:291            data['tunnel_password'] = None292 293    def _get_server_type(self):294        """295        Get server type and server cls.296        :return:297        """298        from pgadmin.browser.server_groups.servers.types import ServerType299 300        if self.ver and not self.server_type:301            for st in ServerType.types():302                if st.instance_of(self.ver):303                    self.server_type = st.stype304                    self.server_cls = st305                    break306 307    def _check_and_reconnect_server(self, conn, conn_info, data):308        """309        Check and try to reconnect the server if server previously connected310        and auto_reconnect is true.311        :param conn:312        :type conn:313        :param conn_info:314        :type conn_info:315        :param data:316        :type data:317        :return:318        :rtype:319        """320        from pgadmin.browser.server_groups.servers.types import ServerType321        if conn_info['wasConnected'] and conn_info['auto_reconnect']:322            try:323                # Check SSH Tunnel needs to be created324                if self.use_ssh_tunnel == 1 and \325                        not self.tunnel_created:326                    self.create_ssh_tunnel(data['tunnel_password'])327 328                    # Check SSH Tunnel is alive or not.329                    self.check_ssh_tunnel_alive()330 331                conn.connect(332                    password=data['password'],333                    server_types=ServerType.types()334                )335                # This will also update wasConnected flag in336                # connection so no need to update the flag manually.337            except CryptKeyMissing:338                # maintain the status as this will help to restore once339                # the key is available340                conn.wasConnected = conn_info['wasConnected']341                conn.auto_reconnect = conn_info['auto_reconnect']342            except Exception as e:343                current_app.logger.exception(e)344                self.connections.pop(conn_info['conn_id'])345                raise346 347    def _restore(self, data):348        """349        Helps restoring to reconnect the auto-connect connections smoothly on350        reload/restart of the app server..351        """352        # restore server version from flask session if flask server was353        # restarted. As we need server version to resolve sql template paths.354        masterpass_processed = process_masterpass_disabled()355 356        ServerManager._get_password_to_conn(data, masterpass_processed)357        # Get server type.358        self._get_server_type()359 360        # We need to know about the existing server variant supports during361        # first connection for identifications.362        self.pinged = datetime.datetime.now()363        try:364            if 'password' in data and data['password'] and \365                    hasattr(data['password'], 'encode'):366                data['password'] = data['password'].encode('utf-8')367            if 'tunnel_password' in data and data['tunnel_password']:368                data['tunnel_password'] = \369                    data['tunnel_password'].encode('utf-8')370        except Exception as e:371            current_app.logger.exception(e)372 373        connections = data['connections']374 375        for conn_id in connections:376            conn_info = connections[conn_id]377            if conn_info['conn_id'] in self.connections:378                conn = self.connections[conn_info['conn_id']]379            else:380                conn = self.connections[conn_info['conn_id']] = Connection(381                    self, conn_info['conn_id'], conn_info['database'],382                    auto_reconnect=conn_info['auto_reconnect'],383                    async_=conn_info['async_'],384                    use_binary_placeholder=conn_info[385                        'use_binary_placeholder'],386                    array_to_string=conn_info['array_to_string']387                )388 389            # only try to reconnect390            self._check_and_reconnect_server(conn, conn_info, data)391 392    def _restore_connections(self):393        for conn_id in self.connections:394            conn = self.connections[conn_id]395            # only try to reconnect if connection was connected previously396            # and auto_reconnect is true.397            was_connected = conn.wasConnected398            auto_reconnect = conn.auto_reconnect399            if conn.wasConnected and conn.auto_reconnect:400                try:401                    # Check SSH Tunnel needs to be created402                    if self.use_ssh_tunnel == 1 and \403                       not self.tunnel_created:404                        self.create_ssh_tunnel(self.tunnel_password)405 406                        # Check SSH Tunnel is alive or not.407                        self.check_ssh_tunnel_alive()408 409                    conn.connect()410                    # This will also update wasConnected flag in411                    # connection so no need to update the flag manually.412                except CryptKeyMissing:413                    # maintain the status as this will help to restore once414                    # the key is available415                    conn.wasConnected = was_connected416                    conn.auto_reconnect = auto_reconnect417                except Exception as e:418                    self.connections.pop(conn_id)419                    current_app.logger.exception(e)420                    raise421 422    def _stop_ssh_tunnel(self, did, database, conn_id):423        """424        Stop ssh tunnel connection if function call without any parameter.425        :param did: Database Id.426        :param database: Database.427        :param conn_id: COnnection Id.428        :return:429        """430        if database is None and conn_id is None and did is None:431            self.stop_ssh_tunnel()432 433    def _check_db_info(self, did, conn_id, database):434        """435        Check did is not none and it is resent in db_info.436        :param did: Database Id.437        :param conn_id: Connection Id.438        :return:439        """440        if database is None and conn_id is None and did is None:441            self.stop_ssh_tunnel()442 443        my_id = None444        if did is not None:445            if did in self.db_info and 'datname' in self.db_info[did]:446                database = self.db_info[did]['datname']447                if database is None:448                    return True, False, my_id449            else:450                return True, False, my_id451 452        if conn_id is not None:453            my_id = CONN_STRING.format(conn_id)454        elif database is not None:455            my_id = DB_STRING.format(database)456 457        return False, True, my_id458 459    def release(self, database=None, conn_id=None, did=None):460        # Stop the SSH tunnel if release() function calls without461        # any parameter.462        is_return, return_value, my_id = self._check_db_info(did, conn_id,463                                                             database)464        if is_return:465            return return_value466 467        if my_id is not None:468            if my_id in self.connections:469                self.connections[my_id]._release()470                del self.connections[my_id]471                if did is not None:472                    del self.db_info[did]473 474                if len(self.connections) == 0:475                    self.ver = None476                    self.sversion = None477                    self.server_type = None478                    self.server_cls = None479                    self.password = None480 481                self.update_session()482 483                return True484            else:485                return False486 487        for con_key in list(self.connections.keys()):488            conn = self.connections[con_key]489            # Cancel the ongoing transaction before closing the connection490            # as it may hang forever491            if conn.connected() and conn.conn_id is not None and \492               conn.conn_id.startswith('CONN:'):493                conn.cancel_transaction(conn.conn_id[5:])494            conn._release()495 496        self.connections = dict()497        self.ver = None498        self.sversion = None499        self.server_type = None500        self.server_cls = None501        self.password = None502 503        self.update_session()504 505        return True506 507    def _update_password(self, passwd):508        self.password = passwd509        for conn_id in self.connections:510            conn = self.connections[conn_id]511            if conn.conn is not None or conn.wasConnected is True:512                conn.password = passwd513 514    def update_session(self):515        managers = session['__pgsql_server_managers'] \516            if '__pgsql_server_managers' in session else dict()517        updated_mgr = self.as_dict()518 519        if not updated_mgr:520            if self.sid in managers:521                managers.pop(self.sid)522        else:523            managers[self.sid] = updated_mgr524        session['__pgsql_server_managers'] = managers525        session.force_write = True526 527    def utility(self, operation):528        """529        utility(operation)530 531        Returns: name of the utility which used for the operation532        """533        if self.server_cls is not None:534            return self.server_cls.utility(operation, self.sversion)535 536        return None537 538    def export_password_env(self, env):539        if self.password:540            if config.DISABLED_LOCAL_PASSWORD_STORAGE:541                crypt_key_present, crypt_key = get_crypt_key()542                if not crypt_key_present:543                    return False, crypt_key544                password = decrypt(self.password, crypt_key).decode()545            elif hasattr(self.password, 'decode'):546                password = self.password.decode('utf-8')547            else:548                password = self.password549 550            os.environ[str(env)] = password551        elif self.passexec:552            password = self.passexec.get()553            os.environ[str(env)] = password554 555    def create_ssh_tunnel(self, tunnel_password):556        """557        This method is used to create ssh tunnel and update the IP Address and558        IP Address and port to localhost and the local bind port return by the559        SSHTunnelForwarder class.560        :return: True if tunnel is successfully created else error message.561        """562        # Fetch Logged in User Details.563        user = User.query.filter_by(id=current_user.id).first()564        if user is None:565            return False, gettext("Unauthorized request.")566 567        if tunnel_password is not None and tunnel_password != '':568            if config.DISABLED_LOCAL_PASSWORD_STORAGE:569                crypt_key_present, crypt_key = get_crypt_key()570                if not crypt_key_present:571                    raise CryptKeyMissing()572 573            try:574                if config.DISABLED_LOCAL_PASSWORD_STORAGE:575                    tunnel_password = decrypt(tunnel_password, crypt_key)576                    # password is in bytes, for python3 we need it in string577                    if isinstance(tunnel_password, bytes):578                        tunnel_password = tunnel_password.decode()579 580            except Exception as e:581                current_app.logger.exception(e)582                return False, gettext("Failed to decrypt the SSH tunnel "583                                      "password.\nError: {0}").format(str(e))584 585        try:586            # If authentication method is 1 then it uses identity file587            # and password588            ssh_logger = None589            if current_app.debug:590                ssh_logger = logging.getLogger('sshtunnel')591                ssh_logger.setLevel(logging.DEBUG)592                for h in current_app.logger.handlers:593                    ssh_logger.addHandler(h)594            if self.tunnel_authentication == 1:595                self.tunnel_object = SSHTunnelForwarder(596                    (self.tunnel_host, int(self.tunnel_port)),597                    ssh_username=self.tunnel_username,598                    ssh_pkey=get_complete_file_path(self.tunnel_identity_file),599                    ssh_private_key_password=tunnel_password,600                    remote_bind_address=(self.host, self.port),601                    logger=ssh_logger,602                    set_keepalive=int(self.tunnel_keep_alive)603                )604            else:605                self.tunnel_object = SSHTunnelForwarder(606                    (self.tunnel_host, int(self.tunnel_port)),607                    ssh_username=self.tunnel_username,608                    ssh_password=tunnel_password,609                    remote_bind_address=(self.host, self.port),610                    logger=ssh_logger,611                    set_keepalive=int(self.tunnel_keep_alive)612                )613            # flag tunnel threads in daemon mode to fix hang issue.614            self.tunnel_object.daemon_forward_servers = True615            self.tunnel_object.start()616            self.tunnel_created = True617        except BaseSSHTunnelForwarderError as e:618            current_app.logger.exception(e)619            return False, gettext("Failed to create the SSH tunnel.\n"620                                  "Error: {0}").format(str(e))621 622        # Update the port to communicate locally623        self.local_bind_port = self.tunnel_object.local_bind_port624 625        return True, None626 627    def check_ssh_tunnel_alive(self):628        # Check SSH Tunnel is alive or not. if it is not then629        # raise the ConnectionLost exception.630        if self.tunnel_object is None or not self.tunnel_object.is_active:631            self.tunnel_created = False632            raise SSHTunnelConnectionLost(self.tunnel_host)633 634    def stop_ssh_tunnel(self):635        # Stop the SSH tunnel if created.636        if self.tunnel_object and self.tunnel_object.is_active:637            self.tunnel_object.stop()638            self.local_bind_port = None639            self.tunnel_object = None640            self.tunnel_created = False641 642    def get_connection_param_value(self, param_name):643        """644        This function return the value of param_name if found in the645        connection parameter.646        """647        value = None648        if self.connection_params and param_name in self.connection_params:649            value = self.connection_params[param_name]650 651        return value652 653    def create_connection_string(self, database, user, password=None):654        """655        This function is used to create connection string based on the656        parameters.657        """658        dsn_args = dict()659        dsn_args['host'] = self.host660        dsn_args['port'] = \661            self.local_bind_port if self.use_ssh_tunnel else self.port662        dsn_args['dbname'] = database663        dsn_args['user'] = user664        if self.service is not None:665            dsn_args['service'] = self.service666        if self.use_ssh_tunnel:667            dsn_args['hostaddr'] = self.local_bind_host668 669        # Make a copy to display the connection string on GUI.670        display_dsn_args = dsn_args.copy()671        # Password should not be visible into the connection string, so672        # setting the value with password to 'xxxxxxx'.673        if password:674            display_dsn_args['password'] = 'xxxxxxx'675            dsn_args['password'] = password676 677        # Loop through all the connection parameters set in the server dialog.678        if self.connection_params and isinstance(self.connection_params, dict):679            for key, value in self.connection_params.items():680                with_complete_path = False681                orig_value = value682                # Getting complete file path if the key is one of the below.683                if key in ['passfile', 'sslcert', 'sslkey', 'sslrootcert',684                           'sslcrl', 'sslcrldir']:685                    with_complete_path = True686                    value = get_complete_file_path(value)687 688                # If key is hostaddr and ssh tunnel is in use don't overwrite.689                if key == 'hostaddr' and self.use_ssh_tunnel:690                    continue691 692                dsn_args[key] = value693                display_dsn_args[key] = orig_value if with_complete_path else \694                    value695 696        self.display_connection_string = make_conninfo(**display_dsn_args)697 698        return make_conninfo(**dsn_args)699 
codekingpro/portable-devtools · Team Ai