1
0
mirror of https://github.com/GNS3/gns3-server synced 2024-11-28 11:18:11 +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)
passwd = uuid.uuid4().hex
instance.change_password(passwd)
# wait for the password change to be processed
sleep(POLL_SEC)
# wait for the password change to be processed. Continuing while
# a password change is processing will cause image creation to fail.
sleep(POLL_SEC*6)
env.host_string = str(instance.accessIPv4)
env.user = "root"

View File

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

View File

@ -24,7 +24,7 @@
# number has been incremented)
"""
Monitors communication with the GNS3 client via tmp file. Will terminate the instance if
Monitors communication with the GNS3 client via tmp file. Will terminate the instance if
communication is lost.
"""
@ -62,7 +62,7 @@ sys.path.append(EXTRA_LIB)
import daemon
my_daemon = None
my_daemon = None
usage = """
USAGE: %s
@ -73,14 +73,15 @@ Options:
-v, --verbose Enable verbose logging
-h, --help Display this menu :)
--cloud_api_key <api_key> Rackspace API key
--cloud_api_key <api_key> Rackspace API key
--cloud_user_name
--instance_id ID of the Rackspace instance to terminate
--deadtime How long in seconds can the communication lose exist before we
shutdown this instance.
Default:
--region Region of instance
--deadtime How long in seconds can the communication lose exist before we
shutdown this instance.
Default:
Example --deadtime=3600 (60 minutes)
--check-interval Defaults to --deadtime, used for debugging
@ -111,6 +112,7 @@ def parse_cmd_line(argv):
"cloud_user_name=",
"cloud_api_key=",
"instance_id=",
"region=",
"deadtime=",
"init-wait=",
"check-interval=",
@ -130,6 +132,7 @@ def parse_cmd_line(argv):
cmd_line_option_list["cloud_user_name"] = None
cmd_line_option_list["cloud_api_key"] = 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["check-interval"] = None
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
elif (opt in ("--instance_id")):
cmd_line_option_list["instance_id"] = val
elif (opt in ("--region")):
cmd_line_option_list["region"] = val
elif (opt in ("--deadtime")):
cmd_line_option_list["deadtime"] = int(val)
elif (opt in ("--check-interval")):
@ -200,6 +205,12 @@ def parse_cmd_line(argv):
print(usage)
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
def get_gns3secrets(cmd_line_option_list):
@ -208,19 +219,19 @@ def get_gns3secrets(cmd_line_option_list):
"""
gns3secret_paths = [
os.path.expanduser("~/"),
os.path.join(os.path.expanduser("~"), '.config', 'GNS3'),
SCRIPT_PATH,
]
config = configparser.ConfigParser()
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):
config.read(gns3secret_file)
try:
for key, value in config.items("Cloud"):
for key, value in config.items("CLOUD_SERVER"):
cmd_line_option_list[key] = value.strip()
except configparser.NoSectionError:
pass
@ -256,7 +267,7 @@ def set_logging(cmd_options):
)
syslog_hndlr.setFormatter(sys_formatter)
log.setLevel(log_level)
log.addHandler(console_log)
log.addHandler(syslog_hndlr)
@ -308,7 +319,7 @@ def monitor_loop(options):
if delta.seconds > options["deadtime"]:
log.warning("Deadtime exceeded, terminating instance ...")
#Terminate involes many layers of HTTP / API calls, lots of
#Terminate involes many layers of HTTP / API calls, lots of
#different errors types could occur here.
try:
rksp = Rackspace(options)
@ -341,8 +352,8 @@ def main():
log.info("Received shutdown signal")
options["shutdown"] = True
pid_file = "%s/.gns3ias.pid" % (expanduser("~"))
pid_file = "%s/.gns3dms.pid" % (expanduser("~"))
if options["shutdown"]:
send_shutdown(pid_file)

View File

@ -7,38 +7,38 @@ class daemon:
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile, options):
def __init__(self, pidfile, options):
self.pidfile = pidfile
self.options = options
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
@ -49,17 +49,26 @@ class daemon:
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
# write pidfile
atexit.register(self.delpid)
pid = str(os.getpid())
with open(self.pidfile,'w+') as f:
f.write(pid + '\n')
def delpid(self):
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):
"""Start the daemon."""
@ -70,13 +79,19 @@ class daemon:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
message = "pidfile {0} already exist. " + \
"Daemon already running?\n"
pid_exist = self.check_pid(pid)
if pid_exist:
message = "Already running: %s\n" % (pid)
sys.stderr.write(message)
sys.exit(1)
else:
message = "pidfile {0} already exist. " + \
"but process is dead\n"
sys.stderr.write(message.format(self.pidfile))
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
@ -90,14 +105,14 @@ class daemon:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. " + \
"Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
@ -118,6 +133,6 @@ class daemon:
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
It will be called after the process has been daemonized by
start() or restart()."""

View File

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

View File

@ -62,13 +62,13 @@ C=CA
ST=Alberta
O=GNS3
localityName=Calgary
commonName=gns3server.localdomain.com
commonName=$DOMAIN
organizationalUnitName=GNS3Server
emailAddress=gns3cert@gns3.com
"
# 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 $?
#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
fail_if_error $?
echo "${DST_DIR}${DOMAIN}.key"
echo "${DST_DIR}${DOMAIN}.crt"
cd $OLD_DIR

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ from gns3server.config import Config
import logging
log = logging.getLogger(__name__)
class DeadMan():
class DeadMan(IModule):
"""
DeadMan module.
@ -54,8 +54,18 @@ class DeadMan():
if 'heartbeat_file' in kwargs:
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.heartbeat()
self.start()
def _start_deadman_process(self):
@ -63,14 +73,19 @@ class DeadMan():
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.append("gns3dms")
cmd.append("--file %s" % (self._heartbeat_file))
cmd.append("--file")
cmd.append("%s" % (self._heartbeat_file))
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
def _stop_deadman_process(self):
@ -82,7 +97,7 @@ class DeadMan():
cmd.append("gns3dms")
cmd.append("-k")
log.debug("Deadman: Running %s"%(cmd))
log.info("Deadman: Running command: %s"%(cmd))
process = subprocess.Popen(cmd, shell=False)
return process
@ -111,8 +126,9 @@ class DeadMan():
Start the deadman process on the server
"""
self._deadman_process = self._start_deadman_process()
log.debug("Deadman: Process is starting")
if self._is_enabled:
self._deadman_process = self._start_deadman_process()
log.debug("Deadman: Process is starting")
@IModule.route("deadman.reset")
def reset(self, request=None):
@ -137,7 +153,7 @@ class DeadMan():
now = time.time()
with open(self._heartbeat_file, 'w') as heartbeat_file:
heartbeat_file.write(now)
heartbeat_file.write(str(now))
heartbeat_file.close()
log.debug("Deadman: heartbeat_file updated: %s %s" % (
@ -145,5 +161,4 @@ class DeadMan():
now,
))
self.start()

View File

@ -141,37 +141,41 @@ class Server(object):
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",
}
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()
# Add our JSON-RPC Websocket handler to Tornado
self.handlers.extend([(r"/", JSONRPCWebSocket, dict(zmq_router=router))])
@ -191,11 +195,8 @@ class Server(object):
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
kwargs["ssl_options"] = ssl_options
if parse_version(tornado.version) >= parse_version("3.1"):
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
python-dateutil
apache-libcloud
requests