#!/usr/bin/env python2
# -*- coding: utf-8 -*-
'''
VPProxy: HTTP/HLS Stream to HTTP Multiplexing Proxy

Based on AceProxy (https://github.com/ValdikSS/AceProxy) design
'''
import traceback
import gevent
import gevent.monkey
# Monkeypatching and all the stuff
gevent.monkey.patch_all()
# Startup delay for daemon restart
gevent.sleep(5)
import glob
import os
import signal
import sys
import logging
import psutil
import BaseHTTPServer
import SocketServer
from socket import error as SocketException
from socket import SHUT_RDWR
import urllib2
import hashlib
import vpconfig
from vpconfig import VPConfig
import vlcclient
import gc
import plugins.modules.ipaddr as ipaddr
from clientcounter import ClientCounter
from plugins.modules.PluginInterface import VPProxyPlugin
try:
    import pwd
    import grp
except ImportError:
    pass

import uuid

from apscheduler.schedulers.background import BackgroundScheduler

class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    requestlist = []

    def handle_one_request(self):
        '''
        Add request to requestlist, handle request and remove from the list
        '''
        HTTPHandler.requestlist.append(self)
        BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
        HTTPHandler.requestlist.remove(self)

    def closeConnection(self):
        '''
        Disconnecting client
        '''
        if self.clientconnected:
            self.clientconnected = False
            try:
                self.wfile.close()
                self.rfile.close()
                self.connection.shutdown(SHUT_RDWR)
            except:
                pass

    def dieWithError(self, errorcode=500):
        '''
        Close connection with error
        '''
        logging.warning("Dying with error")
        if self.clientconnected:
            self.send_error(errorcode)
            self.end_headers()
            self.closeConnection()

    def proxyReadWrite(self):
        '''
        Read video stream and send it to client
        '''
        logger = logging.getLogger('http_proxyReadWrite')
        logger.debug("Started")

        self.vlcstate = True
        self.streamstate = True

        try:
            while True:
            
                if not self.clientconnected:
                    logger.debug("Client is not connected, terminating")
                    break

                VPStuff.vlcclient.mark(self.vlcid)
                data = self.video.read(4096)
                if data and self.clientconnected:
                    self.wfile.write(data)
                else:
                    logger.warning("Video connection closed")
                    break

        except SocketException:
            # Video connection dropped
            logger.warning("Video connection dropped")
        finally:
            self.video.close()
            self.closeConnection()

    def hangDetector(self):
        '''
        Detect client disconnection while in the middle of something
        or just normal connection close.
        '''
        logger = logging.getLogger('http_hangDetector')
        try:
            while True:
                if not self.rfile.read():
                    break
        except:
            pass
        finally:
            self.clientconnected = False
            logger.debug("Client disconnected")
            try:
                self.requestgreenlet.kill()
            except:
                pass
            finally:
                gevent.sleep()
            return

    def do_HEAD(self):
        return self.do_GET(headers_only=True)

    def do_GET(self, headers_only=False):
        '''
        GET request handler
        '''

        logger = logging.getLogger('http_HTTPHandler')
        self.clientconnected = True
        # Don't wait videodestroydelay if error happened
        self.errorhappened = True
        # Headers sent flag for fake headers UAs
        self.headerssent = False
        # Current greenlet
        self.requestgreenlet = gevent.getcurrent()
        # Connected client IP address
        self.clientip = self.request.getpeername()[0]

        req_headers = self.headers 
        self.client_data = { 
          'ip': self.clientip, 
          'forwarded-for': req_headers.get('X-Forwarded-For'),
          'client-agent': req_headers.get('User-Agent'),
          'uuid': uuid.uuid4()
          }
          
        if VPConfig.firewall:
            # If firewall enabled
            self.clientinrange = any(map(lambda i: ipaddr.IPAddress(self.clientip) \
                                in ipaddr.IPNetwork(i), VPConfig.firewallnetranges))

            if (VPConfig.firewallblacklistmode and self.clientinrange) or \
                (not VPConfig.firewallblacklistmode and not self.clientinrange):
                    logger.info('Dropping connection from ' + self.clientip + ' due to ' + \
                                'firewall rules')
                    self.dieWithError(403)  # 403 Forbidden
                    return

        logger.info("Accepted connection from " + self.clientip + " path " + self.path)

        try:
            self.splittedpath = self.path.split('/')
            self.reqtype = self.splittedpath[1].lower()
            # If first parameter is 'pid' or 'torrent' or it should be handled
            # by plugin
            if not (self.reqtype in ('get','mp4','ogg','ogv') or self.reqtype in VPStuff.pluginshandlers):
                self.dieWithError(400)  # 400 Bad Request
                return
        except IndexError:
            self.dieWithError(400)  # 400 Bad Request
            return

        # Handle request with plugin handler
        if self.reqtype in VPStuff.pluginshandlers:
            try:
                VPStuff.pluginshandlers.get(self.reqtype).handle(self)
            except Exception as e:
                logger.error('Plugin exception: ' + repr(e))
                logger.error(traceback.format_exc())
                self.dieWithError()
            finally:
                self.closeConnection()
                return
        self.handleRequest(headers_only)

    def handleRequest(self, headers_only):
      
        # Limit concurrent connections
        if 0 < VPConfig.maxconns <= VPStuff.clientcounter.total:
            logger.debug("Maximum connections reached, can't serve this")
            self.dieWithError(503)  # 503 Service Unavailable
            return

        # Pretend to work fine with Fake UAs or HEAD request.
        useragent = self.headers.get('User-Agent')
        logger.debug("HTTP User Agent:"+useragent)
        fakeua = useragent and useragent in VPConfig.fakeuas
        if headers_only or fakeua:
            if fakeua:
                logger.debug("Got fake UA: " + self.headers.get('User-Agent'))
            # Return 200 and exit
            self.send_response(200)
            self.send_header("Content-Type", "video/mpeg")
            self.end_headers()
            self.closeConnection()
            return

        self.path_unquoted = urllib2.unquote('/'.join(self.splittedpath[2:]))
        # Make list with parameters
        self.params = list()
        for i in xrange(3, 8):
            try:
                self.params.append(int(self.splittedpath[i]))
            except (IndexError, ValueError):
                self.params.append('0')

        # Adding client to clientcounter
        clients = VPStuff.clientcounter.add(self.reqtype+'/'+self.path_unquoted, self.client_data)
        # If we are the one client, but sucessfully got vp instance from clientcounter,
        # then somebody is waiting in the videodestroydelay state

        # Check if we are first client

        self.vlcid = hashlib.md5(self.reqtype+'/'+self.path_unquoted).hexdigest()

        try:
            if not VPStuff.vlcclient.check_stream(self.vlcid):
                logger.debug("First client, should create VLC session")
                shouldcreatevp = True
            else:
                logger.debug("Can reuse existing session")
                shouldcreatevp = False
        except Exception as e:
            logger.error('Plugin exception: ' + repr(e))
            logger.error(traceback.format_exc())
            self.dieWithError()            

        # Send fake headers if this User-Agent is in fakeheaderuas tuple
        if fakeua:
            logger.debug(
                "Sending fake headers for " + useragent)
            self.send_response(200)
            self.send_header('Cache-Control','no-cache, no-store, must-revalidate');
            self.send_header('Pragma','no-cache');
            if self.reqtype in ("ogg","ogv"):
                self.send_header("Content-Type", "video/ogg")
            else:
                self.send_header("Content-Type", "video/mpeg")
            self.end_headers()
            # Do not send real headers at all
            self.headerssent = True

        try:
            self.hanggreenlet = gevent.spawn(self.hangDetector)
            logger.debug("hangDetector spawned")
            gevent.sleep()

            # Getting URL
            self.errorhappened = False

            if shouldcreatevp:
                logger.debug("Got url " + self.path_unquoted)
                # Force ffmpeg demuxing if set in config
                if VPConfig.vlcforceffmpeg:
                    self.vlcprefix = 'http/ffmpeg://'
                else:
                    self.vlcprefix = ''

                logger.info("Starting broadcasting "+self.path)                    
                VPStuff.vlcclient.startBroadcast(
                    self.vlcid, self.vlcprefix + self.path_unquoted, VPConfig.vlcmux, VPConfig.vlcpreaccess, self.reqtype)
                # Sleep a bit, because sometimes VLC doesn't open port in
                # time
                gevent.sleep(0.5)

            # Building new VLC url
            self.url = 'http://' + VPConfig.vlchost + \
                ':' + str(VPConfig.vlcoutport) + '/' + self.vlcid
            logger.debug("VLC url " + self.url)

            # Sending client headers to videostream
            self.video = urllib2.Request(self.url)
            for key in self.headers.dict:
                self.video.add_header(key, self.headers.dict[key])

            self.video = urllib2.urlopen(self.video)

            # Sending videostream headers to client
            if not self.headerssent:
                self.send_response(self.video.getcode())
                if self.video.info().dict.has_key('connection'):
                    del self.video.info().dict['connection']
                if self.video.info().dict.has_key('server'):
                    del self.video.info().dict['server']
                if self.video.info().dict.has_key('transfer-encoding'):
                    del self.video.info().dict['transfer-encoding']
                if self.video.info().dict.has_key('content-type'):
                    del self.video.info().dict['content-type']
                if self.video.info().dict.has_key('keep-alive'):
                    del self.video.info().dict['keep-alive']

                for key in self.video.info().dict:
                    self.send_header(key, self.video.info().dict[key])

                self.send_header('Cache-Control','no-cache, no-store, must-revalidate');
                self.send_header('Pragma','no-cache');

                if self.reqtype=="ogg":
                    self.send_header("Content-Type", "video/ogg")
                else:
                    self.send_header("Content-Type", "video/mpeg")

                # End headers. Next goes video data
                self.end_headers()
                logger.debug("Headers sent")
                self.headerssent = True

            # Run proxyReadWrite
            self.proxyReadWrite()

            # Waiting until hangDetector is joined
            self.hanggreenlet.join()
            logger.debug("Request handler finished")
        except (vlcclient.VlcException) as e:
            logger.error("Exception: " + repr(e))
            VPStuff.vlcerrors = VPStuff.vlcerrors + 1
            logger.error("%s error(s) communicating VLC")
            self.errorhappened = True
            self.dieWithError()            
        except (vpclient.VPException, vlcclient.VlcException, urllib2.URLError) as e:
            logger.error("Exception: " + repr(e))
            self.errorhappened = True
            self.dieWithError()
        except gevent.GreenletExit:
            # hangDetector told us about client disconnection
            pass
        except Exception:
            # Unknown exception
            logger.error(traceback.format_exc())
            self.errorhappened = True
            self.dieWithError()
        finally:
            logger.debug("END REQUEST")
            logger.info("Closed connection from " + self.clientip + " path " + self.path)
            VPStuff.clientcounter.delete(self.reqtype+'/'+self.path_unquoted, self.client_data)
            self.vp.destroy()

class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):

    def handle_error(self, request, client_address):
        # Do not print HTTP tracebacks
        pass


class VPStuff(object):
    '''
    Inter-class interaction class
    '''
    vlcclient=None
    vlcerrors=0

# taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
def drop_privileges(uid_name, gid_name='nogroup'):

    # Get the uid/gid from the name
    running_uid = pwd.getpwnam(uid_name).pw_uid
    running_uid_home = pwd.getpwnam(uid_name).pw_dir
    running_gid = grp.getgrnam(gid_name).gr_gid

    # Remove group privileges
    os.setgroups([])

    # Try setting the new uid/gid
    os.setgid(running_gid)
    os.setuid(running_uid)

    # Ensure a very conservative umask
    old_umask = os.umask(077)

    if os.getuid() == running_uid and os.getgid() == running_gid:
        # could be useful
        os.environ['HOME'] = running_uid_home
        return True
    return False

logging.basicConfig(
    filename=VPConfig.logpath + 'vphttp.log' if VPConfig.loggingtoafile else None,
    format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=VPConfig.debug)
logger = logging.getLogger('INIT')

# Loading plugins
# Trying to change dir (would fail in freezed state)
try:
    os.chdir(os.path.dirname(os.path.realpath(__file__)))
except:
    pass
# Creating dict of handlers
VPStuff.pluginshandlers = dict()
# And a list with plugin instances
VPStuff.pluginlist = list()
pluginsmatch = glob.glob('plugins/*_plugin.py')
sys.path.insert(0, 'plugins')
pluginslist = [os.path.splitext(os.path.basename(x))[0] for x in pluginsmatch]
for i in pluginslist:
    plugin = __import__(i)
    plugname = i.split('_')[0].capitalize()
    try:
        plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
    except Exception as e:
        logger.error("Cannot load plugin " + plugname + ": " + repr(e))
        continue
    logger.debug('Plugin loaded: ' + plugname)
    for j in plugininstance.handlers:
        logger.info("Registering handler '" + j +"'")
        VPStuff.pluginshandlers[j] = plugininstance
    VPStuff.pluginlist.append(plugininstance)

# Check whether we can bind to the defined port safely
if os.getuid() != 0 and VPConfig.httpport <= 1024:
    logger.error("Cannot bind to port " + str(VPConfig.httpport) + " without root privileges")
    sys.exit(1)

server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
logger = logging.getLogger('HTTP')

# Dropping root privileges if needed
if VPConfig.vpproxyuser and os.getuid() == 0:
    if drop_privileges(VPConfig.vpproxyuser):
        logger.info("Dropped privileges to user " + VPConfig.vpproxyuser)
    else:
        logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
        sys.exit(1)

# Creating ClientCounter
VPStuff.clientcounter = ClientCounter()

DEVNULL = open(os.devnull, 'wb')

# Spawning procedures
def spawnVLC(cmd, delay = 0):
    try:
        VPStuff.vlc = psutil.Popen(cmd) #, stdout=DEVNULL, stderr=DEVNULL)
        VPStuff.vlcerrors = 0
        gevent.sleep(delay)
        return True
    except:
        return False

def connectVLC():
    try:
        VPStuff.vlcclient = vlcclient.VlcClient(
            host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
            out_port=VPConfig.vlcoutport)
        return True
    except vlcclient.VlcException as e:
        return False

def isRunning(process):
    if psutil.version_info[0] >= 2:
        if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
            return True
    else:  # for older versions of psutil
        if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
            return True
    return False

def findProcess(name):
    for proc in psutil.process_iter():
        try:
            pinfo = proc.as_dict(attrs=['pid', 'name'])
            if pinfo['name'] == name:
                return pinfo['pid']
        except psutil.AccessDenied:
            # System process
            pass
        except psutil.NoSuchProcess:
            # Process terminated
            pass
    return None

def clean_proc():
    # Trying to close all spawned processes gracefully
    if isRunning(VPStuff.vlc):
        if VPStuff.vlcclient:
            VPStuff.vlcclient.destroy()
        gevent.sleep(1)
    if isRunning(VPStuff.vlc):
        # or not :)
        VPStuff.vlc.terminate()
        gevent.sleep(1)
        if isRunning(VPStuff.vlc):
            VPStuff.vlc.kill()
    del VPStuff.vlc

def restartVLC(cmd, delay = 0):
    clean_proc()
    if spawnVLC(cmd, delay):
        if connectVLC():
            return True
    return False

# This is what we call to stop the server completely
def shutdown(signum = 0, frame = 0):
    logger.info("Stopping server...")
    # Closing all client connections
    for connection in server.RequestHandlerClass.requestlist:
        try:
            # Set errorhappened to prevent waiting for videodestroydelay
            connection.errorhappened = True
            connection.closeConnection()
        except:
            logger.warning("Cannot kill a connection!")
    clean_proc()
    server.server_close()
    sys.exit()

def _reloadconfig(signum=None, frame=None):
    '''
    Reload configuration file.
    SIGHUP handler.
    '''
    global VPConfig

    logger = logging.getLogger('reloadconfig')
    reload(vpconfig)
    from vpconfig import VPConfig
    logger.info('Config reloaded')

sched = BackgroundScheduler()
sched.start()

def clean_streams():
  if VPStuff.vlcclient:
    VPStuff.vlcclient.clean_streams(VPConfig.videodestroydelay)

job = sched.add_job(clean_streams, 'interval', seconds=15)

# setting signal handlers
try:
    gevent.signal(signal.SIGHUP, _reloadconfig)
    gevent.signal(signal.SIGTERM, shutdown)
except AttributeError:
    pass

name = 'vlc'
VPStuff.vlcProc = VPConfig.vlccmd.split()
if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
    logger.info("VLC spawned with pid " + str(VPStuff.vlc.pid))
else:
    logger.error('Cannot spawn or connect to VLC!')
    clean_proc()
    sys.exit(1)

try:
    logger.info("Using gevent %s" % gevent.__version__)
    logger.info("Usig psutil %s" % psutil.__version__)
    logger.info("Using VLC %s" % VPStuff.vlcclient._vlcver)
    logger.info("Server started.")
    while True:

        if not isRunning(VPStuff.vlc):

            del VPStuff.vlc
            if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
                logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
            else:
                logger.error("Cannot spawn VLC!")
                clean_proc()
                sys.exit(1)

        # Return to our server tasks
        server.handle_request()

        if VPStuff.vlcerrors>5:
            if restartVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout):
                logger.info("VLC hung, respawned it with pid " + str(VPStuff.vlc.pid))
            else:
                logger.error("Cannot spawn VLC!")
                clean_proc()
                sys.exit(1)
                                                                
except (KeyboardInterrupt, SystemExit):
    sched.shutdown()
    shutdown()
