From 3140dd0bca979ccb9c4467307848a4091a83256e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez?= Date: Wed, 3 Apr 2013 02:54:35 +0000 Subject: [PATCH] * Added new module "net" under uds.core.util * Moved out some method from models to net * Started a generic network validation/retrieval function * Added security to html5 guacamole proxy to UDS, allowing to restrict requests for credentials from an IP * Including "from __future__ import unicode_literals" (step forward python 3) --- .../org.eclipse.core.resources.prefs | 1 + server/src/uds/auths/IP/Authenticator.py | 8 +- server/src/uds/core/BaseModule.py | 1 + server/src/uds/core/Environment.py | 1 + server/src/uds/core/Serializable.py | 2 + server/src/uds/core/auths/auth.py | 2 + server/src/uds/core/util/net.py | 142 ++++++++++++++++++ server/src/uds/core/workers/__init__.py | 1 + server/src/uds/dispatchers/guacamole/views.py | 35 +++-- server/src/uds/models.py | 45 ++---- .../src/uds/transports/HTML5RDP/HTML5RDP.py | 13 +- 11 files changed, 199 insertions(+), 52 deletions(-) create mode 100644 server/src/uds/core/util/net.py diff --git a/server/.settings/org.eclipse.core.resources.prefs b/server/.settings/org.eclipse.core.resources.prefs index acedbf22..dee20d71 100644 --- a/server/.settings/org.eclipse.core.resources.prefs +++ b/server/.settings/org.eclipse.core.resources.prefs @@ -88,6 +88,7 @@ encoding//src/uds/core/util/UniqueNameGenerator.py=utf-8 encoding//src/uds/core/util/connection.py=utf-8 encoding//src/uds/core/util/log.py=utf-8 encoding//src/uds/core/util/modfinder.py=utf-8 +encoding//src/uds/core/util/net.py=utf-8 encoding//src/uds/core/util/stats/__init__.py=utf-8 encoding//src/uds/core/util/stats/charts.py=utf-8 encoding//src/uds/core/util/stats/counters.py=utf-8 diff --git a/server/src/uds/auths/IP/Authenticator.py b/server/src/uds/auths/IP/Authenticator.py index 154658cc..029d9beb 100644 --- a/server/src/uds/auths/IP/Authenticator.py +++ b/server/src/uds/auths/IP/Authenticator.py @@ -35,7 +35,7 @@ from django.utils.translation import ugettext_noop as _ from uds.core.auths import Authenticator from uds.core.auths.GroupsManager import GroupsManager -from uds.models import Network +from uds.core.util import net from uds.core.util.Config import Config import logging, random, string @@ -74,11 +74,11 @@ class IPAuth(Authenticator): def getGroups(self, ip, groupsManager): # these groups are a bit special. They are in fact ip-ranges, and we must check that the ip is in betwen # The ranges are stored in group names - ip = Network.ipToLong(ip) + ip = net.ipToLong(ip) for g in groupsManager.getGroupsNames(): rangeStart, rangeEnd = g.split('-') - rangeStart = Network.ipToLong(rangeStart) - rangeEnd = Network.ipToLong(rangeEnd) + rangeStart = net.ipToLong(rangeStart) + rangeEnd = net.ipToLong(rangeEnd) if ip >= rangeStart and ip <= rangeEnd: groupsManager.validate(g) diff --git a/server/src/uds/core/BaseModule.py b/server/src/uds/core/BaseModule.py index 33305cb9..016a50bd 100644 --- a/server/src/uds/core/BaseModule.py +++ b/server/src/uds/core/BaseModule.py @@ -30,6 +30,7 @@ ''' .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals from django.utils.translation import ugettext as _ from uds.core.ui.UserInterface import UserInterface diff --git a/server/src/uds/core/Environment.py b/server/src/uds/core/Environment.py index b0d2f189..1e99c070 100644 --- a/server/src/uds/core/Environment.py +++ b/server/src/uds/core/Environment.py @@ -30,6 +30,7 @@ ''' .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals TEMP_ENV = 'temporary' GLOBAL_ENV = 'global' diff --git a/server/src/uds/core/Serializable.py b/server/src/uds/core/Serializable.py index d8c26624..173653ff 100644 --- a/server/src/uds/core/Serializable.py +++ b/server/src/uds/core/Serializable.py @@ -30,6 +30,8 @@ ''' .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals + class Serializable(object): ''' diff --git a/server/src/uds/core/auths/auth.py b/server/src/uds/core/auths/auth.py index 95f40c6d..2ba475c8 100644 --- a/server/src/uds/core/auths/auth.py +++ b/server/src/uds/core/auths/auth.py @@ -33,6 +33,8 @@ Provides useful functions for authenticating, used by web interface. .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals + from functools import wraps from django.http import HttpResponseRedirect from uds.core.util.Config import GlobalConfig diff --git a/server/src/uds/core/util/net.py b/server/src/uds/core/util/net.py new file mode 100644 index 00000000..f0c99e6d --- /dev/null +++ b/server/src/uds/core/util/net.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals +import re + +from exceptions import ValueError + +# Test patters for networks +reCIDR = re.compile('^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$') +reMask = re.compile('^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})netmask([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$') +re1Asterisk = re.compile('^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.\*$') +re2Asterisk = re.compile('^([0-9]{1,3})\.([0-9]{1,3})\.\*(\.\*)?$') +re3Asterisk = re.compile('^([0-9]{1,3})\.\*(\.\*)?(\.\*)?$') +reRange = re.compile('^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})-([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$') +reHost = re.compile('^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$') + +def ipToLong(ip): + ''' + convert decimal dotted quad string to long integer + ''' + try: + hexn = ''.join(["%02X" % long(i) for i in ip.split('.')]) + return long(hexn, 16) + except: + return 0 # Invalid values will map to "0.0.0.0" --> 0 + +def longToIp(n): + ''' + convert long int to dotted quad string + ''' + try: + d = 256 * 256 * 256 + q = [] + while d > 0: + m,n = divmod(n,d) + q.append(str(m)) + d = d/256 + + return '.'.join(q) + except: + return '0.0.0.0' # Invalid values will map to "0.0.0.0" + + +def networksFromString(strNets, allowMultipleNetworks = True): + ''' + Parses the network from strings in this forms: + - A.* (or A.*.* or A.*.*.*) + - A.B.* (or A.B.*.* ) + - A.B.C.* (i.e. 192.168.0.*) + - A.B.C.D/N (i.e. 192.168.0.0/24) + - A.B.C.D netmask X.X.X.X (i.e. 192.168.0.0 netmask 255.255.255.0) + - A.B.C.D - E.F.G.D (i.e. 192-168.0.0-192.168.0.255) + - A.B.C.D + If allowMultipleNetworks is True, it allows ',' and ';' separators (and, ofc, more than 1 network) + Returns a list of networks tuples in the form [(start1, end1), (start2, end2) ...] + ''' + def check(*args): + for n in args: + if int(n) < 0 or int(n) > 255: + raise Exception() + + def toNum(*args): + start = 256*256*256 + val = 0 + for n in args: + val += start*int(n) + start /= 256 + return val + + def maskFromBits(nBits): + v = 0 + for n in xrange(nBits): + v |= 1<<(31-n) + return v + + nets = strNets.replace(' ', '') + if allowMultipleNetworks is True: + res = [] + for strNet in re.split('[;,]',nets): + if strNet != '': + res.append(networksFromString(strNet, False)) + return res + + try: + # Test patterns + m = reCIDR.match(strNets) + if m is not None: + check(*m.groups()) + bits = int(m.group(5)) + if bits < 0 | bits > 32: + raise Exception() + val = toNum(*m.groups()) + bits = maskFromBits(bits) + noBits = ~bits & 0xffffffff + return (val&bits, val|noBits) + + m = reMask.match(strNets) + if m is not None: + check(*m.groups()) + val = toNum(*(m.group(i+1) for i in xrange(4))) + bits = toNum(*(m.group(i+5) for i in xrange(4))) + noBits = ~bits & 0xffffffff + return (val&bits, val|noBits) + + # No pattern recognized, invalid network + raise Exception() + except: + raise ValueError(nets) + + + + \ No newline at end of file diff --git a/server/src/uds/core/workers/__init__.py b/server/src/uds/core/workers/__init__.py index f2f6bb37..0f28ca64 100644 --- a/server/src/uds/core/workers/__init__.py +++ b/server/src/uds/core/workers/__init__.py @@ -30,6 +30,7 @@ ''' .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals def __init__(): ''' diff --git a/server/src/uds/dispatchers/guacamole/views.py b/server/src/uds/dispatchers/guacamole/views.py index 445a65c8..e05933de 100644 --- a/server/src/uds/dispatchers/guacamole/views.py +++ b/server/src/uds/dispatchers/guacamole/views.py @@ -31,8 +31,11 @@ @author: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals + from django.http import HttpResponse from uds.core.util.Cache import Cache +from uds.core.util import net import logging logger = logging.getLogger(__name__) @@ -47,19 +50,31 @@ def dict2resp(dct): def guacamole(request, tunnelId): logger.debug('Received credentials request for tunnel id {0}'.format(tunnelId)) + + try: + cache = Cache('guacamole') + + val = cache.get(tunnelId, None) - cache = Cache('guacamole') - - val = cache.get(tunnelId, None) - - if val is None: + # Ensure request for credentials are allowed + allowFrom = val['allow-from'].replace(' ', '') + # and remove allow-from from parameters + del val['allow-from'] + + allowFrom = net.networksFromString(allowFrom) + + + # Remove key from cache, just 1 use + # Cache has a limit lifetime, so we will allow to "reload" the page + # cache.remove(tunnelId) + + #response = 'protocol\trdp\rhostname\tw7adolfo\rusername\tadmin\rpassword\ttemporal' + response = dict2resp(val) + + except: return HttpResponse(ERROR, content_type=CONTENT_TYPE) + - # Remove key from cache, just 1 use - # Cache has a limit lifetime, so we will allow to "reload" the page - # cache.remove(tunnelId) - #response = 'protocol\trdp\rhostname\tw7adolfo\rusername\tadmin\rpassword\ttemporal' - response = dict2resp(val) return HttpResponse(response, content_type=CONTENT_TYPE) diff --git a/server/src/uds/models.py b/server/src/uds/models.py index 89c16d30..8aae317e 100644 --- a/server/src/uds/models.py +++ b/server/src/uds/models.py @@ -40,6 +40,7 @@ from uds.core.Environment import Environment from uds.core.db.LockingManager import LockingManager from uds.core.util.State import State from uds.core.util import log +from uds.core.util import net from uds.core.services.Exceptions import InvalidServiceException from datetime import datetime, timedelta from time import mktime @@ -463,7 +464,7 @@ class Transport(models.Model): ''' if self.networks.count() == 0: return True - ip = Network.ipToLong(ip) + ip = net.ipToLong(ip) if self.nets_positive: return self.networks.filter(net_start__lte=ip, net_end__gte=ip).count() > 0 else: @@ -1971,40 +1972,12 @@ class Network(models.Model): net_end = models.BigIntegerField(db_index = True) transports = models.ManyToManyField(Transport, related_name='networks', db_table='uds_net_trans') - @staticmethod - def ipToLong(ip): - ''' - convert decimal dotted quad string to long integer - ''' - try: - hexn = ''.join(["%02X" % long(i) for i in ip.split('.')]) - return long(hexn, 16) - except: - return 0 # Invalid values will map to "0.0.0.0" --> 0 - - @staticmethod - def longToIp(n): - ''' - convert long int to dotted quad string - ''' - try: - d = 256 * 256 * 256 - q = [] - while d > 0: - m,n = divmod(n,d) - q.append(str(m)) - d = d/256 - - return '.'.join(q) - except: - return '0.0.0.0' # Invalid values will map to "0.0.0.0" - @staticmethod def networksFor(ip): ''' Returns the networks that are valid for specified ip in dotted quad (xxx.xxx.xxx.xxx) ''' - ip = Network.ipToLong(ip) + ip = net.ipToLong(ip) return Network.objects.filter(net_start__lte=ip, net_end__gte=ip) @staticmethod @@ -2017,7 +1990,7 @@ class Network(models.Model): netEnd: Network end ''' - return Network.objects.create(name=name, net_start = Network.ipToLong(netStart), net_end = Network.ipToLong(netEnd)) + return Network.objects.create(name=name, net_start = net.ipToLong(netStart), net_end = Network.ipToLong(netEnd)) @property def netStart(self): @@ -2027,7 +2000,7 @@ class Network(models.Model): Returns: string representing the dotted quad of this network start ''' - return Network.longToIp(self.net_start) + return net.longToIp(self.net_start) @property def netEnd(self): @@ -2037,7 +2010,7 @@ class Network(models.Model): Returns: string representing the dotted quad of this network end ''' - return Network.longToIp(self.net_end) + return net.longToIp(self.net_end) def update(self, name, netStart, netEnd): ''' @@ -2051,10 +2024,10 @@ class Network(models.Model): netEnd: new Network end (quad dotted) ''' self.name = name - self.net_start = Network.ipToLong(netStart) - self.net_end = Network.ipToLong(netEnd) + self.net_start = net.ipToLong(netStart) + self.net_end = net.ipToLong(netEnd) self.save() def __unicode__(self): - return u'Network {0} from {1} to {2}'.format(self.name, Network.longToIp(self.net_start), Network.longToIp(self.net_end)) + return u'Network {0} from {1} to {2}'.format(self.name, net.longToIp(self.net_start), net.longToIp(self.net_end)) diff --git a/server/src/uds/transports/HTML5RDP/HTML5RDP.py b/server/src/uds/transports/HTML5RDP/HTML5RDP.py index 8800a86d..5e98f0f5 100644 --- a/server/src/uds/transports/HTML5RDP/HTML5RDP.py +++ b/server/src/uds/transports/HTML5RDP/HTML5RDP.py @@ -31,9 +31,12 @@ @author: Adolfo Gómez, dkmaster at dkmon dot com ''' +from __future__ import unicode_literals + from django.utils.translation import ugettext_noop as _ from uds.core.ui.UserInterface import gui from uds.core.util.Cache import Cache +from uds.core.util import net from uds.core.transports.BaseTransport import Transport from uds.core.util import connection @@ -54,7 +57,9 @@ class HTML5RDPTransport(Transport): iconFile = 'rdp.png' needsJava = False # If this transport needs java for rendering - guacamoleServer = gui.TextField(label=_('Tunnel Server'), order = 1, tooltip = _('Host of the tunnel server (use http/http & port if needed)'), defvalue = 'https://') + guacamoleServer = gui.TextField(label=_('Tunnel Server'), order = 1, tooltip = _('Host of the tunnel server (use http/http & port if needed)'), defvalue = 'https://', length = 64) + allowRequestsFrom = gui.TextField(label=_('Allowed hosts'), order = 1, tooltip = _('Hosts allowed to ask for credentials for users (use * for all host, but not recommended). Comma separated list'), + defvalue = '*', length = 256) useEmptyCreds = gui.CheckBoxField(label = _('Empty creds'), order = 2, tooltip = _('If checked, the credentials used to connect will be emtpy')) fixedName = gui.TextField(label=_('Username'), order = 3, tooltip = _('If not empty, this username will be always used as credential')) fixedPassword = gui.PasswordField(label=_('Password'), order = 4, tooltip = _('If not empty, this password will be always used as credential')) @@ -66,6 +71,10 @@ class HTML5RDPTransport(Transport): a = '' if self.guacamoleServer.value[0:4] != 'http': raise Transport.ValidationException(_('The server must be http or https')) + try: + net.networksFromString(self.allowRequestsFrom.value) + except Exception as e: + raise Transport.ValidationException(_('Invalid network: {0}').format(str(e))) # Same check as normal RDP transport def isAvailableFor(self, ip): @@ -115,7 +124,7 @@ class HTML5RDPTransport(Transport): username = username + '@' + username # Build params dict - params = { 'protocol':'rdp', 'hostname':ip, 'username': username, 'password': password } + params = { 'protocol':'rdp', 'hostname':ip, 'username': username, 'password': password, 'allow-from': self.allowRequestsFrom.value } logger.debug('RDP Params: {0}'.format(params))