1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-24 17:28:08 +00:00

Merge pull request #37 from planctechnologies/server_security2

Add secure communication between gui and server 2/2
This commit is contained in:
Jeremy Grossmann 2014-09-30 11:24:28 -06:00
commit b3e86be182
14 changed files with 397 additions and 97 deletions

View File

@ -101,8 +101,9 @@ def main():
startup_script) startup_script)
passwd = uuid.uuid4().hex passwd = uuid.uuid4().hex
instance.change_password(passwd) instance.change_password(passwd)
# wait for the password change to be processed # wait for the password change to be processed. Continuing while
sleep(POLL_SEC) # a password change is processing will cause image creation to fail.
sleep(POLL_SEC*6)
env.host_string = str(instance.accessIPv4) env.host_string = str(instance.accessIPv4)
env.user = "root" env.user = "root"

View File

@ -11,9 +11,7 @@ mkdir -p /opt/gns3
pushd /opt/gns3 pushd /opt/gns3
git clone --branch ${git_branch} ${git_url} git clone --branch ${git_branch} ${git_url}
cd gns3-server cd gns3-server
pip3 install tornado pip3 install -r dev-requirements.txt
pip3 install pyzmq
pip3 install jsonschema
python3 ./setup.py install python3 ./setup.py install
${rc_local} ${rc_local}

View File

@ -77,6 +77,7 @@ Options:
--cloud_user_name --cloud_user_name
--instance_id ID of the Rackspace instance to terminate --instance_id ID of the Rackspace instance to terminate
--region Region of instance
--deadtime How long in seconds can the communication lose exist before we --deadtime How long in seconds can the communication lose exist before we
shutdown this instance. shutdown this instance.
@ -111,6 +112,7 @@ def parse_cmd_line(argv):
"cloud_user_name=", "cloud_user_name=",
"cloud_api_key=", "cloud_api_key=",
"instance_id=", "instance_id=",
"region=",
"deadtime=", "deadtime=",
"init-wait=", "init-wait=",
"check-interval=", "check-interval=",
@ -130,6 +132,7 @@ def parse_cmd_line(argv):
cmd_line_option_list["cloud_user_name"] = None cmd_line_option_list["cloud_user_name"] = None
cmd_line_option_list["cloud_api_key"] = None cmd_line_option_list["cloud_api_key"] = None
cmd_line_option_list["instance_id"] = None cmd_line_option_list["instance_id"] = None
cmd_line_option_list["region"] = None
cmd_line_option_list["deadtime"] = 60 * 60 #minutes cmd_line_option_list["deadtime"] = 60 * 60 #minutes
cmd_line_option_list["check-interval"] = None cmd_line_option_list["check-interval"] = None
cmd_line_option_list["init-wait"] = 5 * 60 cmd_line_option_list["init-wait"] = 5 * 60
@ -162,6 +165,8 @@ def parse_cmd_line(argv):
cmd_line_option_list["cloud_api_key"] = val cmd_line_option_list["cloud_api_key"] = val
elif (opt in ("--instance_id")): elif (opt in ("--instance_id")):
cmd_line_option_list["instance_id"] = val cmd_line_option_list["instance_id"] = val
elif (opt in ("--region")):
cmd_line_option_list["region"] = val
elif (opt in ("--deadtime")): elif (opt in ("--deadtime")):
cmd_line_option_list["deadtime"] = int(val) cmd_line_option_list["deadtime"] = int(val)
elif (opt in ("--check-interval")): elif (opt in ("--check-interval")):
@ -200,6 +205,12 @@ def parse_cmd_line(argv):
print(usage) print(usage)
sys.exit(2) sys.exit(2)
if cmd_line_option_list["region"] is None:
print("You need to specify a region")
print(usage)
sys.exit(2)
return cmd_line_option_list return cmd_line_option_list
def get_gns3secrets(cmd_line_option_list): def get_gns3secrets(cmd_line_option_list):
@ -208,19 +219,19 @@ def get_gns3secrets(cmd_line_option_list):
""" """
gns3secret_paths = [ gns3secret_paths = [
os.path.expanduser("~/"), os.path.join(os.path.expanduser("~"), '.config', 'GNS3'),
SCRIPT_PATH, SCRIPT_PATH,
] ]
config = configparser.ConfigParser() config = configparser.ConfigParser()
for gns3secret_path in gns3secret_paths: for gns3secret_path in gns3secret_paths:
gns3secret_file = "%s/.gns3secrets.conf" % (gns3secret_path) gns3secret_file = "%s/cloud.conf" % (gns3secret_path)
if os.path.isfile(gns3secret_file): if os.path.isfile(gns3secret_file):
config.read(gns3secret_file) config.read(gns3secret_file)
try: try:
for key, value in config.items("Cloud"): for key, value in config.items("CLOUD_SERVER"):
cmd_line_option_list[key] = value.strip() cmd_line_option_list[key] = value.strip()
except configparser.NoSectionError: except configparser.NoSectionError:
pass pass
@ -342,7 +353,7 @@ def main():
log.info("Received shutdown signal") log.info("Received shutdown signal")
options["shutdown"] = True options["shutdown"] = True
pid_file = "%s/.gns3ias.pid" % (expanduser("~")) pid_file = "%s/.gns3dms.pid" % (expanduser("~"))
if options["shutdown"]: if options["shutdown"]:
send_shutdown(pid_file) send_shutdown(pid_file)

View File

@ -60,6 +60,15 @@ class daemon:
def delpid(self): def delpid(self):
os.remove(self.pidfile) os.remove(self.pidfile)
def check_pid(self, pid):
""" Check For the existence of a unix pid. """
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
def start(self): def start(self):
"""Start the daemon.""" """Start the daemon."""
@ -72,10 +81,16 @@ class daemon:
pid = None pid = None
if pid: if pid:
message = "pidfile {0} already exist. " + \ pid_exist = self.check_pid(pid)
"Daemon already running?\n"
sys.stderr.write(message.format(self.pidfile)) if pid_exist:
message = "Already running: %s\n" % (pid)
sys.stderr.write(message)
sys.exit(1) sys.exit(1)
else:
message = "pidfile {0} already exist. " + \
"but process is dead\n"
sys.stderr.write(message.format(self.pidfile))
# Start the daemon # Start the daemon
self.daemonize() self.daemonize()

View File

@ -41,6 +41,7 @@ class Rackspace(object):
self.authenticated = False self.authenticated = False
self.hostname = socket.gethostname() self.hostname = socket.gethostname()
self.instance_id = options["instance_id"] self.instance_id = options["instance_id"]
self.region = options["region"]
log.debug("Authenticating with Rackspace") log.debug("Authenticating with Rackspace")
log.debug("My hostname: %s" % (self.hostname)) log.debug("My hostname: %s" % (self.hostname))
@ -51,10 +52,11 @@ class Rackspace(object):
if self.authenticated == False: if self.authenticated == False:
log.critical("Not authenticated against rackspace!!!!") log.critical("Not authenticated against rackspace!!!!")
for region_dict in self.rksp.list_regions(): for region in self.rksp.list_regions():
region_k, region_v = region_dict.popitem() log.debug("Rackspace regions: %s" % (region))
log.debug("Checking region: %s" % (region_k))
self.rksp.set_region(region_v) log.debug("Checking region: %s" % (self.region))
self.rksp.set_region(self.region)
for server in self.rksp.list_instances(): for server in self.rksp.list_instances():
log.debug("Checking server: %s" % (server.name)) log.debug("Checking server: %s" % (server.name))
if server.name.lower() == self.hostname.lower() and server.id == self.instance_id: if server.name.lower() == self.hostname.lower() and server.id == self.instance_id:

View File

@ -62,13 +62,13 @@ C=CA
ST=Alberta ST=Alberta
O=GNS3 O=GNS3
localityName=Calgary localityName=Calgary
commonName=gns3server.localdomain.com commonName=$DOMAIN
organizationalUnitName=GNS3Server organizationalUnitName=GNS3Server
emailAddress=gns3cert@gns3.com emailAddress=gns3cert@gns3.com
" "
# Generate the server private key # Generate the server private key
openssl genrsa -aes256 -out $DST_DIR/$DOMAIN.key -passout env:PASSPHRASE 2048 openssl genrsa -aes256 -out $DOMAIN.key -passout env:PASSPHRASE 2048
fail_if_error $? fail_if_error $?
#openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE #openssl rsa -outform der -in $DOMAIN.pem -out $DOMAIN.key -passin env:PASSPHRASE
@ -93,4 +93,7 @@ fail_if_error $?
openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt
fail_if_error $? fail_if_error $?
echo "${DST_DIR}${DOMAIN}.key"
echo "${DST_DIR}${DOMAIN}.crt"
cd $OLD_DIR cd $OLD_DIR

View File

@ -62,16 +62,21 @@ class Config(object):
# 5: server.conf in the current working directory # 5: server.conf in the current working directory
home = os.path.expanduser("~") home = os.path.expanduser("~")
self._cloud_config = os.path.join(home, ".config", appname, "cloud.conf")
filename = "server.conf" filename = "server.conf"
self._files = [os.path.join(home, ".config", appname, filename), self._files = [os.path.join(home, ".config", appname, filename),
os.path.join(home, ".config", appname + ".conf"), os.path.join(home, ".config", appname + ".conf"),
os.path.join("/etc/xdg", appname, filename), os.path.join("/etc/xdg", appname, filename),
os.path.join("/etc/xdg", appname + ".conf"), os.path.join("/etc/xdg", appname + ".conf"),
filename] filename,
self._cloud_config]
self._config = configparser.ConfigParser() self._config = configparser.ConfigParser()
self.read_config() self.read_config()
def list_cloud_config_file(self):
return self._cloud_config
def read_config(self): def read_config(self):
""" """
Read the configuration files. Read the configuration files.

View File

@ -29,6 +29,9 @@ log = logging.getLogger(__name__)
class GNS3BaseHandler(tornado.web.RequestHandler): class GNS3BaseHandler(tornado.web.RequestHandler):
def get_current_user(self): def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user") user = self.get_secure_cookie("user")
if not user: if not user:
return None return None
@ -38,6 +41,9 @@ class GNS3BaseHandler(tornado.web.RequestHandler):
class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler): class GNS3WebSocketBaseHandler(tornado.websocket.WebSocketHandler):
def get_current_user(self): def get_current_user(self):
if 'required_user' not in self.settings:
return "FakeUser"
user = self.get_secure_cookie("user") user = self.get_secure_cookie("user")
if not user: if not user:
return None return None

View File

@ -22,7 +22,6 @@ from ..version import __version__
class VersionHandler(GNS3BaseHandler): class VersionHandler(GNS3BaseHandler):
@tornado.web.authenticated
def get(self): def get(self):
response = {'version': __version__} response = {'version': __version__}
self.write(response) self.write(response)

View File

@ -17,12 +17,13 @@
import sys import sys
from .base import IModule from .base import IModule
from .deadman import DeadMan
from .dynamips import Dynamips from .dynamips import Dynamips
from .qemu import Qemu
from .vpcs import VPCS from .vpcs import VPCS
from .virtualbox import VirtualBox from .virtualbox import VirtualBox
from .qemu import Qemu
MODULES = [Dynamips, VPCS, VirtualBox, Qemu] MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
# IOU runs only on Linux # IOU runs only on Linux

View File

@ -30,7 +30,7 @@ from gns3server.config import Config
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DeadMan(): class DeadMan(IModule):
""" """
DeadMan module. DeadMan module.
@ -54,8 +54,18 @@ class DeadMan():
if 'heartbeat_file' in kwargs: if 'heartbeat_file' in kwargs:
self._heartbeat_file = kwargs['heartbeat_file'] self._heartbeat_file = kwargs['heartbeat_file']
self._is_enabled = False
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
instance_id = cloud_config["instance_id"]
cloud_user_name = cloud_config["cloud_user_name"]
cloud_api_key = cloud_config["cloud_api_key"]
self._is_enabled = True
except KeyError:
log.critical("Missing cloud.conf - disabling Deadman Switch")
self._deadman_process = None self._deadman_process = None
self.heartbeat()
self.start() self.start()
def _start_deadman_process(self): def _start_deadman_process(self):
@ -63,14 +73,19 @@ class DeadMan():
Start a subprocess and return the object Start a subprocess and return the object
""" """
#gnsserver gets configuration options from cloud.conf. This is where
#the client adds specific cloud information.
#gns3dms also reads in cloud.conf. That is why we don't need to specific
#all the command line arguments here.
cmd = [] cmd = []
cmd.append("gns3dms") cmd.append("gns3dms")
cmd.append("--file %s" % (self._heartbeat_file)) cmd.append("--file")
cmd.append("%s" % (self._heartbeat_file))
cmd.append("--background") cmd.append("--background")
log.debug("Deadman: Running %s"%(cmd)) log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, shell=False) process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, shell=False)
return process return process
def _stop_deadman_process(self): def _stop_deadman_process(self):
@ -82,7 +97,7 @@ class DeadMan():
cmd.append("gns3dms") cmd.append("gns3dms")
cmd.append("-k") cmd.append("-k")
log.debug("Deadman: Running %s"%(cmd)) log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, shell=False) process = subprocess.Popen(cmd, shell=False)
return process return process
@ -111,6 +126,7 @@ class DeadMan():
Start the deadman process on the server Start the deadman process on the server
""" """
if self._is_enabled:
self._deadman_process = self._start_deadman_process() self._deadman_process = self._start_deadman_process()
log.debug("Deadman: Process is starting") log.debug("Deadman: Process is starting")
@ -137,7 +153,7 @@ class DeadMan():
now = time.time() now = time.time()
with open(self._heartbeat_file, 'w') as heartbeat_file: with open(self._heartbeat_file, 'w') as heartbeat_file:
heartbeat_file.write(now) heartbeat_file.write(str(now))
heartbeat_file.close() heartbeat_file.close()
log.debug("Deadman: heartbeat_file updated: %s %s" % ( log.debug("Deadman: heartbeat_file updated: %s %s" % (
@ -145,5 +161,4 @@ class DeadMan():
now, now,
)) ))
self.start() self.start()

View File

@ -141,37 +141,41 @@ class Server(object):
instance.start() # starts the new process 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): def run(self):
""" """
Starts the Tornado web server and ZeroMQ server. Starts the Tornado web server and ZeroMQ server.
""" """
# FIXME: debug mode!
settings = { settings = {
"debug":True, "debug":True,
"cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), "cookie_secret": base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes),
"login_url": "/login", "login_url": "/login",
"required_user" : "test123",
"required_pass" : "test456",
} }
ssl_options = {}
try:
cloud_config = Config.instance().get_section_config("CLOUD_SERVER")
cloud_settings = {
"required_user" : cloud_config['WEB_USERNAME'],
"required_pass" : cloud_config['WEB_PASSWORD'],
}
settings.update(cloud_settings)
if cloud_config["SSL_ENABLED"] == "yes":
ssl_options = {
"certfile" : cloud_config["SSL_CRT"],
"keyfile" : cloud_config["SSL_KEY"],
}
log.info("Certs found - starting in SSL mode")
except KeyError:
log.info("Missing cloud.conf - disabling HTTP auth and SSL")
router = self._create_zmq_router() router = self._create_zmq_router()
# Add our JSON-RPC Websocket handler to Tornado # Add our JSON-RPC Websocket handler to Tornado
self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))]) self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))])
@ -191,11 +195,8 @@ class Server(object):
zmq.zmq_version())) zmq.zmq_version()))
kwargs = {"address": self._host} kwargs = {"address": self._host}
ssl_options = self._get_cert_info()
if ssl_options: if ssl_options:
log.info("Certs found - starting in SSL mode") kwargs["ssl_options"] = ssl_options
kwargs['ssl_options'] = ssl_options
if parse_version(tornado.version) >= parse_version("3.1"): if parse_version(tornado.version) >= parse_version("3.1"):
kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit kwargs["max_buffer_size"] = 524288000 # 500 MB file upload limit

242
gns3server/start_server.py Normal file
View File

@ -0,0 +1,242 @@
# -*- 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 <http://www.gnu.org/licenses/>.
# __version__ is a human-readable version number.
# __version_info__ is a four-tuple for programmatic comparison. The first
# three numbers are the components of the version number. The fourth
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
"""
Startup script for GNS3 Server Cloud Instance
"""
import os
import sys
import configparser
import getopt
import datetime
import signal
from logging.handlers import *
from os.path import expanduser
from gns3server.config import Config
import ast
import subprocess
import uuid
SCRIPT_NAME = os.path.basename(__file__)
#Is the full path when used as an import
SCRIPT_PATH = os.path.dirname(__file__)
if not SCRIPT_PATH:
SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(
sys.argv[0])))
LOG_NAME = "gns3-startup"
log = None
usage = """
USAGE: %s
Options:
-d, --debug Enable debugging
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--data Python dict of data to be written to the config file:
" { 'gns3' : 'Is AWESOME' } "
""" % (SCRIPT_NAME)
# Parse cmd line options
def parse_cmd_line(argv):
"""
Parse command line arguments
argv: Pass in cmd line arguments
"""
short_args = "dvh"
long_args = ("debug",
"verbose",
"help",
"data=",
)
try:
opts, extra_opts = getopt.getopt(argv[1:], short_args, long_args)
except getopt.GetoptError as e:
print("Unrecognized command line option or missing required argument: %s" %(e))
print(usage)
sys.exit(2)
cmd_line_option_list = {}
cmd_line_option_list["debug"] = False
cmd_line_option_list["verbose"] = True
cmd_line_option_list["data"] = None
if sys.platform == "linux":
cmd_line_option_list['syslog'] = "/dev/log"
elif sys.platform == "osx":
cmd_line_option_list['syslog'] = "/var/run/syslog"
else:
cmd_line_option_list['syslog'] = ('localhost',514)
for opt, val in opts:
if (opt in ("-h", "--help")):
print(usage)
sys.exit(0)
elif (opt in ("-d", "--debug")):
cmd_line_option_list["debug"] = True
elif (opt in ("-v", "--verbose")):
cmd_line_option_list["verbose"] = True
elif (opt in ("--data")):
cmd_line_option_list["data"] = ast.literal_eval(val)
return cmd_line_option_list
def set_logging(cmd_options):
"""
Setup logging and format output for console and syslog
Syslog is using the KERN facility
"""
log = logging.getLogger("%s" % (LOG_NAME))
log_level = logging.INFO
log_level_console = logging.WARNING
if cmd_options['verbose'] == True:
log_level_console = logging.INFO
if cmd_options['debug'] == True:
log_level_console = logging.DEBUG
log_level = logging.DEBUG
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
sys_formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level_console)
console_log.setFormatter(formatter)
syslog_hndlr = SysLogHandler(
address=cmd_options['syslog'],
facility=SysLogHandler.LOG_KERN
)
syslog_hndlr.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_hndlr)
return log
def _generate_certs():
cmd = []
cmd.append("%s/cert_utils/create_cert.sh" % (SCRIPT_PATH))
log.debug("Generating certs ...")
output_raw = subprocess.check_output(cmd, shell=False,
stderr=subprocess.STDOUT)
output_str = output_raw.decode("utf-8")
output = output_str.strip().split("\n")
log.debug(output)
return (output[-2], output[-1])
def _start_gns3server():
cmd = []
cmd.append("gns3server")
log.info("Starting gns3server ...")
subprocess.Popen(cmd, shell=False)
def main():
global log
options = parse_cmd_line(sys.argv)
log = set_logging(options)
def _shutdown(signalnum=None, frame=None):
"""
Handles the SIGINT and SIGTERM event, inside of main so it has access to
the log vars.
"""
log.info("Received shutdown signal")
sys.exit(0)
# Setup signal to catch Control-C / SIGINT and SIGTERM
signal.signal(signal.SIGINT, _shutdown)
signal.signal(signal.SIGTERM, _shutdown)
client_data = {}
config = Config.instance()
cfg = config.list_cloud_config_file()
cfg_path = os.path.dirname(cfg)
try:
os.makedirs(cfg_path)
except FileExistsError:
pass
(server_key, server_crt) = _generate_certs()
cloud_config = configparser.ConfigParser()
cloud_config['CLOUD_SERVER'] = {}
if options['data']:
cloud_config['CLOUD_SERVER'] = options['data']
cloud_config['CLOUD_SERVER']['SSL_KEY'] = server_key
cloud_config['CLOUD_SERVER']['SSL_CRT'] = server_crt
cloud_config['CLOUD_SERVER']['SSL_ENABLED'] = 'yes'
cloud_config['CLOUD_SERVER']['WEB_USERNAME'] = str(uuid.uuid4()).upper()[0:8]
cloud_config['CLOUD_SERVER']['WEB_PASSWORD'] = str(uuid.uuid4()).upper()[0:8]
with open(cfg, 'w') as cloud_config_file:
cloud_config.write(cloud_config_file)
cloud_config_file.close()
_start_gns3server()
with open(server_crt, 'r') as cert_file:
cert_data = cert_file.readlines()
cert_file.close()
client_data['SSL_CRT_FILE'] = server_crt
client_data['SSL_CRT'] = cert_data
client_data['WEB_USERNAME'] = cloud_config['CLOUD_SERVER']['WEB_USERNAME']
client_data['WEB_PASSWORD'] = cloud_config['CLOUD_SERVER']['WEB_PASSWORD']
print(client_data)
if __name__ == "__main__":
result = main()
sys.exit(result)

View File

@ -5,4 +5,5 @@ jsonschema
pycurl pycurl
python-dateutil python-dateutil
apache-libcloud apache-libcloud
requests