#!/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 re import sys import socket import argparse import subprocess import rpki.autoconf fqdn = socket.getfqdn() vhost = '''\ 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 Order deny,allow Allow from all # # Defines the URL to the portal-gui # WSGIScriptAlias / %(datarootdir)s/rpki/wsgi/rpki.wsgi Order deny,allow Allow from all Alias /media/ %(datarootdir)s/rpki/media/ Alias /site_media/ %(datarootdir)s/rpki/media/ Order deny,allow Allow from all # Leave the trailing slash off the URL, otherwise /rcynic is swallowed by the # WSGIScriptAlias Alias /rcynic %(RCYNIC_HTML_DIR)s/ # Redirect to the dashboard when someone hits the bare vhost RedirectMatch ^/$ /rpki/ # Enable HTTPS SSLEngine on SSLCertificateFile %(sysconfdir)s/rpki/apache.cer SSLCertificateKeyFile %(sysconfdir)s/rpki/apache.key # 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 ''' % dict(rpki.autoconf.__dict__, fqdn = fqdn) def Guess(args): """ Guess what platform this is and dispatch to platform constructor. """ import platform 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 in ("debian", "ubuntu"): return Debian(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 = None 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) def install(self): with open(self.apache_conf_sample, "w") as f: self.log("Writing %s" % f.name) if self.apache_conf_preface is not None: f.write(self.apache_conf_preface) f.write(vhost) if not os.path.exists(self.apache_conf): self.unlink(self.apache_conf) self.log("Linking %s to %s" % ( self.apache_conf, self.apache_conf_sample)) os.link(self.apache_conf_sample, self.apache_conf) 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() 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: try: self._apache_name = "apache%s%s" % re.search("^Server version: Apache/(\\d+)\\.(\\d+)", subprocess.check_output(("httpd", "-V"))).groups() except: raise RuntimeError("Couldn't deduce Apache version number") return self._apache_name @property def apache_conf_target(self): return "/usr/local/etc/%s/Includes/rpki.conf" % self.apache_name apache_conf_preface = '''\ Listen [::]:443 Listen 0.0.0.0:443 NameVirtualHost *:443 ''' + "\n" def restart(self): self.run("service", self.apache_name, "restart") class Debian(Platform): """ Debian and related platforms like Ubuntu. """ apache_conf_target = "/etc/apache2/sites-available/rpki" 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("a2ensite", "rpki") def disable(self): self.run("a2dissite", "rpki") def restart(self): self.run("service", "apache2", "restart") class NIY(Platform): def __init__(self, args): raise NotImplementedError("Platform %s not implemented yet, sorry" % self.__class__.__name__) class Redhat(NIY): """ Redhat family of Linux distributions (Fedora, CentOS). """ class Darwin(NIY): """ 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") group1.add_argument("--freebsd", help = "configure for FreeBSD", action = "store_const", dest = "platform", const = FreeBSD) group1.add_argument("--debian", "--ubuntu", help = "configure for Debian/Ubuntu", action = "store_const", dest = "platform", const = Debian) 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()