diff --git a/docker-compose.yml b/docker-compose.yml index 12b60f6..9989d6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,7 @@ services: # - "443:443" volumes: - layersstatic:/usr/share/nginx/html - # - layerscerts:/etc/letsencrypt + - ./docker/certs:/opt/cert container_name: layersweb layersrabbit: image: rabbitmq:alpine @@ -62,4 +62,3 @@ services: volumes: layersmeta: layersstatic: - layerscerts: diff --git a/docker/certs/README b/docker/certs/README new file mode 100644 index 0000000..12768ef --- /dev/null +++ b/docker/certs/README @@ -0,0 +1,7 @@ +This directory will be mounted as a volume in the Docker container setup to +contain SSL certificates for the web server. + +If you run dockersetup.py and specify a certificate with --cert and +corresponding key with --key then they will be copied here; alternatively +let the setup script generate a self-signed certificate and key and they +will be written here as well. diff --git a/dockersetup.py b/dockersetup.py index ba1c7c8..b6b3dc6 100755 --- a/dockersetup.py +++ b/dockersetup.py @@ -18,21 +18,28 @@ # It will build and run these containers and set up the database. import sys +import os import argparse import re import subprocess import time import random +import shutil def get_args(): - parser = argparse.ArgumentParser( - description='Script sets up the Layer Index tool with Docker Containers.') + parser = argparse.ArgumentParser(description='Script sets up the Layer Index tool with Docker Containers.') + parser.add_argument('-o', '--hostname', type=str, help='Hostname of your machine. Defaults to localhost if not set.', required=False, default = "localhost") parser.add_argument('-p', '--http-proxy', type=str, help='http proxy in the format http://', required=False) parser.add_argument('-s', '--https-proxy', type=str, help='https proxy in the format http://', required=False) parser.add_argument('-d', '--databasefile', type=str, help='Location of your database file to import. Must be a .sql file.', required=False) - parser.add_argument('-m', '--portmapping', type=str, help='Port mapping in the format HOST:CONTAINER. Default is %(default)s', required=False, default='8080:80') + parser.add_argument('-m', '--portmapping', type=str, help='Port mapping in the format HOST:CONTAINER. Default is %(default)s', required=False, default='8080:80,8081:443') + parser.add_argument('--no-https', action="store_true", default=False, help='Disable HTTPS (HTTP only) for web server') + parser.add_argument('--cert', type=str, help='Existing SSL certificate to use for HTTPS web serving', required=False) + parser.add_argument('--cert-key', type=str, help='Existing SSL certificate key to use for HTTPS web serving', required=False) + args = parser.parse_args() + port = proxymod = "" try: if args.http_proxy: @@ -42,10 +49,26 @@ def get_args(): except IndexError: raise argparse.ArgumentTypeError("http_proxy must be in format http://") + for entry in args.portmapping.split(','): + if len(entry.split(":")) != 2: + raise argparse.ArgumentTypeError("Port mapping must in the format HOST:CONTAINER. Ex: 8080:80. Multiple mappings should be separated by commas.") - if len(args.portmapping.split(":")) != 2: - raise argparse.ArgumentTypeError("Port mapping must in the format HOST:CONTAINER. Ex: 8080:80") - return args.hostname, args.http_proxy, args.https_proxy, args.databasefile, port, proxymod, args.portmapping + if args.no_https: + if args.cert or args.cert_key: + raise argparse.ArgumentTypeError("--no-https and --cert/--cert-key options are mutually exclusive") + if args.cert and not os.path.exists(args.cert): + raise argparse.ArgumentTypeError("Specified certificate file %s does not exist" % args.cert) + if args.cert_key and not os.path.exists(args.cert_key): + raise argparse.ArgumentTypeError("Specified certificate key file %s does not exist" % args.cert_key) + if args.cert_key and not args.cert: + raise argparse.ArgumentTypeError("Certificate key file specified but not certificate") + cert_key = args.cert_key + if args.cert and not cert_key: + cert_key = os.path.splitext(args.cert)[0] + '.key' + if not os.path.exists(cert_key): + raise argparse.ArgumentTypeError("Could not find certificate key, please use --cert-key to specify it") + + return args.hostname, args.http_proxy, args.https_proxy, args.databasefile, port, proxymod, args.portmapping, args.no_https, args.cert, cert_key # Edit http_proxy and https_proxy in Dockerfile def edit_dockerfile(http_proxy, https_proxy): @@ -86,15 +109,24 @@ def edit_dockercompose(hostname, dbpassword, secretkey, portmapping): filedata= readfile("docker-compose.yml") in_layersweb = False in_layersweb_ports = False + in_layersweb_ports_format = None newlines = [] lines = filedata.splitlines() for line in lines: if in_layersweb_ports: format = line[0:line.find("-")].replace("#", "") - newlines.append(format + '- "' + portmapping + '"' + "\n") - in_layersweb_ports = False - in_layersweb = False - elif "layersweb:" in line: + if in_layersweb_ports_format: + if format != in_layersweb_ports_format: + in_layersweb_ports = False + in_layersweb = False + else: + continue + else: + in_layersweb_ports_format = format + for portmap in portmapping.split(','): + newlines.append(format + '- "' + portmap + '"' + "\n") + continue + if "layersweb:" in line: in_layersweb = True newlines.append(line + "\n") elif "hostname:" in line: @@ -117,6 +149,46 @@ def edit_dockercompose(hostname, dbpassword, secretkey, portmapping): newlines.append(line + "\n") writefile("docker-compose.yml", ''.join(newlines)) + +def edit_nginx_ssl_conf(hostname, https_port, certdir, certfile, keyfile): + filedata = readfile('docker/nginx-ssl.conf') + newlines = [] + lines = filedata.splitlines() + for line in lines: + if 'ssl_certificate ' in line: + format = line[0:line.find('ssl_certificate')] + newlines.append(format + 'ssl_certificate ' + os.path.join(certdir, certfile) + ';\n') + elif 'ssl_certificate_key ' in line: + format = line[0:line.find('ssl_certificate_key')] + newlines.append(format + 'ssl_certificate_key ' + os.path.join(certdir, keyfile) + ';\n') + # Add a line for the dhparam file + newlines.append(format + 'ssl_dhparam ' + os.path.join(certdir, 'dhparam.pem') + ';\n') + elif 'https://layers.openembedded.org' in line: + line = line.replace('https://layers.openembedded.org', 'https://%s:%s' % (hostname, https_port)) + newlines.append(line + "\n") + else: + line = line.replace('layers.openembedded.org', hostname) + newlines.append(line + "\n") + + # Write to a different file so we can still replace the hostname next time + writefile("docker/nginx-ssl-edited.conf", ''.join(newlines)) + + +def edit_dockerfile_web(hostname, no_https): + filedata = readfile('Dockerfile.web') + newlines = [] + lines = filedata.splitlines() + for line in lines: + if line.startswith('COPY ') and line.endswith('/etc/nginx/nginx.conf'): + if no_https: + srcfile = 'docker/nginx.conf' + else: + srcfile = 'docker/nginx-ssl-edited.conf' + line = 'COPY %s /etc/nginx/nginx.conf' % srcfile + newlines.append(line + "\n") + writefile("Dockerfile.web", ''.join(newlines)) + + def generatepasswords(passwordlength): return ''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*-_=+') for i in range(passwordlength)]) @@ -137,7 +209,22 @@ secretkey = generatepasswords(50) dbpassword = generatepasswords(10) ## Get user arguments and modify config files -hostname, http_proxy, https_proxy, dbfile, port, proxymod, portmapping = get_args() +hostname, http_proxy, https_proxy, dbfile, port, proxymod, portmapping, no_https, cert, cert_key = get_args() + +https_port = None +http_port = None +for portmap in portmapping.split(','): + outport, inport = portmap.split(':', 1) + if inport == '443': + https_port = outport + elif inport == '80': + http_port = outport +if (not https_port) and (not no_https): + print("No HTTPS port mapping (to port 443 inside the container) was specified and --no-https was not specified") + sys.exit(1) +if not (http_port or https_port): + print("Port mapping must include a mapping to port 80 or 443 inside the container (or both)") + sys.exit(1) if http_proxy: edit_gitproxy(proxymod, port) @@ -146,6 +233,33 @@ if http_proxy or https_proxy: edit_dockercompose(hostname, dbpassword, secretkey, portmapping) +edit_dockerfile_web(hostname, no_https) + +if not no_https: + local_cert_dir = os.path.abspath('docker/certs') + if cert: + if os.path.abspath(os.path.dirname(cert)) != local_cert_dir: + shutil.copy(cert, local_cert_dir) + certfile = os.path.basename(cert) + if os.path.abspath(os.path.dirname(cert_key)) != local_cert_dir: + shutil.copy(cert_key, local_cert_dir) + keyfile = os.path.basename(cert_key) + else: + print('') + print('Generating self-signed SSL certificate. Please specify your hostname (%s) when prompted for the Common Name.' % hostname) + certfile = 'setup-selfsigned.crt' + keyfile = 'setup-selfsigned.key' + return_code = subprocess.call('openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout %s -out %s' % (os.path.join(local_cert_dir, keyfile), os.path.join(local_cert_dir, certfile)), shell=True) + if return_code != 0: + print("Self-signed certificate generation failed") + sys.exit(1) + return_code = subprocess.call('openssl dhparam -out %s 2048' % os.path.join(local_cert_dir, 'dhparam.pem'), shell=True) + if return_code != 0: + print("DH group generation failed") + sys.exit(1) + + edit_nginx_ssl_conf(hostname, https_port, '/opt/cert', certfile, keyfile) + ## Start up containers return_code = subprocess.call("docker-compose up -d", shell=True) if return_code != 0: @@ -190,9 +304,10 @@ if return_code != 0: sys.exit(1) print("") -ports = portmapping.split(':') -if ports[1] == '443': +if https_port and not no_https: protocol = 'https' + port = https_port else: protocol = 'http' -print("The application should now be accessible at %s://%s:%s" % (protocol, hostname, ports[0])) + port = http_port +print("The application should now be accessible at %s://%s:%s" % (protocol, hostname, port))