#!/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. $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 have_ssl_module: 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, urllib, cookielib import tempfile 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) def save_error(err): """ Save the data from the file-like object "f" into a temporary file and open a web browser to view the result. """ with tempfile.NamedTemporaryFile(prefix = "rpkidemo-error", suffix = ".html", delete = False) as tmpf: tmpf.write(err.read()) # Save filename for use outside the with statement. This ensures # the file is properly flushed prior to invoking the web browser. fname = tmpf.name sys.stderr.write("errors saved in %s\n" % fname) webbrowser.open("file://" + fname) 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/" myrpki_url = base_url + "rpki/" auth_url = myrpki_url + "demo/login" example_myrpki_cfg = "%s/rpkid/examples/rpki.conf" % top working_dir = "%s/rpkidemo-data" % cwd myrpki_py = "%s/rpkid/myrpki.py" % top user_agent = "RPKIDemo" delay = 15 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((".
# $Id$

"""HTTPS utilities, both client and server.

At the moment this only knows how to use the PEM certs in my
subversion repository; generalizing it would not be hard, but the more
general version should use SQL anyway.
"""

import httplib, BaseHTTPServer, tlslite.api, glob, rpki.x509, traceback, rpki.exceptions

rpki_content_type = "application/x-rpki"

def client(msg, privateKey, certChain, x509TrustList, host="localhost", port=4433, url="/"):
  """Open client HTTPS connection, send a message, wait for response.

  This function wraps most of what one needs to do to send a message
  over HTTPS and get a response.  The certificate checking isn't quite
  up to snuff; it's better than with the other packages I've found,
  but doesn't appear to handle subjectAltName extensions (sigh).
  """
  
  httpc = tlslite.api.HTTPTLSConnection(host=host,
                                        port=port,
                                        privateKey=privateKey.get_tlslite(),
                                        certChain=certChain.tlslite_certChain(),
                                        x509TrustList=x509TrustList.tlslite_trustList())
  httpc.connect()
  httpc.request("POST", url, msg, {"Content-Type" : rpki_content_type})
  response = httpc.getresponse()
  if response.status == httplib.OK:
    return response.read()
  else:
    r = response.read()
    raise rpki.exceptions.HTTPRequestFailed, "HTTP request failed with status %s, response %s" % (response.status, r)

class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  """Derived type to supply POST handler."""

  rpki_handlers = None                  # Subclass must bind

  def rpki_find_handler(self):
    for s,h in self.rpki_handlers:
      if self.path.startswith(s):
        return h
    return None

  def do_POST(self):
    """POST handler."""
    try:
      handler = self.rpki_find_handler()
      if self.headers["Content-Type"] != rpki_content_type:
        rcode, rtext = 415, "Received Content-Type %s, expected %s" % (self.headers["Content-Type"], rpki_content_type)
      elif handler is None:
        rcode, rtext = 404, "No handler found for URL " + self.path
      else:
        rcode, rtext = handler(query=self.rfile.read(int(self.headers["Content-Length"])), path=self.path)
    except Exception, edata:
      traceback.print_exc()
      rcode, rtext = 500, "Unhandled exception %s" % edata
    self.send_response(rcode)
    self.send_header("Content-Type", rpki_content_type)
    self.end_headers()
    self.wfile.write(rtext)

class httpServer(tlslite.api.TLSSocketServerMixIn, BaseHTTPServer.HTTPServer):
  """Derived type to handle TLS aspects of HTTPS."""

  rpki_certChain = None
  rpki_privateKey = None
  rpki_sessionCache = None
  
  def handshake(self, tlsConnection):
    """TLS handshake handler."""
    assert self.rpki_certChain is not None
    assert self.rpki_privateKey is not None
    assert self.rpki_sessionCache is not None
    try:
      tlsConnection.handshakeServer(certChain=self.rpki_certChain,
                                    privateKey=self.rpki_privateKey,
                                    sessionCache=self.rpki_sessionCache)
      tlsConnection.ignoreAbruptClose = True
      return True
    except tlslite.api.TLSError, error:
      print "TLS handshake failure:", str(error)
      return False

def server(handlers, privateKey, certChain, port=4433, host=""):
  """Run an HTTPS server and wait (forever) for connections."""

  if not isinstance(handlers, (tuple, list)):
    handlers = (("/", handlers),)

  class boundRequestHandler(requestHandler):
    rpki_handlers = handlers

  httpd = httpServer((host, port), boundRequestHandler)

  httpd.rpki_privateKey = privateKey.get_tlslite()
  httpd.rpki_certChain = certChain.tlslite_certChain()
  httpd.rpki_sessionCache = tlslite.api.SessionCache()

  httpd.serve_forever()