#!/usr/bin/env python
# $Id$
#
# Copyright (C) 2013 Internet Systems Consortium ("ISC")
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
import os
import sys
import socket
import urllib2
import argparse
import platform
import textwrap
import subprocess
import rpki.autoconf
fqdn = socket.getfqdn()
vhost_template = """\
#
# Stuff that should be visible with both HTTP and HTTPS is (now)
# outside the vhost block (see if this works properly...).
#
#
# Allow access to the directory where rcynic-html writes
# its output files.
#
%(allow)s
#
# Add alias pointing to rcynic-html's output files.
#
# If for some reason you need to change this, be careful to leave
# the trailing slash off the URL, otherwise /rcynic will be
# swallowed by the WSGIScriptAlias
#
Alias /rcynic %(RCYNIC_HTML_DIR)s/
#
# Allow access to the directory where pubd writes RRDP files.
#
%(allow)s
#
# Add alias pointing to pubd's RRD output files.
#
Alias /rrdp %(datarootdir)s/rpki/rrdp-publication/
#
# RRDP "notification" file needs a short expiration: this is
# a critical part of how RRDP interacts with HTTP caching.
# Timeout is per current RRDP I-D, this will need to track
# any changes as the specification evolves.
#
ExpiresActive on
ExpiresDefault "access plus 1 minute"
#
# By default, this configuration assumes that you use name-based
# virtual hosting. If that's not what you want, you may need
# to change this.
#
#
# By default, we enable an HTTPS virtual host on this machine's
# fully qualified domain name. This works for simple
# configurations, but if you're running a more complex Apache
# configuration or want to run the GUI on a different hostname,
# you may need to change this.
#
ServerName %(fqdn)s
#
# Configure the WSGI application to run as a separate process from
# the Apache daemon itself.
#
%(WSGI_DAEMON_PROCESS)s
%(WSGI_PROCESS_GROUP)s
#
# Allow access to our WSGI directory.
#
%(allow)s
#
# Define the URL to the RPKI GUI
#
WSGIScriptAlias / %(datarootdir)s/rpki/wsgi/rpki.wsgi
#
# Allow access to static content (icons, etc).
#
%(allow)s
#
# Add the aliases Django expects for static content.
#
Alias /media/ %(datarootdir)s/rpki/media/
Alias /site_media/ %(datarootdir)s/rpki/media/
#
# Redirect to the GUI dashboard when someone hits the bare vhost.
#
RedirectMatch ^/$ /rpki/
#
# Enable HTTPS
#
SSLEngine on
#
# Specify HTTPS server certificate and key files for this virtual host.
# This should suffice for simple configurations, but if you're running
# a more complex Apache configuration you may need to change or remove
# these lines.
#
SSLCertificateFile %(sysconfdir)s/rpki/apache.cer
SSLCertificateKeyFile %(sysconfdir)s/rpki/apache.key
#
# Recommended settings based on
# https://wiki.mozilla.org/Security/Server_Side_TLS
# (Currently using the Intermediate cipher suite)
#
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
SSLHonorCipherOrder on
SSLCompression off
# OCSP Stapling, only in httpd 2.3.3 and later
#SSLUseStapling on
#SSLStaplingResponderTimeout 5
#SSLStaplingReturnResponderErrors off
# On Apache 2.4+, SSLStaplingCache must be set *outside* of the VirtualHost
#SSLStaplingCache shmcb:/var/run/ocsp(128000)
# Enable this if your want HSTS (recommended)
# Header add Strict-Transport-Security "max-age=15768000"
#
# Take pity on users running Internet Exploder
#
BrowserMatch "MSIE [2-6]" ssl-unclean-shutdown nokeepalive downgrade-1.0 force-response-1.0
BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
"""
allow_22_template = '''
Order deny,allow
Allow from all\
'''
allow_24_template = '''
Require all granted\
'''
name_virtual_host_template = '''\
#
# In most cases we want to use name-based virtual hosting. If this causes
# problems with your existing Apache configuration, try commenting out this line.
#
NameVirtualHost *:443
'''
def Guess(args):
"""
Guess what platform this is and dispatch to platform constructor.
"""
system = platform.system()
if system == "FreeBSD":
return FreeBSD(args)
if system == "Darwin":
return Darwin(args)
if system == "Linux":
distro = platform.linux_distribution()[0].lower()
if distro == "debian":
return Debian(args)
if distro == "ubuntu":
return Ubuntu(args)
if distro in ("fedora", "centos"):
return Redhat(args)
raise NotImplementedError("Can't guess what platform this is, sorry")
class Platform(object):
"""
Abstract base class representing an operating system platform.
"""
apache_cer = os.path.join(rpki.autoconf.sysconfdir, "rpki", "apache.cer")
apache_key = os.path.join(rpki.autoconf.sysconfdir, "rpki", "apache.key")
apache_conf = os.path.join(rpki.autoconf.sysconfdir, "rpki", "apache.conf")
apache_conf_sample = apache_conf + ".sample"
apache_conf_preface = ""
def __init__(self, args):
self.args = args
self.log("RPKI Apache configuration: platform \"%s\", action \"%s\"" % (
self.__class__.__name__, args.action))
getattr(self, args.action)()
def log(self, msg):
if self.args.verbose:
print msg
def run(self, *cmd, **kwargs):
self.log("Running %s" % " ".join(cmd))
subprocess.check_call(cmd, **kwargs)
req_cmd = ("openssl", "req", "-new",
"-config", "/dev/stdin",
"-out", "/dev/stdout",
"-keyout", apache_key,
"-newkey", "rsa:2048")
x509_cmd = ("openssl", "x509", "-req", "-sha256",
"-signkey", apache_key,
"-in", "/dev/stdin",
"-out", apache_cer,
"-days", "3650")
req_conf = '''\
[req]
default_bits = 2048
default_md = sha256
distinguished_name = req_dn
prompt = no
encrypt_key = no
[req_dn]
CN = %s
''' % fqdn
def unlink(self, fn, silent = False):
if os.path.lexists(fn):
if not silent:
self.log("Removing %s" % fn)
os.unlink(fn)
elif not silent:
self.log("Would have removed %s if it existed" % fn)
def del_certs(self, silent = False):
self.unlink(self.apache_cer, silent)
self.unlink(self.apache_key, silent)
def add_certs(self):
if os.path.exists(self.apache_cer) and os.path.exists(self.apache_key):
return
self.del_certs()
req = subprocess.Popen(self.req_cmd,
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = open("/dev/null", "w"))
x509 = subprocess.Popen(self.x509_cmd,
stdin = req.stdout,
stderr = open("/dev/null", "w"))
req.stdin.write(self.req_conf)
req.stdin.close()
if req.wait():
raise subprocess.CalledProcessError(req.returncode, self.req_cmd)
if x509.wait():
raise subprocess.CalledProcessError(x509.returncode, self.x509_cmd)
self.log("Created %s and %s, chmoding %s" % (
self.apache_cer, self.apache_key, self.apache_key))
os.chmod(self.apache_key, 0600)
_vhost = None
@property
def vhost(self):
if self._vhost is None:
allow = allow_22_template if self.args.apache_version <= 22 else allow_24_template
self._vhost = vhost_template % dict(rpki.autoconf.__dict__, fqdn = fqdn, allow = allow)
return self._vhost
@property
def name_virtual_host(self):
return name_virtual_host_template if self.args.apache_version <= 22 else ""
@property
def too_complex(self):
return textwrap.dedent('''\
# It looks like you already have HTTPS enabled in your
# Apache configuration, which makes your configuration too
# complex for us to enable support for the RPKI GUI automatically.
#
# To enable support, take a look at %s
# and copy what you need from that file into %s,
# paying attention to the comments which mark the bits that
# you might (or might not) need to change or omit, depending
# on the details of your particular Apache configuration.
''' % (self.apache_conf_sample, self.apache_conf))
@property
def apache_conf_target(self):
raise NotImplementedError
def restart(self):
raise NotImplementedError
def install(self):
with open(self.apache_conf_sample, "w") as f:
self.log("Writing %s" % f.name)
f.write(self.apache_conf_preface)
f.write(self.name_virtual_host)
f.write(self.vhost)
if not os.path.exists(self.apache_conf):
self.unlink(self.apache_conf)
with open(self.apache_conf, "w") as f:
self.log("Writing %s" % f.name)
if self.test_url("https://%s/" % fqdn):
f.write(self.too_complex)
sys.stdout.write(self.too_complex)
else:
if not self.test_tcp("localhost", 443):
f.write(self.apache_conf_preface)
f.write(self.name_virtual_host)
f.write(self.vhost)
if not os.path.exists(self.apache_conf_target):
self.unlink(self.apache_conf_target)
self.log("Symlinking %s to %s" % (
self.apache_conf_target, self.apache_conf))
os.symlink(self.apache_conf, self.apache_conf_target)
self.add_certs()
self.enable()
self.restart()
def enable(self):
pass
def disable(self):
pass
def remove(self):
try:
same = open(self.apache_conf, "r").read() == open(self.apache_conf_sample, "r").read()
except:
same = False
self.unlink(self.apache_conf_sample)
if same:
self.unlink(self.apache_conf)
self.unlink(self.apache_conf_target)
self.disable()
self.restart()
def purge(self):
self.remove()
self.unlink(self.apache_conf)
self.del_certs()
@staticmethod
def test_url(url = "https://localhost/"):
try:
urllib2.urlopen(url).close()
except IOError:
return False
else:
return True
@staticmethod
def test_tcp(host = "localhost", port = 443, family = socket.AF_UNSPEC, proto = socket.SOCK_STREAM):
try:
addrinfo = socket.getaddrinfo(host, port, family, proto)
except socket.error:
return False
for af, socktype, proto, canon, sa in addrinfo: # pylint: disable=W0612
try:
s = socket.socket(af, socktype, proto)
s.connect(sa)
s.close()
except socket.error:
continue
else:
return True
return False
class FreeBSD(Platform):
"""
FreeBSD.
"""
# On FreeBSD we have to ask httpd what version it is before we know
# where to put files or what to call the service. In FreeBSD's makefiles,
# this value is called APACHE_VERSION, and is calculated thusly:
#
# httpd -V | sed -ne 's/^Server version: Apache\/\([0-9]\)\.\([0-9]*\).*/\1\2/p'
_apache_name = None
@property
def apache_name(self):
if self._apache_name is None:
self._apache_name = "apache%s" % self.args.apache_version
return self._apache_name
@property
def apache_conf_target(self):
return "/usr/local/etc/%s/Includes/rpki.conf" % self.apache_name
apache_conf_preface = textwrap.dedent('''\
# These directives tell Apache to listen on the HTTPS port
# and to enable name-based virtual hosting. If you already
# have HTTPS enabled elsewhere in your configuration, you may
# need to remove these.
Listen [::]:443
Listen 0.0.0.0:443
''')
def restart(self):
self.run("service", self.apache_name, "restart")
class Debian(Platform):
"""
Debian and related platforms like Ubuntu.
"""
# Pull the current version number for released code. Use
# something very large when there is no version (eg, "sid").
@property
def distribution_version(self):
v = platform.linux_distribution()[1].split(".")
if all(d.isdigit() for d in v):
return tuple(int(d) for d in v)
else:
return (99999999, 0)
# On Debian, the filename must end in .conf on Stretch and must not
# end in .conf on Wheezy. Haven't checked Jessie yet, will need to
# update this if we ever sort out the version skew mess on Jessie.
@property
def apache_conf_target(self):
if self.distribution_version < (8, 0):
return "/etc/apache2/sites-available/rpki"
else:
return "/etc/apache2/sites-available/rpki.conf"
snake_oil_cer = "/etc/ssl/certs/ssl-cert-snakeoil.pem"
snake_oil_key = "/etc/ssl/private/ssl-cert-snakeoil.key"
def add_certs(self):
if not os.path.exists(self.snake_oil_cer) or not os.path.exists(self.snake_oil_key):
return Platform.add_certs(self)
if not os.path.exists(self.apache_cer):
self.unlink(self.apache_cer)
os.symlink(self.snake_oil_cer, self.apache_cer)
if not os.path.exists(self.apache_key):
self.unlink(self.apache_key)
os.symlink(self.snake_oil_key, self.apache_key)
def enable(self):
self.run("a2enmod", "ssl")
self.run("a2enmod", "expires")
self.run("a2ensite", "rpki")
#
# In light of BREACH and CRIME attacks, mod_deflate is looking
# like a bad idea, so make sure it's off.
self.run("a2dismod", "-f", "deflate")
def disable(self):
self.run("a2dissite", "rpki")
def restart(self):
self.run("service", "apache2", "restart")
class Ubuntu(Debian):
# On Ubuntu, the filename must end in .conf on Trusty and must not
# end in .conf on Precise.
@property
def apache_conf_target(self):
if self.distribution_version < (14, 0):
return "/etc/apache2/sites-available/rpki"
else:
return "/etc/apache2/sites-available/rpki.conf"
class NIY(Platform): # pylint: disable=W0223
def __init__(self, args):
super(NIY, self).__init__(args)
raise NotImplementedError("Platform %s not implemented yet, sorry" % self.__class__.__name__)
class Redhat(NIY): # pylint: disable=W0223
"Redhat family of Linux distributions (Fedora, CentOS)."
class Darwin(NIY): # pylint: disable=W0223
"Mac OS X (aka Darwin)."
def main():
"""
Generate and (de)install configuration suitable for using Apache httpd
to drive the RPKI web interface under WSGI.
"""
parser = argparse.ArgumentParser(description = __doc__)
group1 = parser.add_mutually_exclusive_group()
group2 = parser.add_mutually_exclusive_group()
parser.add_argument("-v", "--verbose",
help = "whistle while you work", action = "store_true")
parser.add_argument("--apache-version",
help = "Apache version (default " + rpki.autoconf.APACHE_VERSION + ")",
type = int, default = rpki.autoconf.APACHE_VERSION)
group1.add_argument("--freebsd",
help = "configure for FreeBSD",
action = "store_const", dest = "platform", const = FreeBSD)
group1.add_argument("--debian",
help = "configure for Debian",
action = "store_const", dest = "platform", const = Debian)
group1.add_argument("--ubuntu",
help = "configure for Ubuntu",
action = "store_const", dest = "platform", const = Ubuntu)
group1.add_argument("--redhat", "--fedora", "--centos",
help = "configure for Redhat/Fedora/CentOS",
action = "store_const", dest = "platform", const = Redhat)
group1.add_argument("--macosx", "--darwin",
help = "configure for Mac OS X (Darwin)",
action = "store_const", dest = "platform", const = Darwin)
group1.add_argument("--guess",
help = "guess which platform configuration to use",
action = "store_const", dest = "platform", const = Guess)
group2.add_argument("-i", "--install",
help = "install configuration",
action = "store_const", dest = "action", const = "install")
group2.add_argument("-r", "--remove", "--deinstall", "--uninstall",
help = "remove configuration",
action = "store_const", dest = "action", const = "remove")
group2.add_argument("-P", "--purge",
help = "remove configuration with extreme prejudice",
action = "store_const", dest = "action", const = "purge")
parser.set_defaults(platform = Guess, action = "install")
args = parser.parse_args()
try:
args.platform(args)
except Exception, e:
sys.exit(str(e))
if __name__ == "__main__":
main()