diff --git a/server/.pydevproject b/server/.pydevproject index f0c43afa2..280f5e7c7 100644 --- a/server/.pydevproject +++ b/server/.pydevproject @@ -1,8 +1,6 @@ - - - -python2.7 + +Default python 2.7 DJANGO_MANAGE_LOCATION diff --git a/server/.settings/org.eclipse.core.resources.prefs b/server/.settings/org.eclipse.core.resources.prefs index 308802c9b..117355f82 100644 --- a/server/.settings/org.eclipse.core.resources.prefs +++ b/server/.settings/org.eclipse.core.resources.prefs @@ -8,6 +8,10 @@ encoding//documentation/_downloads/samples/services/__init__.py=utf-8 encoding//documentation/conf.py=utf-8 encoding//src/server/settings.py=utf-8 encoding//src/server/urls.py=utf-8 +encoding//src/uds/REST/__init__.py=utf-8 +encoding//src/uds/REST/handlers.py=utf-8 +encoding//src/uds/REST/methods/authentication.py=utf-8 +encoding//src/uds/REST/processors.py=utf-8 encoding//src/uds/__init__.py=utf-8 encoding//src/uds/auths/ActiveDirectory/Authenticator.py=utf-8 encoding//src/uds/auths/ActiveDirectory/__init__.py=utf-8 diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 000000000..c157f04b0 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1 @@ +django-compressor \ No newline at end of file diff --git a/server/src/server/urls.py b/server/src/server/urls.py index 822d5f7e5..e575feb7e 100644 --- a/server/src/server/urls.py +++ b/server/src/server/urls.py @@ -2,8 +2,7 @@ ''' Url patterns for UDS project (Django) ''' - -from django.conf.urls.defaults import patterns, include +from django.conf.urls import patterns, include # Uncomment the next two lines to enable the admin: # from django.contrib import admin diff --git a/server/src/uds/REST/__init__.py b/server/src/uds/REST/__init__.py new file mode 100644 index 000000000..d85d1676f --- /dev/null +++ b/server/src/uds/REST/__init__.py @@ -0,0 +1,175 @@ +# -*- 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 + +from django import http +from django.views.generic.base import View +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.conf import settings +from handlers import Handler, HandlerError, AccessDenied + +import logging + +logger = logging.getLogger(__name__) + +__all__ = [ str(v) for v in ['Handler', 'Dispatcher'] ] + +class Dispatcher(View): + services = { '' : None } # Will include a default /rest handler, but rigth now this will be fine + + @method_decorator(csrf_exempt) + def dispatch(self, request, **kwargs): + import processors + + # Remove session, so response middelwares do nothing with this + del request.session + # Now we extract method and posible variables from path + path = kwargs['arguments'].split('/') + del kwargs['arguments'] + + # Transverse service nodes too look for path + service = Dispatcher.services + full_path = [] + # Last element will be + do_break = False + cls = None + while len(path) > 0 and not do_break: + # .json, .xml, ... will break path recursion + do_break = path[0].find('.') != -1 + clean_path = path[0].split('.')[0] + if service.has_key(clean_path): + service = service[clean_path] + full_path.append(path[0]) + path = path[1:] + else: + break + + full_path = '/'.join(full_path) + logger.debug(full_path) + + cls = service[''] + if cls is None: + return http.HttpResponseNotFound('method not found') + + + # Guess content type from content type header or ".xxx" to method + try: + p = full_path.split('.') + processor = processors.available_processors_ext_dict[p[1]](request) + except: + processor = processors.available_processors_mime_dict.get(request.META['CONTENT_TYPE'], processors.default_processor)(request) + + + # Obtain method to be invoked + http_method = request.method.lower() + + args = path + + # Inspect + lang = None + if len(args) > 0: + for l in settings.LANGUAGES: + if args[-1] == l[0]: + lang = l[0] + logger.error('Found lang {0}'.format(l)) + args = args[:-1] + break + # Intantiate method handler and locate http_method dispatcher + try: + handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs) + + operation = getattr(handler, http_method) + except processors.ParametersException as e: + return http.HttpResponseServerError('Invalid parameters invoking {0}: {1}'.format(path[0], e)) + except AttributeError: + allowedMethods = [] + for n in ['get', 'post', 'put', 'delete']: + if hasattr(handler, n): + allowedMethods.append(n) + return http.HttpResponseNotAllowed(allowedMethods) + except AccessDenied: + return http.HttpResponseForbidden('method access denied') + except: + logger.exception('error accessing attribute') + logger.debug('Getting attribute {0} for {1}'.format(http_method, full_path)) + return http.HttpResponseServerError('Unexcepected error') + + # Invokes the handler's operation, add headers to response and returns + try: + response = processor.getResponse(operation()) + for k, v in handler.headers().iteritems(): + response[k] = v + return response + except HandlerError as e: + return http.HttpResponseBadRequest(unicode(e)) + except Exception as e: + logger.exception('Error processing request') + return http.HttpResponseServerError(unicode(e)) + + # Initializes the dispatchers + @staticmethod + def initialize(): + ''' + This imports all packages that are descendant of this package, and, after that, + it register all subclases of service provider as + ''' + import os.path, pkgutil + import sys + + # Dinamycally import children of this package. The __init__.py files must register, if needed, inside ServiceProviderFactory + package = 'methods' + + pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package) + for _, name, _ in pkgutil.iter_modules([pkgpath]): + __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], -1) + + for cls in Handler.__subclasses__(): # @UndefinedVariable + # Skip ClusteredServiceProvider + if cls.name is None: + name = cls.__name__.lower() + else: + name = cls.name + logger.debug('Adding handler {0} for method {1} in path {2}'.format(cls, name, cls.path)) + service_node = Dispatcher.services + if cls.path is not None: + for k in cls.path.split('/'): + if service_node.get(k) is None: + service_node[k] = { '' : None } + service_node = service_node[k] + if service_node.get(name) is None: + service_node[name] = { '' : None } + + service_node[name][''] = cls + +Dispatcher.initialize() diff --git a/server/src/uds/REST/handlers.py b/server/src/uds/REST/handlers.py new file mode 100644 index 000000000..0b9c81f30 --- /dev/null +++ b/server/src/uds/REST/handlers.py @@ -0,0 +1,132 @@ +# -*- 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 +from django.contrib.sessions.backends.db import SessionStore +from django.utils.translation import activate +from django.conf import settings + +from uds.core.util.Config import GlobalConfig + +import logging + +logger = logging.getLogger(__name__) + +AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN' + +class HandlerError(Exception): + pass + +class AccessDenied(HandlerError): + pass + +class Handler(object): + name = None # If name is not used, name will be the class name in lower case + path = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way + authenticated = True # By default, all handlers needs authentication + only_admin = False # By default, the methods will be accesible by anyone + + # method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace' + def __init__(self, request, path, operation, params, *args, **kwargs): + + if self.only_admin: + self.authenticated = True # If only_admin, must also be authenticated + + self._request = request + self._path = path + self._operation = operation + self._params = params + self._args = args + self._kwargs = kwargs + self._headers = {} + self._authToken = None + if self.authenticated: # Only retrieve auth related data on authenticated handlers + try: + self._authToken = self._request.META.get(AUTH_TOKEN_HEADER, '') + self._session = SessionStore(session_key = self._authToken) + if not self._session.has_key('REST'): + raise Exception() # No valid session, so auth_token is also invalid + except: + if settings.DEBUG: + if self._authToken == 'a': + self.genAuthToken(-1, 'root', 'es', True) + else: + self._authToken = None + self._session = None + + + if self._authToken is None: + raise AccessDenied() + + if self.only_admin and not self.getValue('is_admin'): + raise AccessDenied() + + def headers(self): + return self._headers + + def header(self, header_): + return self._headers.get(header_) + + def addHeader(self, header, value): + self._headers[header] = value + + def removeHeader(self, header): + try: + del self._headers[header] + except: + pass + + # Auth related + def getAuthToken(self): + return self._authToken + + def genAuthToken(self, id_auth, username, locale, is_admin): + session = SessionStore() + session.set_expiry(GlobalConfig.ADMIN_IDLE_TIME.getInt()) + session['REST'] = { 'auth': id_auth, 'username': username, 'locale': locale, 'is_admin': is_admin } + session.save() + self._authToken = session.session_key + self._session = session + return self._authToken + + def cleanAuthToken(self): + self._authToken = None + if self._session: + self._session.delete() + self._session = None + + # Session related (from auth token) + def getValue(self, key): + try: + return self._session['REST'].get(key) + except: + return None diff --git a/server/src/uds/REST/methods/__init__.py b/server/src/uds/REST/methods/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/uds/REST/methods/authentication.py b/server/src/uds/REST/methods/authentication.py new file mode 100644 index 000000000..ff7585228 --- /dev/null +++ b/server/src/uds/REST/methods/authentication.py @@ -0,0 +1,105 @@ +# -*- 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 + +from uds.core.util.Config import GlobalConfig +from uds.models import Authenticator, User + +from uds.REST import Handler, HandlerError + +import logging + +logger = logging.getLogger(__name__) + + +class Login(Handler): + path = 'auth' + authenticated = False # By default, all handlers needs authentication + admin_method = False # By default, the methods will be accesible by anyone + + def post(self): + ''' + This login uses parameters to generate auth token + The alternative is to use the template tag inside "REST" that is called auth_token, that extracts an auth token from an user session + We can use any of this forms due to the fact that the auth token is in fact a session key + Parameters: + mandatory: + username: + password: + auth: + optional: + locale: (defaults to "en") + Result: + on success: { 'result': 'ok', 'auth': [auth_code] } + on error: { 'result: 'error', 'error': [error string] } + ''' + try: + username, auth, password = self._params['username'], self._params['auth'], self._params['password'] + locale = self._params.get('locale', 'en') + if auth == 'admin': + if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and GlobalConfig.SUPER_USER_PASS.get(True) == password: + self.genAuthToken(-1, username, locale, True) + return{'result': 'ok', 'token': self.getAuthToken()} + else: + raise Exception('Invalid credentials') + except Exception as e: + logger.exception('exception') + return {'result': 'error', 'error': unicode(e)} + + +class Logout(Handler): + path = 'auth' + authenticated = True # By default, all handlers needs authentication + admin_method = False # By default, the methods will be accesible by anyone + + def get(self): + # Remove auth token + self.cleanAuthToken() + return 'done' + + def post(self): + return self.get() + +class Auth(Handler): + authenticated = False # By default, all handlers needs authentication + admin_method = False # By default, the methods will be accesible by anyone + + def auths(self): + for a in Authenticator.all(): + if a.getType().isCustom() is False: + yield { 'auth' : str(a.small_name), 'name' : a.name } + + def get(self): + return list(self.auths()) + + \ No newline at end of file diff --git a/server/src/uds/REST/processors.py b/server/src/uds/REST/processors.py new file mode 100644 index 000000000..a97d0909e --- /dev/null +++ b/server/src/uds/REST/processors.py @@ -0,0 +1,98 @@ +# -*- 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 + +from django.utils import simplejson +from django import http + +import logging + +logger = logging.getLogger(__name__) + +class ParametersException(Exception): + pass + +class ContentProcessor(object): + mime_type = None + extensions = None + + def __init__(self, request): + self._request = request + + def processParameters(self): + return '' + + def getResponse(self, obj): + return http.HttpResponse(content = self.render(obj), content_type=self.mime_type + "; charset=utf-8") + + def render(self, obj): + return unicode(obj) + +# --------------- +# Json Processor +# --------------- +class JsonProcessor(ContentProcessor): + mime_type = 'application/json' + extensions = ['json'] + + def processParameters(self): + try: + if len(self._request.body) == 0: + return {} + res = simplejson.loads(self._request.body) + logger.debug(res) + return res + except Exception as e: + logger.error('parsing json: {0}'.format(e)) + raise ParametersException(unicode(e)) + + def render(self, obj): + return simplejson.dumps(obj) + +# --------------- +# Json Processor +# --------------- +class XMLProcessor(ContentProcessor): + mime_type = 'application/xml' + extensions = ['xml'] + + def processParameters(self): + return '' + +processors_list = (JsonProcessor,XMLProcessor) +default_processor = JsonProcessor +available_processors_mime_dict = dict((cls.mime_type, cls) for cls in processors_list) +available_processors_ext_dict = {} +for cls in processors_list: + for ext in cls.extensions: + available_processors_ext_dict[ext] = cls diff --git a/server/src/uds/dispatchers/guacamole/urls.py b/server/src/uds/dispatchers/guacamole/urls.py index 051b98577..1d48e3cb0 100644 --- a/server/src/uds/dispatchers/guacamole/urls.py +++ b/server/src/uds/dispatchers/guacamole/urls.py @@ -31,7 +31,7 @@ @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from django.conf.urls.defaults import patterns, include +from django.conf.urls import patterns urlpatterns = patterns(__package__, (r'^guacamole/(?P.+)$', 'views.guacamole'), diff --git a/server/src/uds/dispatchers/pam/urls.py b/server/src/uds/dispatchers/pam/urls.py index c80395a63..6ca906a1f 100644 --- a/server/src/uds/dispatchers/pam/urls.py +++ b/server/src/uds/dispatchers/pam/urls.py @@ -31,7 +31,7 @@ @author: Adolfo Gómez, dkmaster at dkmon dot com ''' -from django.conf.urls.defaults import patterns, include +from django.conf.urls import patterns urlpatterns = patterns(__package__, (r'^pam$', 'views.pam'), diff --git a/server/src/uds/urls.py b/server/src/uds/urls.py index d5fb5b3cb..6fc6a8c62 100644 --- a/server/src/uds/urls.py +++ b/server/src/uds/urls.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - # # Copyright (c) 2012 Virtual Cable S.L. # All rights reserved. @@ -32,8 +31,9 @@ ''' from django.conf import settings -from django.conf.urls.defaults import patterns, include +from django.conf.urls import patterns, include from uds.core.util.modfinder import loadModulesUrls +from uds import REST urlpatterns = patterns('uds', (r'^$', 'web.views.index'), @@ -64,8 +64,7 @@ urlpatterns = patterns('uds', (r'^authJava/(?P.+)/(?P.*)$', 'web.views.authJava'), (r'^authinfo/(?P.+)', 'web.views.authInfo'), (r'^about', 'web.views.about'), - (r'^about', 'web.views.about'), - + (r'^rest/(?P.*)$', REST.Dispatcher.as_view()), ) # Append urls from special dispatcher diff --git a/server/src/uds/xmlrpc/views.py b/server/src/uds/xmlrpc/views.py index 3d4717b5b..dda97d6fc 100644 --- a/server/src/uds/xmlrpc/views.py +++ b/server/src/uds/xmlrpc/views.py @@ -76,7 +76,7 @@ class XMLRPCDispatcher(SimpleXMLRPCDispatcher): def dispatch(self, request, **kwargs): import xmlrpclib - xml = request.raw_post_data + xml = request.body try: params, method = xmlrpclib.loads(xml) try: