diff --git a/docs/en_US/images/server_advanced.png b/docs/en_US/images/server_advanced.png new file mode 100644 index 000000000..3f365fc10 Binary files /dev/null and b/docs/en_US/images/server_advanced.png differ diff --git a/docs/en_US/server_dialog.rst b/docs/en_US/server_dialog.rst index 7a0c38598..3cd33c238 100644 --- a/docs/en_US/server_dialog.rst +++ b/docs/en_US/server_dialog.rst @@ -38,3 +38,10 @@ Use the fields in the *Connection* tab to configure a connection: * Click the *Cancel* button to exit without saving work. * Click the *Reset* button to restore configuration parameters. +Click the *Advanced* tab to continue. + +.. image:: images/server_advanced.png + +Use the fields in the *Advanced* tab to configure a connection: + +* Specify the IP address of the server host. Using this field to specify the host IP address will avoid a DNS lookup on connection, however it may be useful to specify both a host name and address when using Kerberos, GSSAPI, or SSPI authentication methods, as well as for verify-full SSL certificate verification \ No newline at end of file diff --git a/web/migrations/versions/3c1e4b6eda55_.py b/web/migrations/versions/3c1e4b6eda55_.py new file mode 100644 index 000000000..308e9ebee --- /dev/null +++ b/web/migrations/versions/3c1e4b6eda55_.py @@ -0,0 +1,35 @@ + +"""empty message + +Revision ID: 3c1e4b6eda55 +Revises: 09d53fca90c7 +Create Date: 2017-06-13 17:05:30.671859 + +""" +import base64 + +import sys +from alembic import op +from pgadmin.model import db, Server +import config +import os +from pgadmin.setup import get_version + + +# revision identifiers, used by Alembic. +revision = '3c1e4b6eda55' +down_revision = '09d53fca90c7' +branch_labels = None +depends_on = None + + +def upgrade(): + verison = get_version() + + db.engine.execute( + 'ALTER TABLE server ADD COLUMN hostaddr TEXT(1024)' + ) + + +def downgrade(): + pass diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 0634e3f03..130bf3672 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -8,7 +8,7 @@ ########################################################################## import simplejson as json - +import re import pgadmin.browser.server_groups as sg from flask import render_template, request, make_response, jsonify, \ current_app, url_for @@ -211,6 +211,26 @@ class ServerNode(PGChildNodeView): 'delete': 'pause_wal_replay', 'put': 'resume_wal_replay' }] }) + EXP_IP4 = "^\s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."\ + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."\ + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\."\ + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\s*$" + EXP_IP6 = '^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|'\ + '(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|'\ + '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|'\ + '(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|'\ + ':((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|'\ + '(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|'\ + '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|'\ + '(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|'\ + '[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|'\ + '((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|'\ + '(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|'\ + '1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|'\ + '((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$' + pat4 = re.compile(EXP_IP4) + pat6 = re.compile(EXP_IP6) + def nodes(self, gid): res = [] @@ -353,6 +373,7 @@ class ServerNode(PGChildNodeView): config_param_map = { 'name': 'name', 'host': 'host', + 'hostaddr': 'hostaddr', 'port': 'port', 'db': 'maintenance_db', 'username': 'username', @@ -378,13 +399,24 @@ class ServerNode(PGChildNodeView): request.data, encoding='utf-8' ) + if 'hostaddr' in data and data['hostaddr'] != '': + if not self.pat4.match(data['hostaddr']): + if not self.pat6.match(data['hostaddr']): + return make_json_response( + success=0, + status=400, + errormsg=gettext('Host address not valid') + ) + + + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) conn = manager.connection() connected = conn.connected() if connected: for arg in ( - 'host', 'port', 'db', 'username', 'sslmode', 'role' + 'host', 'hostaddr', 'port', 'db', 'username', 'sslmode', 'role' ): if arg in data: return forbidden( @@ -501,6 +533,7 @@ class ServerNode(PGChildNodeView): 'id': server.id, 'name': server.name, 'host': server.host, + 'hostaddr': server.hostaddr, 'port': server.port, 'db': server.maintenance_db, 'username': server.username, @@ -541,6 +574,15 @@ class ServerNode(PGChildNodeView): ) ) + if 'hostaddr' in data and data['hostaddr'] != '': + if not self.pat4.match(data['hostaddr']): + if not self.pat6.match(data['hostaddr']): + return make_json_response( + success=0, + status=400, + errormsg=gettext('Host address not valid') + ) + server = None try: @@ -549,6 +591,7 @@ class ServerNode(PGChildNodeView): servergroup_id=data[u'gid'] if u'gid' in data else gid, name=data[u'name'], host=data[u'host'], + hostaddr=data[u'hostaddr'] if u'hostaddr' in data else None, port=data[u'port'], maintenance_db=data[u'db'], username=data[u'username'], diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index 52d60f541..72d44d160 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -602,6 +602,7 @@ define('pgadmin.node.server', [ name: '', sslmode: 'prefer', host: '', + hostaddr: '', port: 5432, db: 'postgres', username: current_user.name, @@ -650,6 +651,9 @@ define('pgadmin.node.server', [ },{ id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], disabled: 'isConnected' + },{ + id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'), + mode: ['properties', 'edit', 'create'], disabled: 'isConnected' },{ id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], disabled: 'isConnected', min: 1024, max: 65535 @@ -697,26 +701,69 @@ define('pgadmin.node.server', [ var check_for_empty = function(id, msg) { var v = self.get(id); if ( - _.isUndefined(v) || String(v).replace(/^\s+|\s+$/g, '') == '' + _.isUndefined(v) || v === null || String(v).replace(/^\s+|\s+$/g, '') == '' ) { err[id] = msg; errmsg = errmsg || msg; + return true; + } else { + self.errorModel.unset(id); + return false; + } + } + var check_for_valid_ipv6 = function(val){ + // Regular expression for validating IPv6 address formats + var exps = ['^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|', + '(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|', + '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|', + '(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|', + ':((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|', + '(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|', + '2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|', + '(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|', + '[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|', + '((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|', + '(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|', + '1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|', + '((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$']; + + var exp = new RegExp(exps.join('')); + return exp.test(val.trim()); + } + var check_for_valid_ip = function(id, msg) { + var v4exps = "(^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)"; + var v4exp = new RegExp(v4exps); + var v = self.get(id); + if ( + v && !(v4exp.test(v.trim())) + ) { + if(!check_for_valid_ipv6(v)){ + err[id] = msg; + errmsg = msg; + } } else { self.errorModel.unset(id); } } if (!self.isNew() && 'id' in self.sessAttrs) { - err['id'] = gettext('The ID cannot be changed.');; + err['id'] = gettext('The ID cannot be changed.'); errmsg = err['id']; } else { self.errorModel.unset('id'); } check_for_empty('name', gettext('Name must be specified.')); - check_for_empty( - 'host', gettext('Hostname or address must be specified.') - ); + if (check_for_empty( + 'host', gettext('Either Host name or Host address must be specified.') + ) && check_for_empty('hostaddr', gettext('Either Host name or Host address must be specified.'))){ + errmsg = errmsg || gettext('Either Host name or Host address must be specified'); + } else { + errmsg = undefined; + delete err['host']; + delete err['hostaddr']; + } + check_for_empty( 'db', gettext('Maintenance database must be specified.') ); @@ -724,6 +771,9 @@ define('pgadmin.node.server', [ 'username', gettext('Username must be specified.') ); check_for_empty('port', gettext('Port must be specified.')); + check_for_valid_ip( + 'hostaddr', gettext('Host address must be valid IPv4 or IPv6 address.') + ); this.errorModel.set(err); if (_.size(err)) { diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index f7db56745..2205508ba 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -108,6 +108,7 @@ class Server(db.Model): ) name = db.Column(db.String(128), nullable=False) host = db.Column(db.String(128), nullable=False) + hostaddr = db.Column(db.String(128), nullable=True) port = db.Column( db.Integer(), db.CheckConstraint('port >= 1024 AND port <= 65534'), @@ -128,6 +129,8 @@ class Server(db.Model): backref=db.backref('server', cascade="all, delete-orphan"), lazy='joined') + + class ModulePreference(db.Model): """Define a preferences table for any modules.""" __tablename__ = 'module_preference' diff --git a/web/pgadmin/utils/driver/psycopg2/__init__.py b/web/pgadmin/utils/driver/psycopg2/__init__.py index 876b887b7..16342d46e 100644 --- a/web/pgadmin/utils/driver/psycopg2/__init__.py +++ b/web/pgadmin/utils/driver/psycopg2/__init__.py @@ -316,6 +316,7 @@ class Connection(BaseConnection): pg_conn = psycopg2.connect( host=mgr.host, + hostaddr=mgr.hostaddr, port=mgr.port, database=database, user=user, @@ -1106,6 +1107,7 @@ Failed to execute query (execute_void) for the server #{server_id} - {conn_id} try: pg_conn = psycopg2.connect( host=mgr.host, + hostaddr=mgr.hostaddr, port=mgr.port, database=self.db, user=mgr.user, @@ -1373,6 +1375,7 @@ Failed to reset the connection to the server due to following error: try: pg_conn = psycopg2.connect( host=self.manager.host, + hostaddr=self.manager.hostaddr, port=self.manager.port, database=self.db, user=self.manager.user, @@ -1519,6 +1522,7 @@ class ServerManager(object): self.sid = server.id self.host = server.host + self.hostaddr = server.hostaddr self.port = server.port self.db = server.maintenance_db self.did = None