#!/usr/bin/env python
"""Register with a facilitator through Google App Engine."""

import getopt
import httplib
import os
import socket
import subprocess
import sys
import urlparse
import urllib2

from flashproxy.keys import PIN_GOOGLE_CA_CERT, PIN_GOOGLE_PUBKEY_SHA1, check_certificate_pin, temp_cert
from flashproxy.util import parse_addr_spec, format_addr

try:
    from M2Crypto import SSL
except ImportError:
    # Defer the error reporting so that --help works even without M2Crypto.
    SSL = None

DEFAULT_REMOTE_ADDRESS = ""
DEFAULT_REMOTE_PORT = 9000
DEFAULT_TRANSPORT = "websocket"

# The domain to which requests appear to go.
FRONT_DOMAIN = "www.google.com"
# The value of the Host header within requests.
TARGET_DOMAIN = "fp-reg-a.appspot.com"

FLASHPROXY_REG_URL = "flashproxy-reg-url"

class options(object):
    address_family = socket.AF_UNSPEC
    use_certificate_pin = True
    facilitator_pubkey_filename = None
    transport = DEFAULT_TRANSPORT
    safe_logging = True

def usage(f = sys.stdout):
    print >> f, """\
Usage: %(progname)s [REMOTE][:PORT]
Register with a flash proxy facilitator through a Google App Engine app.
By default the remote address registered is "%(remote_addr)s" (the
external IP address is guessed).

  -4                    name lookups use only IPv4.
  -6                    name lookups use only IPv6.
      --disable-pin     don't check server public key against a known pin.
      --facilitator-pubkey=FILENAME
                        encrypt registrations to the given PEM-formatted
                          public key (default built-in).
  -h, --help            show this help.
      --transport=TRANSPORT  register using the given transport
                          (default "%(transport)s").
      --unsafe-logging  don't scrub IP addresses from logs.\
""" % {
    "progname": sys.argv[0],
    "remote_addr": format_addr((DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)),
    "transport": DEFAULT_TRANSPORT,
}

def safe_str(s):
    """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
    if options.safe_logging:
        return "[scrubbed]"
    else:
        return s

def safe_format_addr(addr):
    return safe_str(format_addr(addr))

def generate_url(addr):
    if getattr(sys, "frozen", False):
        script_dir = os.path.dirname(sys.executable)
    else:
        script_dir = sys.path[0]
    if not script_dir:
        # Maybe the script was read from stdin; in any case don't guess at the directory.
        raise ValueError("Can't find executable directory for registration helpers")
    command = [os.path.join(script_dir, FLASHPROXY_REG_URL)]
    command += ["-f", urlparse.urlunparse(("https", FRONT_DOMAIN, "/", "", "", ""))]
    if options.transport is not None:
        command += ["--transport", options.transport]
    if options.facilitator_pubkey_filename is not None:
        command += ["--facilitator-pubkey", options.facilitator_pubkey_filename]
    command.append(format_addr(addr))
    p = subprocess.Popen(command, stdout=subprocess.PIPE)
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        raise ValueError("%s exited with status %d" % (FLASHPROXY_REG_URL, p.returncode))
    return stdout.strip()

# Like socket.create_connection in that it tries resolving different address
# families, but doesn't connect the socket.
def create_socket(address, timeout = None):
    host, port = address
    addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
    if not addrs:
        raise socket.error("getaddrinfo returns an empty list")
    err = None
    for addr in addrs:
        try:
            s = socket.socket(addr[0], addr[1], addr[2])
            if timeout is not None and type(timeout) == float:
                s.settimeout(timeout)
            return s
        except Exception, e:
            err = e
    raise err

# Certificate validation and pinning for urllib2. Inspired by
# http://web.archive.org/web/20110125104752/http://www.muchtooscrawled.com/2010/03/https-certificate-verification-in-python-with-urllib2/.

class PinHTTPSConnection(httplib.HTTPSConnection):
    def connect(self):
        sock = create_socket((self.host, self.port), self.timeout)
        if self._tunnel_host:
            self.sock = sock
            self._tunnel()

        ctx = SSL.Context("tlsv1")
        ctx.set_verify(SSL.verify_peer, 3)

        with temp_cert(PIN_GOOGLE_CA_CERT) as ca_filename:
            ret = ctx.load_verify_locations(ca_filename)
            assert ret == 1

        self.sock = SSL.Connection(ctx, sock)
        self.sock.connect((self.host, self.port))

        if options.use_certificate_pin:
            check_certificate_pin(self.sock, PIN_GOOGLE_PUBKEY_SHA1)

class PinHTTPSHandler(urllib2.HTTPSHandler):
    def https_open(self, req):
        return self.do_open(PinHTTPSConnection, req)

def urlopen(url):
    req = urllib2.Request(url)
    req.add_header("Host", TARGET_DOMAIN)
    opener = urllib2.build_opener(PinHTTPSHandler())
    return opener.open(req)

def get_external_ip():
    f = urlopen(urlparse.urlunparse(("https", FRONT_DOMAIN, "/ip", "", "", "")))
    try:
        return f.read().strip()
    finally:
        f.close()

opt, args = getopt.gnu_getopt(sys.argv[1:], "46h", [
    "disable-pin",
    "facilitator-pubkey=",
    "help",
    "transport=",
    "unsafe-logging",
])
for o, a in opt:
    if o == "-4":
        options.address_family = socket.AF_INET
    elif o == "-6":
        options.address_family = socket.AF_INET6
    elif o == "--disable-pin":
        options.use_certificate_pin = False
    elif o == "--facilitator-pubkey":
        options.facilitator_pubkey_filename = a
    elif o == "-h" or o == "--help":
        usage()
        sys.exit()
    elif o == "--transport":
        options.transport = a
    elif o == "--unsafe-logging":
        options.safe_logging = False

if len(args) == 0:
    remote_addr = (DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
elif len(args) == 1:
    remote_addr = parse_addr_spec(args[0], DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
else:
    usage(sys.stderr)
    sys.exit(1)

if SSL is None:
    print >> sys.stderr, """\
This program requires the M2Crypto library, which is not installed.

You can install it using one of the packages at
http://chandlerproject.org/Projects/MeTooCrypto#Downloads.

On Debian-like systems, use the command "apt-get install python-m2crypto".\
"""
    sys.exit(1)

if options.address_family != socket.AF_UNSPEC:
    getaddrinfo = socket.getaddrinfo
    def getaddrinfo_replacement(host, port, family, *args, **kwargs):
        return getaddrinfo(host, port, options.address_family, *args, **kwargs)
    socket.getaddrinfo = getaddrinfo_replacement

if not remote_addr[0]:
    try:
        ip = get_external_ip()
    except urllib2.HTTPError, e:
        print >> sys.stderr, "Status code was %d, not 200" % e.code
        sys.exit(1)
    except urllib2.URLError, e:
        print >> sys.stderr, "Failed to get external IP address: %s" % str(e.reason)
        sys.exit(1)
    except Exception, e:
        print >> sys.stderr, "Failed to get external IP address: %s" % str(e)
        sys.exit(1)
    try:
        remote_addr = parse_addr_spec(ip, *remote_addr)
    except ValueError, e:
        print >> sys.stderr, "Error parsing external IP address %s: %s" % (safe_str(repr(ip)), str(e))
        sys.exit(1)

try:
    url = generate_url(remote_addr)
except Exception, e:
    print >> sys.stderr, "Error running %s: %s" % (FLASHPROXY_REG_URL, str(e))
    sys.exit(1)

try:
    http = urlopen(url)
except urllib2.HTTPError, e:
    print >> sys.stderr, "Status code was %d, not 200" % e.code
    sys.exit(1)
except urllib2.URLError, e:
    print >> sys.stderr, "Failed to register: %s" % str(e.reason)
    sys.exit(1)
except Exception, e:
    print >> sys.stderr, "Failed to register: %s" % str(e)
    sys.exit(1)
http.close()

print "Registered \"%s\" with %s." % (safe_format_addr(remote_addr), TARGET_DOMAIN)
