#!/usr/bin/env python """ Hosted GUI client startup script, for workshops, etc. As of when this is run, we assume that the tarball (contents TBD and perhaps changing from one workshop to another) have been unpacked, that we are on some Unix-like machine, and that we are executing in a Python interpreter. We have to check anything else we care about. In what we hope is the most common case, this script should be run with no options. The two existing options, for special cases, are: --initialize_only Stop after generating identity.xml --bypass_setup Do not generate identity.xml or attempt entitydb setup operations Both of these are intended to support special cases where the default mode of operation is not sufficient, and some manual setup of the entitydb relationships is required. In such cases, the expected usage would be to run this with -initialize_only, use the myrpki.py tool to set do the entitydb setup, then run this again with --bypass_setup. $Id$ Copyright (C) 2010 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. """ # Check Python version before doing anything else import sys python_version = sys.version_info[:2] have_ssl_module = python_version == (2, 6) if python_version == (2, 5): print """ WARNING WARNING WARNING You are running Python version 2.5, which does not include real SSL support. This means that sessions created by this script will be vulnerable to monkey-in-the-middle attacks. Python 2.6 does not have this problem. """ while True: answer = raw_input("Are you SURE you want to proceed? (yes/NO) ").strip().lower() if answer in ("", "n", "no"): sys.exit("You have chosen wisely") elif answer in ("y", "yes"): print "You have been warned" break else: print 'Please answer "yes" or "no"' elif python_version == (2, 6): try: import ssl except ImportError: sys.exit("You're running Python 2.6, but I can't find the ssl module, so you have no SSL support at all, argh!") else: sys.exit("Sorry, this script requires Python 2.6, I seem to be running in %s" % sys.version) # Ok, it's safe to import the other stuff we need now import os, subprocess, webbrowser, urllib2, getpass, re, errno, time, email.utils, httplib, socket, getopt from xml.etree.ElementTree import fromstring as ElementFromString def save(filename, data): """ Save data to a file. """ tempname = "%s.%d.tmp" % (filename, os.getpid()) f = open(tempname, "w") f.write(data) f.close() os.rename(tempname, filename) class CSV_File(object): """ A CSV file that's being maintained by the GUI but being monitored, downloaded, and used here. """ def __init__(self, filename, url): self.filename = filename self.url = url try: self.timestamp = os.stat(filename).st_mtime except: self.store(0, "") def last_modified(self): """ Return CSV file timestamp formatted for use with HTTP. """ return email.utils.formatdate(self.timestamp, False, True) def store(self, timestamp, data): """ Save CSV file, and record new timestamp. """ save(self.filename, data) self.timestamp = timestamp os.utime(self.filename, (time.time(), timestamp)) class AbstractHTTPSConnection(httplib.HTTPSConnection): """ Customization of httplib.HTTPSConnection to enable certificate validation. This is an abstract class; subclass must set trust_anchor to the filename of a anchor file in the format that the ssl module expects. """ trust_anchor = None def connect(self): assert self.trust_anchor is not None sock = socket.create_connection((self.host, self.port), self.timeout) if getattr(self, "_tunnel_host", None): self.sock = sock self._tunnel() self.sock = ssl.wrap_socket(sock, keyfile = self.key_file, certfile = self.cert_file, cert_reqs = ssl.CERT_REQUIRED, ssl_version = ssl.PROTOCOL_TLSv1, ca_certs = self.trust_anchor) class main(object): """ Main program. """ # Environmental parameters top = os.path.realpath(os.path.join((sys.path[0] or "."), "..")) cwd = os.getcwd() # Parameters that we might want to get from a config file. # Just wire them all in for the moment. base_url = "https://demo.rpki.net/" auth_url = base_url + 'accounts/login/' myrpki_url = base_url + 'myrpki/' example_myrpki_cfg = "%s/rpkid/examples/myrpki.conf" % top working_dir = "%s/rpkidemo-data" % cwd myrpki_py = "%s/rpkid/myrpki.py" % top user_agent = "RPKIDemo" delay = 15 realm = "myrpki" use_cookies = True trust_anchor = "%s/scripts/rpkidemo.pem" % top openssl = None def setup_openssl(self): """ Find a usable version of OpenSSL, or build one if we must. """ def scrape(*args): return subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.STDOUT).communicate()[0] def usable_openssl(f): return f is not None and os.path.exists(f) and "-ss_cert" in scrape(f, "ca", "-?") and "Usage cms" in scrape(f, "cms", "-?") for d in os.environ["PATH"].split(":"): f = os.path.join(d, "openssl") if usable_openssl(f): self.openssl = f break if self.openssl is None: print "Couldn't find usable openssl on path, attempting to build one" subprocess.check_call(("./configure",), cwd = self.top) subprocess.check_call(("make",), cwd = os.path.join(self.top, "openssl")) self.openssl = os.path.join(self.top, "openssl", "openssl", "apps", "openssl") print "Done building openssl" print if usable_openssl(self.openssl): print "Using", self.openssl else: sys.exit("Could not find or build usable version of openssl, giving up") @staticmethod def setup_utc(): """ This script thinks in UTC. """ os.environ["TZ"] = "UTC" time.tzset() def setup_username(self): """ Get username and password for web interface, construct urllib2 "opener" tailored for our use, perform an initial GET (ignoring result, other than exceptions) to test the username and password. """ print "I need to know your username and password on the Django GUI server to proceed" while True: try: self.username = raw_input("Username: ") self.password = getpass.getpass() auth_handler = urllib2.HTTPDigestAuthHandler() auth_handler.add_password( realm = self.realm, uri = self.base_url, user = self.username, passwd = self.password) handlers = [auth_handler] if self.use_cookies: handlers.append(urllib2.HTTPCookieProcessor) if have_ssl_module: class HTTPSConnection(AbstractHTTPSConnection): trust_anchor = self.trust_anchor class HTTPSHandler(urllib2.HTTPSHandler): def https_open(self, req): return self.do_open(HTTPSConnection, req) handlers.append(HTTPSHandler) self.opener = urllib2.build_opener(*handlers) self.opener.open(self.auth_url) # Test login credentials return except urllib2.URLError, e: print "Could not log in to server: %s" % e print "Please try again" def setup_working_directory(self): """ Create working directory and move to it. """ try: print "Creating", self.working_dir os.mkdir(self.working_dir) except OSError, e: if e.errno != errno.EEXIST: raise print self.working_dir, "already exists, reusing it" os.chdir(self.working_dir) def setup_config_file(self): """ Generate myrpki.conf """ if os.path.exists("myrpki.conf"): print "You already have a myrpki.conf file, so I will use it" return print "Generating myrpki.conf" section_regexp = re.compile("\s*\[\s*(.+?)\s*\]\s*$") variable_regexp = re.compile("\s*([-a-zA-Z0-9_]+)\s*=\s*(.+?)\s*$") f = open("myrpki.conf", "w") f.write("# Automatically generated, do not edit\n") section = None for line in open(self.example_myrpki_cfg): m = section_regexp.match(line) if m: section = m.group(1) m = variable_regexp.match(line) option = m.group(1) if m and section == "myrpki" else None value = m.group(2) if option else None if option == "handle": line = "handle = %s\n" % self.username if option == "openssl": line = "openssl = %s\n" % self.openssl if option in ("run_rpkid", "run_pubd", "run_rootd") and value != "false": line = "%s = false\n" % option f.write(line) f.close() def myrpki(self, *cmd): """ Run a myrpki command. """ return subprocess.check_call((sys.executable, self.myrpki_py) + cmd) def upload(self, url, filename, content_type = "Application/XML"): """ Upload filename to URL, return result. """ url = "%s%s/%s" % (self.myrpki_url, url, self.username) data = open(filename).read() print "Uploading", filename, "to", url try: return self.opener.open(urllib2.Request(url, data, { "Content-Type" : content_type, "User-Agent" : self.user_agent })) except urllib2.HTTPError: sys.stderr.write("Problem uploading to URL %s\n" % url) raise def update(self): """ Run configure_resources, upload result, download updated result. """ self.myrpki("configure_resources") r = self.upload("upload-myrpki-xml", "myrpki.xml") save("myrpki.xml", r.read()) def setup_csv_files(self): """ Create CSV file objects and synchronize timestamps. """ self.csv_files = [ CSV_File("asns.csv", "demo/down/asns/%s" % self.username), CSV_File("prefixes.csv", "demo/down/prefixes/%s" % self.username), CSV_File("roas.csv", "demo/down/roas/%s" % self.username) ] def poll(self, csv_file): """ Poll for new version of a CSV file, save if changed, return boolean indicating whether file has changed. """ try: url = self.myrpki_url + csv_file.url r = self.opener.open(urllib2.Request(url, None, { "If-Modified-Since" : csv_file.last_modified(), "User-Agent" : self.user_agent })) timestamp = time.mktime(r.info().getdate("Last-Modified")) csv_file.store(timestamp, r.read()) return True except urllib2.HTTPError, e: if e.code == 304: # 304 == "Not Modified" return False else: sys.stderr.write("Problem polling URL %s\n" % url) raise def poll_loop(self): """ Loop forever, polling for updates. """ while True: changed = False for csv_file in self.csv_files: if self.poll(csv_file): changed = True if changed: self.update() time.sleep(self.delay) def getopt(self): """ Parse options. """ self.bypass_setup = False self.initialize_only = False opts, argv = getopt.getopt(sys.argv[1:], "bhi?", ["bypass_setup", "help", "initialize_only"]) for o, a in opts: if o in ("-h", "--help", "-?"): print __doc__ sys.exit(0) elif o in ("-i", "--initialize_only"): self.initialize_only = True elif o in ("-b", "--bypass_setup"): self.bypass_setup = True if argv: sys.exit("Unexpected arguments %r" % (argv,)) def __init__(self): self.getopt() self.setup_utc() self.setup_openssl() self.setup_username() self.setup_working_directory() self.setup_config_file() self.setup_csv_files() if not self.bypass_setup: self.myrpki("initialize") if self.initialize_only: print "You asked me to stop after initialization, so I am doing so" sys.exit(0) if not self.bypass_setup: r = self.upload("upload-parent-request", "entitydb/identity.xml") parent_data = r.read() save("parent.xml", parent_data) self.myrpki("configure_parent", "parent.xml") parent_handle = ElementFromString(parent_data).get("parent_handle") r = self.upload("upload-repository-request", "entitydb/repositories/%s.xml" % parent_handle) save("repository.xml", r.read()) self.myrpki("configure_repository", "repository.xml") self.update() self.update() webbrowser.open(self.myrpki_url) self.poll_loop() main()