#!/usr/bin/env python

import socket
import re
import sys
import SocketServer
import errno
import getopt
import os
import threading
import time

import fac
import query_onionoo
import base_server
from base_server import log
from base_server import safe_str

LISTEN_ADDRESS = "127.0.0.1"
DEFAULT_LISTEN_PORT = 9003
DEFAULT_LOG_FILENAME = "fac-onionoo.log"

ELAPSED_UPDATE_TOR_NODES_TIME = 3600


class options(base_server.options):
    listen_port = DEFAULT_LISTEN_PORT
    log_filename = DEFAULT_LOG_FILENAME
    # Default value
    onionoo_spec = "https://onionoo.torproject.org/details?type=relay&running=true"

    @staticmethod
    def set_onionoo_spec(spec):
        options.onionoo_spec = spec

def usage(f = sys.stdout):
    print >> f, """\
Usage: %(progname)s -o ONIONOO SERVER <OPTIONS>
Helper to the Flash proxy facilitator: Create a local database of Tor Exit nodes
querying periodically to an ONIONOO SERVER, and checks if a Proxy querying the
Facilitator has an IP beloning to a Tor Exit Node. Listen on 127.0.0.1 and port 
PORT (by default %(port)d).

  -d, --debug             don't daemonize, log to stdout.
  -h, --help              show this help.
  -l, --log FILENAME      write log to FILENAME (default \"%(log)s\").
  -p, --port PORT         listen on PORT (by default %(port)d).
      --pidfile FILENAME  write PID to FILENAME after daemonizing.
  -o, --onionoo ONIONOO   the ONIONOO server (HTTPS_URL) where to query about Tor Exit nodes.
      --unsafe-logging    don't scrub IP addresses from logs.\
""" % {
    "progname": sys.argv[0],
    "port": DEFAULT_LISTEN_PORT,
    "log": DEFAULT_LOG_FILENAME,
}


# The class to manage the local db with the latest Tor Exit nodes.
class TorExitNodesSet(object):
    def __init__(self):
        self.set = []
        self.cv = threading.Condition()

    def replace(self, exit_nodes):
        self.cv.acquire()
        try:
            self.set = exit_nodes
            self.cv.notify()
        finally:
            self.cv.release()

    def query(self, ip):
        self.cv.acquire()
        try:
            if (not self.set) or (len(self.set) == 0) or (not (ip in self.set)):
                return False
            else:
                return True
        finally:
            self.cv.release()

    def __len__(self):
        self.cv.acquire()
        try:
            return len(self.set)
        finally:
            self.cv.release()

# The class to implement the communication protcol with
# the TCP server answering about if an IP belongs to a
# Tor Exit node or not.
class Handler(base_server.Handler):
    def handle_command(self, command, params):
        if command == "CHECK_IP":
            return self.do_CHECK_URL(params)
        else:
            self.send_error()
            return False

    def do_CHECK_URL(self, params):
        is_exit = False
        proxy_spec = fac.param_first("FROM", params)
        res = fac.ip_ver(proxy_spec, onlyhost=True)

        if res is not None:
            proxy_ip = res[0]
            if tor_exit_nodes.query(proxy_ip):
               is_exit = True
        else:
            log(options.log_file, u" It was not possible to test the proxy IP: %s " %(safe_str(unicode(proxy_spec))))
            print >> self.wfile, fac.render_transaction("NOT_EXIT_ERR")
            return True
            
        if not is_exit:
            amount_nodes = len(tor_exit_nodes)
            if amount_nodes == 0:
                msg = "NOT_EXIT_0"
            else:
                msg="NOT_EXIT_" + str(amount_nodes)
            log(options.log_file, u"proxy %s isn't a Tor Exit node" %(safe_str(unicode(proxy_ip))))
            print >> self.wfile, fac.render_transaction(msg)
            
        else:
            log(options.log_file, u"proxy %s is a Tor Exit node" %(safe_str(unicode(proxy_ip))))
            print >> self.wfile, fac.render_transaction("IS_EXIT")
        return True

# The class to implement the multithreaded 
# TCP server that is mantaining a local db
# with the latest Tor Exit nodes, and is 
# answering about if an IP belongs to a
# Tor Exit node or not.
class Server(base_server.Server):
    pass

# It is the main function for the thread in charge of updating the local db 
# with the latest Tor Exit nodes.
def update_local_db():
    tor_nodes = None
    while True:
        try:
            tor_nodes = query_onionoo.query_onionoo_server(options.onionoo_spec,last_modified,options.log_file)
            if tor_nodes is not None:
                tor_exit_nodes.replace(tor_nodes)
            log(options.log_file, u"Finished of querying the ONIONOO server, going to sleep.")
            time.sleep(ELAPSED_UPDATE_TOR_NODES_TIME)
        except Exception:
            e = sys.exc_info()[1]
            # Here I'm not using 'safe_str' to print the exception, perhaps it is better using it?
            log(options.log_file, u"There was a problem with the thread querying the ONIONOO server: %s , but still alive" %(str(e)))

# The local db with the latest Tor Exit nodes.
tor_exit_nodes = TorExitNodesSet()

# The timestamp with the last time that was done
# a query against the ONIONOOO server.
last_modified = query_onionoo.LastModified()

def main():
    opts, args = getopt.gnu_getopt(sys.argv[1:], "dhl:p:o:",
        ["debug", "help", "log=", "port=", "pidfile=", "onionoo=", "unsafe-logging"])
    for o, a in opts:
        if o == "-d" or o == "--debug":
            options.daemonize = False
            options.log_filename = None
        elif o == "-h" or o == "--help":
            usage()
            sys.exit()
        elif o == "-l" or o == "--log":
            options.log_filename = a
        elif o == "-p" or o == "--port":
            options.listen_port = int(a)
        elif o == "--pidfile":
            options.pid_filename = a
        elif o == "-o" or o == "--onionoo":
            try:
                options.set_onionoo_spec(a)
            except socket.gaierror, e:
                print >> sys.stderr, u"Can't resolve onionoo %s: %s" % (repr(a), str(e))
                sys.exit(1)
        elif o == "--unsafe-logging":
            options.safe_logging = False

    if not options.onionoo_spec:
        print >> sys.stderr, """\
The -o option is required. It is used to query about the Tor Exit Nodes.
  -o HTTPS_URL\
    """
        sys.exit(1)

    if options.log_filename:
        options.log_file = open(options.log_filename, "a")
        # Send error tracebacks to the log.
        sys.stderr = options.log_file
    else:
        options.log_file = sys.stdout

    addrinfo = socket.getaddrinfo(LISTEN_ADDRESS, options.listen_port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]

    # Creating the server with the extra argument 'log_file'.
    server = Server(addrinfo[4], Handler, options.log_file)

    log(options.log_file, u"start on %s" % fac.format_addr(addrinfo[4]))
    log(options.log_file, u"using onionoo address %s" % options.onionoo_spec)

    if options.daemonize:
        log(options.log_file, u"daemonizing")
        pid = os.fork()
        if pid != 0:
            if options.pid_filename:
                f = open(options.pid_filename, "w")
                print >> f, pid
                f.close()
            sys.exit(0)

    # It is the thread in charge of updating the local db with the latest Tor Exit nodes.
    thread = threading.Thread(target = update_local_db)
    # It is necessary to set 'daemon' to 'True' so that the thread dies automatically
    # when the main program is terminated: 
    # See at: http://stackoverflow.com/questions/190010/daemon-threads-explanation 
    thread.daemon = True
    thread.start()

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        sys.exit(0)

if __name__ == "__main__":
    main()
