diff --git a/websockify/django_auth_plugins.py b/websockify/django_auth_plugins.py new file mode 100644 index 00000000..04e8ab52 --- /dev/null +++ b/websockify/django_auth_plugins.py @@ -0,0 +1,144 @@ +''' +Django authentication plugins for Python WebSocket library. +Copyright 2015 Luca Capacci +Licensed under LGPL version 3 + + +**************************************** SessionIdAuth **************************************** + + SessionIdAuth grants access to the target only to the users authenticated in a django web app. + + Usage: put the websockify folder inside the django project, as shown below: + + django_project + |__________django_project + | |__________ settings.py + | | + | |__________ urls.py + | | + | |__________ wsgi.py + | + | + |__________ websockify + | |__________ some files... + | | + | |__________ websockify + | |_____________ django_auth_plugins.py + | + | + |__________ other files and folders... + + Right after starting the django web app, run websockify with the --auth-plugin option (Example: ./websockify/run 6080 localhost:5900 --auth-plugin="websockify.django_auth_plugins.SessionIdAuth") + + +**************************************** SessionIdAuthAndHostPort **************************************** + + SessionIdAuthAndHostPort determines the target based on the authenticated user. + + Usage: put the websockify folder inside the django project, as shown below: + + django_project + |__________django_project + | |__________ settings.py + | | + | |__________ urls.py + | | + | |__________ wsgi.py + | + | + |__________ websockify + | |__________ some files... + | | + | |__________ websockify + | |_____________ django_auth_plugins.py + | + | + |__________ other files and folders... + + + Edit get_host_port(current_user) to implement an algorithm to determine a target and a host for each user. + + Right after starting the django web app, run websockify with the --auth-plugin and the --auth-host-port options (Example: ./websockify/run 6080 --auth-plugin="websockify.django_auth_plugins.SessionIdAuthAndHostPort" --auth-host-port) + + For a complete example: https://github.com/lucacapacci/noVncDjangoPoC + +''' + + +from auth_plugins import AuthenticationError + + +class SessionIdAuth(object): + def __init__(self, src=None): + init_django() + + def authenticate(self, headers, target_host, target_port): + try: + cookies = headers.get('Cookie').split("; ") + for cookie in cookies: + if cookie.startswith("sessionid"): + session_token = cookie.split("=")[1] + current_user = user_from_session_key(session_token) + from django.contrib.auth.models import AnonymousUser + if type(current_user) is AnonymousUser: + raise AuthenticationError(response_code=403) + except: + raise AuthenticationError(response_code=403) + + +class SessionIdAuthAndHostPort(object): + def __init__(self, src=None): + init_django() + + def authenticate(self, headers, target_host, target_port): + try: + cookies = headers.get('Cookie').split("; ") + for cookie in cookies: + if cookie.startswith("sessionid"): + session_token = cookie.split("=")[1] + current_user = user_from_session_key(session_token) + from django.contrib.auth.models import AnonymousUser + if type(current_user) is AnonymousUser: + raise AuthenticationError(response_code=403) + return get_host_port(current_user) + except: + raise AuthenticationError(response_code=403) + + +def get_host_port(current_user): + host_port_dict = {'john': ('localhost', 5900), + 'bob': ('localhost', 5901)} + + if current_user.username in host_port_dict: + return host_port_dict[current_user.username] + else: + raise AuthenticationError(response_code=403) + + +def user_from_session_key(session_key): + from django.conf import settings + from django.contrib.auth import SESSION_KEY, BACKEND_SESSION_KEY, load_backend + from django.contrib.auth.models import AnonymousUser + + session_engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) + session_wrapper = session_engine.SessionStore(session_key) + session = session_wrapper.load() + user_id = session.get(SESSION_KEY) + backend_id = session.get(BACKEND_SESSION_KEY) + if user_id and backend_id: + auth_backend = load_backend(backend_id) + user = auth_backend.get_user(user_id) + if user: + return user + return AnonymousUser() + + +def init_django(): + import sys + import os + current_path = os.path.dirname(os.path.abspath(__file__)) + django_app_path = os.path.abspath(os.path.join(current_path, os.pardir, os.pardir)) + sys.path.insert(0, django_app_path) + os.environ['DJANGO_SETTINGS_MODULE'] = u'{0}.settings'.format(os.path.split(django_app_path)[1]) + import django + django.setup() diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 46ab5459..36515560 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -53,9 +53,16 @@ def validate_connection(self): if self.server.auth_plugin: try: - self.server.auth_plugin.authenticate( - headers=self.headers, target_host=self.server.target_host, - target_port=self.server.target_port) + if self.server.auth_host_port: + server_target_host, server_target_port = self.server.auth_plugin.authenticate(headers=self.headers, + target_host=self.server.target_host, + target_port=self.server.target_port) + self.server.target_host = server_target_host + self.server.target_port = server_target_port + else: + self.server.auth_plugin.authenticate( + headers=self.headers, target_host=self.server.target_host, + target_port=self.server.target_port) except auth.AuthenticationError: ex = sys.exc_info()[1] self.send_auth_error(ex) @@ -229,6 +236,7 @@ def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs): self.token_plugin = kwargs.pop('token_plugin', None) self.auth_plugin = kwargs.pop('auth_plugin', None) + self.auth_host_port = kwargs.pop('auth_host_port', False) # Last 3 timestamps command was run self.wrap_times = [0, 0, 0] @@ -288,6 +296,9 @@ def started(self): if self.token_plugin: msg = " - proxying from %s:%s to targets generated by %s" % ( self.listen_host, self.listen_port, type(self.token_plugin).__name__) + elif self.auth_host_port: + msg = " - proxying from %s:%s to targets generated by %s" % ( + self.listen_host, self.listen_port, type(self.auth_plugin).__name__) else: msg = " - proxying from %s:%s to %s" % ( self.listen_host, self.listen_port, dst_string) @@ -407,6 +418,8 @@ def websockify_init(): parser.add_option("--auth-source", default=None, metavar="ARG", help="an argument to be passed to the auth plugin" "on instantiation") + parser.add_option("--auth-host-port", action="store_true", + help="let the auth plugin set host and port") parser.add_option("--auto-pong", action="store_true", help="Automatically respond to ping frames with a pong") parser.add_option("--heartbeat", type=int, default=0, @@ -423,6 +436,8 @@ def websockify_init(): if opts.auth_source and not opts.auth_plugin: parser.error("You must use --auth-plugin to use --auth-source") + if opts.auth_host_port and not opts.auth_plugin: + parser.error("You must use --auth-plugin to use --auth-host-port") # Transform to absolute path as daemon may chdir if opts.target_cfg: @@ -435,7 +450,7 @@ def websockify_init(): del opts.target_cfg # Sanity checks - if len(args) < 2 and not (opts.token_plugin or opts.unix_target): + if len(args) < 2 and not (opts.token_plugin or opts.unix_target or opts.auth_host_port): parser.error("Too few arguments") if sys.argv.count('--'): opts.wrap_cmd = args[1:] @@ -460,7 +475,7 @@ def websockify_init(): try: opts.listen_port = int(opts.listen_port) except: parser.error("Error parsing listen port") - if opts.wrap_cmd or opts.unix_target or opts.token_plugin: + if opts.wrap_cmd or opts.unix_target or opts.token_plugin or opts.auth_host_port: opts.target_host = None opts.target_port = None else: