diff --git a/gns3server/cert_utils/create_cert.sh b/gns3server/cert_utils/create_cert.sh new file mode 100755 index 00000000..57427088 --- /dev/null +++ b/gns3server/cert_utils/create_cert.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Bash shell script for generating self-signed certs. Run this in a folder, as it +# generates a few files. Large portions of this script were taken from the +# following artcile: +# +# http://usrportage.de/archives/919-Batch-generating-SSL-certificates.html +# +# Additional alterations by: Brad Landers +# Date: 2012-01-27 +# https://gist.github.com/bradland/1690807 + +# Script accepts a single argument, the fqdn for the cert + +DST_DIR="$HOME/.config/GNS3Certs/" +OLD_DIR=`pwd` + +#GNS3 Server expects to find certs with the default FQDN below. If you create +#different certs you will need to update server.py +DOMAIN="$1" +if [ -z "$DOMAIN" ]; then + DOMAIN="gns3server.localdomain.com" +fi + +fail_if_error() { + [ $1 != 0 ] && { + unset PASSPHRASE + cd $OLD_DIR + exit 10 + } +} + + +mkdir -p $DST_DIR +fail_if_error $? +cd $DST_DIR + + +# Generate a passphrase +export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo) + +# Certificate details; replace items in angle brackets with your own info +subj=" +C=CA +ST=Alberta +O=GNS3 +localityName=Calgary +commonName=gns3server.localdomain.com +organizationalUnitName=GNS3Server +emailAddress=gns3cert@gns3.com +" + +# Generate the server private key +openssl genrsa -aes256 -out $DST_DIR/$DOMAIN.key -passout env:PASSPHRASE 2048 +fail_if_error $? + +#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE + +# Generate the CSR +openssl req \ + -new \ + -batch \ + -subj "$(echo -n "$subj" | tr "\n" "/")" \ + -key $DOMAIN.key \ + -out $DOMAIN.csr \ + -passin env:PASSPHRASE +fail_if_error $? +cp $DOMAIN.key $DOMAIN.key.org +fail_if_error $? + +# Strip the password so we don't have to type it every time we restart Apache +openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE +fail_if_error $? + +# Generate the cert (good for 10 years) +openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt +fail_if_error $? + +cd $OLD_DIR \ No newline at end of file diff --git a/gns3server/handlers/auth_handler.py b/gns3server/handlers/auth_handler.py new file mode 100644 index 00000000..f136ab02 --- /dev/null +++ b/gns3server/handlers/auth_handler.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Simple file upload & listing handler. +""" + + +import os +import tornado.web +import tornado.websocket + +import logging +log = logging.getLogger(__name__) + +class GNS3BaseHandler(tornado.web.RequestHandler): + def get_current_user(self): + user = self.get_secure_cookie("user") + if not user: + return None + + if self.settings['required_user'] == user.decode("utf-8"): + return user + +class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler): + def get_current_user(self): + user = self.get_secure_cookie("user") + if not user: + return None + + if self.settings['required_user'] == user.decode("utf-8"): + return user + + +class LoginHandler(tornado.web.RequestHandler): + def get(self): + self.write('
' + 'Name: ' + 'Password: ' + '' + '
') + + try: + redirect_to = self.get_argument("next") + self.set_secure_cookie("login_success_redirect_to", redirect_to) + except tornado.web.MissingArgumentError: + pass + + def post(self): + + user = self.get_argument("name") + password = self.get_argument("password") + + if self.settings['required_user'] == user and self.settings['required_pass'] == password: + self.set_secure_cookie("user", user) + auth_status = "successful" + else: + self.set_secure_cookie("user", "None") + auth_status = "failure" + + log.info("Authentication attempt %s: %s" %(auth_status, user)) + + try: + redirect_to = self.get_secure_cookie("login_success_redirect_to") + except tornado.web.MissingArgumentError: + redirect_to = "/" + + self.redirect(redirect_to) \ No newline at end of file diff --git a/gns3server/handlers/file_upload_handler.py b/gns3server/handlers/file_upload_handler.py index c819a401..15673604 100644 --- a/gns3server/handlers/file_upload_handler.py +++ b/gns3server/handlers/file_upload_handler.py @@ -23,6 +23,7 @@ Simple file upload & listing handler. import os import stat import tornado.web +from .auth_handler import GNS3BaseHandler from ..version import __version__ from ..config import Config @@ -30,7 +31,7 @@ import logging log = logging.getLogger(__name__) -class FileUploadHandler(tornado.web.RequestHandler): +class FileUploadHandler(GNS3BaseHandler): """ File upload handler. @@ -54,6 +55,7 @@ class FileUploadHandler(tornado.web.RequestHandler): except OSError as e: log.error("could not create the upload directory {}: {}".format(self._upload_dir, e)) + @tornado.web.authenticated def get(self): """ Invoked on GET request. @@ -70,6 +72,7 @@ class FileUploadHandler(tornado.web.RequestHandler): path=path, items=items) + @tornado.web.authenticated def post(self): """ Invoked on POST request. diff --git a/gns3server/handlers/jsonrpc_websocket.py b/gns3server/handlers/jsonrpc_websocket.py index 26c96e6a..e14ae8c3 100644 --- a/gns3server/handlers/jsonrpc_websocket.py +++ b/gns3server/handlers/jsonrpc_websocket.py @@ -22,6 +22,7 @@ JSON-RPC protocol over Websockets. import zmq import uuid import tornado.websocket +from .auth_handler import GNS3WebSocketBaseHandler from tornado.escape import json_decode from ..jsonrpc import JSONRPCParseError from ..jsonrpc import JSONRPCInvalidRequest @@ -33,7 +34,7 @@ import logging log = logging.getLogger(__name__) -class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): +class JSONRPCWebSocket(GNS3WebSocketBaseHandler): """ STOMP protocol over Tornado Websockets with message routing to ZeroMQ dealer clients. @@ -119,7 +120,15 @@ class JSONRPCWebSocket(tornado.websocket.WebSocketHandler): """ log.info("Websocket client {} connected".format(self.session_id)) - self.clients.add(self) + + authenticated_user = self.get_current_user() + + if authenticated_user: + self.clients.add(self) + log.info("Websocket authenticated user: %s" % (authenticated_user)) + else: + self.close() + log.info("Websocket non-authenticated user attempt: %s" % (authenticated_user)) def on_message(self, message): """ diff --git a/gns3server/handlers/version_handler.py b/gns3server/handlers/version_handler.py index c85aa31c..3b338bd2 100644 --- a/gns3server/handlers/version_handler.py +++ b/gns3server/handlers/version_handler.py @@ -16,11 +16,13 @@ # along with this program. If not, see . import tornado.web +from .auth_handler import GNS3BaseHandler from ..version import __version__ -class VersionHandler(tornado.web.RequestHandler): +class VersionHandler(GNS3BaseHandler): + @tornado.web.authenticated def get(self): response = {'version': __version__} self.write(response) diff --git a/gns3server/server.py b/gns3server/server.py index d4869e53..275123ad 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -33,12 +33,16 @@ import tornado.ioloop import tornado.web import tornado.autoreload import pkg_resources +from os.path import expanduser +import base64 +import uuid from pkg_resources import parse_version from .config import Config from .handlers.jsonrpc_websocket import JSONRPCWebSocket from .handlers.version_handler import VersionHandler from .handlers.file_upload_handler import FileUploadHandler +from .handlers.auth_handler import LoginHandler from .builtins.server_version import server_version from .builtins.interfaces import interfaces from .modules import MODULES @@ -46,12 +50,12 @@ from .modules import MODULES import logging log = logging.getLogger(__name__) - class Server(object): # built-in handlers handlers = [(r"/version", VersionHandler), - (r"/upload", FileUploadHandler)] + (r"/upload", FileUploadHandler), + (r"/login", LoginHandler)] def __init__(self, host, port, ipc=False): @@ -136,11 +140,38 @@ class Server(object): JSONRPCWebSocket.register_destination(destination, instance.name) instance.start() # starts the new process + + def _get_cert_info(self): + """ + Finds the cert and key file needed for SSL + """ + + home = expanduser("~") + ssl_dir = "%s/.conf/GNS3Certs/" % (home) + log.debug("Looking for SSL certs in: %s" % (ssl_dir)) + + keyfile = "%s/gns3server.localdomain.com.key" % (ssl_dir) + certfile = "%s/gns3server.localdomain.com.crt" % (ssl_dir) + + if os.path.isfile(keyfile) and os.path.isfile(certfile): + return { "certfile" : certfile, + "keyfile" : keyfile, + } + def run(self): """ Starts the Tornado web server and ZeroMQ server. """ + # FIXME: debug mode! + settings = { + "debug":True, + "cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), + "login_url": "/login", + "required_user" : "test123", + "required_pass" : "test456", + } + router = self._create_zmq_router() # Add our JSON-RPC Websocket handler to Tornado self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))]) @@ -150,7 +181,7 @@ class Server(object): templates_dir = pkg_resources.resource_filename("gns3server", "templates") tornado_app = tornado.web.Application(self.handlers, template_path=templates_dir, - debug=True) # FIXME: debug mode! + **settings) # FIXME: debug mode! try: print("Starting server on {}:{} (Tornado v{}, PyZMQ v{}, ZMQ v{})".format(self._host, @@ -159,6 +190,13 @@ class Server(object): zmq.__version__, zmq.zmq_version())) kwargs = {"address": self._host} + + ssl_options = self._get_cert_info() + + if ssl_options: + log.info("Certs found - starting in SSL mode") + kwargs['ssl_options'] = ssl_options + if parse_version(tornado.version) >= parse_version("3.1"): kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit tornado_app.listen(self._port, **kwargs)