#!/usr/local/nsg/usr/bin/python
# vim: tabstop=4 softtabstop=4 shiftwidth=4 textwidth=80 smarttab expandtab
"""
* Copyright (C) 2014  Sangoma Technologies Corp.
* All Rights Reserved.
*
* Author(s)
* Alkassoum Ahmoud <aahmoud@sangoma.com>
*
* This code is Sangoma Technologies Confidential Property.
* Use of and access to this code is covered by a previously executed
* non-disclosure agreement between Sangoma Technologies and the Recipient.
* This code is being supplied for evaluation purposes only and is not to be
* used for any other purpose.
"""

import sys
import os
import time
import logging
import re
import oswc
import sngpy
import pyodbc
import subprocess
import signal
#import iptc
#import netifaces as ni
import errno
import ctypes
from optparse import OptionParser
import ftplib
import getpass
import threading

def _get_local_files(local_dir, skip_files=None,walk=False):
    '''Retrieve local files list
    result_list == a list of dictionaries with path and mtime keys. ex: {'path':<filepath>,'mtime':<file last modified time>}
    ignore_dirs == a list of directories to ignore, should not include the base_dir.
    ignore_files == a list of files to ignore.
    ignore_file_ext == a list of extentions to ignore. 
    ''' 
    result_list = []   

    ignore_dirs = ['CVS', '.svn',',git']
    ignore_files = ['.project', '.pydevproject']
    ignore_file_ext = ['.pyc']

    if skip_files is not None:
        ignore_files.append(skip_files)

    base_dir = os.path.abspath(local_dir)

    for current_dir, dirs, files in os.walk(base_dir):
        for this_dir in ignore_dirs:
            if this_dir in dirs:
                dirs.remove(this_dir)

        sub_dir = current_dir.replace(base_dir, '')
        if not walk and sub_dir:
            break

        for this_file in files:
            if this_file not in ignore_files and os.path.splitext(this_file)[-1].lower() not in ignore_file_ext:
                filepath = os.path.join(current_dir, this_file)
                file_monitor_dict = {
                                     'path': filepath, 
                                     'mtime': os.path.getmtime(filepath)
                                     } 
                result_list.append(file_monitor_dict)
    return result_list 


class MediaRecorderFtpClient:

    def __init__(self,logger,server,user=None,password=None,base_local_dir=None,base_remote_dir=None):
        self._logger = logger
        self._server = server
        self._user = user
        self._password = password
        self._base_local_dir = os.path.abspath(base_local_dir)
        self._base_remote_dir = os.path.normpath(base_remote_dir)
        #self._timeout = timeout
        self._ftp_client = None
        self._PATH_CACHE = []

    def _initialize(self):
        self._logger.debug("Initializing ftp connection towards server %s using usser name %s and password %s\n" % \
                               (self._server,self._user,self._password))
        if self._server is None:
            self._logger.error("Invalid ftp server\n")
            return False

        if self._ftp_client is not None:
            self._ftp_client.quit()
            self._ftp_client = None

        self._ftp_client = ftplib.FTP()

        try:
            self._ftp_client.connect(self._server)
            self._logger.info('Connected to (%s)\n' % self._server)
        except socket.gaierror, e:
            self._ftp_client = None
            self._logger.error('Could not connect to (%s): %s\n' % (self._server, str(e.args)))
            return False
        except IOError, e:
            self._ftp_client = None
            self._logger.error('File not found: %s\n' % (str(e.args)))
            return False
        except socket.error, e:
            self._ftp_client = None
            self._logger.error('Could not connect to (%s): %s\n' % (self._server, str(e.args)))
            return False

        if self._user is not None and self._password is not None:
            try:
                self._ftp_client.login(self._user,self._password)
                self._logger.info('Logged into (%s) as (%s)\n' % (self._server, self._user))
            except ftplib.error_perm, e:
                self._logger.error('Check Username/Password: %s\n' % (str(e.args)))
                self._ftp_client.quit()
                self._ftp_client = None
                return False

        self._logger.info('FTP connection to server (%s) successfully initialized)\n' % (self._server))
        return True

    def _ftp_exists(self, path):
        '''path exists check function for ftp handler'''
        exists = None
        if path not in self._PATH_CACHE:
            try:
                self._ftp_client.cwd(path)
                exists = True
                self._PATH_CACHE.append(path)
            except ftplib.error_perm, e:
                if str(e.args).count('550'):    
                    exists = False
        else:
            exists = True

        return exists

    def _ftp_mkdirs(self, path, sep='/'):
        '''mkdirs function for ftp handler'''
        split_path = path.split(sep)

        new_dir = ''
        for server_dir in split_path:
            if server_dir:
                new_dir += sep + server_dir
                if not self._ftp_exists(new_dir):
                    try:
                        self._logger.debug('Attempting to create directory (%s) ...\n' % (new_dir))
                        self._ftp_client.mkd(new_dir)
                    except Exception, e:
                        self._logger.error('%s' % (str(e.args)))                



    def upload_all(self,delete_local_files_after_upload=True,files_to_skip=None,walk=False):

        if self._ftp_client is not None:
            self._ftp_client.quit()
            self._ftp_client = None

        if not self._initialize():
            self._logger.error('Unable to initialize ftp connection ...\n')
            return False

        self._logger.debug("Uploading all files under %s to ftp server %s directory %s \n" % \
                               (self._base_local_dir,self._server,self._base_remote_dir))

        local_files = _get_local_files(self._base_local_dir, files_to_skip,walk)
        for file_info in local_files:
            filepath = file_info['path']
            continue_on = False

            path, filename = os.path.split(filepath)
            remote_sub_path = path.replace(self._base_local_dir, '')
            remote_path = path.replace(self._base_local_dir, self._base_remote_dir)
            remote_path = remote_path.replace('\\', '/') # Convert to unix style

            if not self._ftp_exists(remote_path):
                self._ftp_mkdirs(remote_path)

            # Change to directory
            try:
                self._ftp_client.cwd(remote_path)
                continue_on = True
            except ftplib.error_perm, e:
                self._logger.error('%s\n' % (str(e.args)))

            if continue_on:
                if os.path.exists(filepath):
                    f_h = open(filepath,'rb')
                    filename = os.path.split(f_h.name)[-1]

                    display_filename = os.path.join(remote_sub_path, filename)
                    display_filename = display_filename.replace('\\', '/')

                    self._logger.debug('Sending (%s) ...\n' % (display_filename))
                    send_cmd = 'STOR %s' % (filename)
                    try:
                        self._ftp_client.storbinary(send_cmd, f_h)
                        f_h.close()
                        if delete_local_files_after_upload:
                            os.remove(filepath)
                    except Exception, e:
                        f_h.close()
                        self._logger.error("%s\n" % str(e.args))
                else:
                    self._logger.warning("File no longer exists, (%s)!\n" % (filepath))

        self._ftp_client.quit()
        self._ftp_client = None
        self._logger.debug('Successfully uploaded files to ftp server!\n')
        return True

class MediaRecorderFtpThread(threading.Thread):
    def __init__(self,logger,recording_ftp_client,timeout):
        threading.Thread.__init__(self)
        self._recording_ftp_client = recording_ftp_client
        self._timeout = timeout
        self._logger = logger
    def run(self):
        elapsed_time = 0
        self._logger.info("FTP uploading thread is now running with a period of %u" % (self._timeout))
        while is_daemon_alive():
            try:
                if elapsed_time >= self._timeout:
                    files_to_skip = get_current_active_recordings()
                    self._recording_ftp_client.upload_all(True,files_to_skip,True)
                    elapsed_time = 0
                else:
                    # sleep until timeout expired
                    time.sleep(1)
                    elapsed_time += 1
            except KeyboardInterrupt:
                self._logger.info("Stopping ftp uploading thread. User aborted.")
                break
        self._logger.info("FTP uploading thread terminated")


class MediaRecorderListener(oswc.EventListener):

    def __init__(self, logger):
        self._logger = logger
        super(oswc.EventListener, self).__init__()

    def on_event(self, event):
        self._logger.debug("Received media session event %s\n" % event)
        mediarec = get_mediarec()
        session_id = event.get_header("session-id")
        action = event.get_header("action")
        direction = event.get_header("direction")
        local_audio_ip = event.get_header("local-audio-ip")
        local_audio_port = event.get_header("local-audio-port")
        remote_audio_ip = event.get_header("remote-audio-ip")
        remote_audio_port = event.get_header("remote-audio-port")
        local_video_ip = event.get_header("local-video-ip")
        local_video_port = event.get_header("local-video-port")
        remote_video_ip = event.get_header("remote-video-ip")
        remote_video_port = event.get_header("remote-video-port")
        if action == 'start' :
            mediarec.start_recording(session_id,direction,local_audio_ip,local_audio_port,\
                                          remote_audio_ip,remote_audio_port,local_video_ip,\
                                          local_video_port,remote_video_ip,remote_video_port) 
        elif action == 'stop':
            mediarec.stop_recording(session_id,direction,local_audio_ip,local_audio_port,\
                                          remote_audio_ip,remote_audio_port,local_video_ip,\
                                          local_video_port,remote_video_ip,remote_video_port)
        else:
            self._logger.warning("Upsupported recording action %s\n" % action)


class MediaRecorder(sngpy.Service):

    _service_name = 'mediarec'

    def __init__(self, logger):
        self._logger = logger
        self._conf = dict()
        self._conf_params = ['recording-folder']
        self._oswc_conn = None
        self.sched = None
        self._null = None
        self._sleep_interval = 1
        self._recording_ftp_client = None

        self._swconf = dict()
        self._swconf_params = ['connection-string']
        self._recorded_sessions = dict()

        self._ftp_upload_supported = False
        self._ftpconf = dict()
        self._ftpconf_params = ['server','user','password','base-local-directory','base-remote-directory','ftp-upload-period']

        self._active_recordings = []
        self._active_recording_list_lock = threading.Lock()
        self._ftp_thread = None

        # custom MediaRec options should be added here via parser.add_option()
        self.parser = OptionParser()
        
        formatter = logging.Formatter(self._logformat)

        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)
        self._logger.addHandler(console_handler)

        self.sched = sngpy.Scheduler(time.time, time.sleep)

        self._null = open(os.devnull, 'w')
        super(MediaRecorder, self).__init__()


    def _parse_conf(self, params):
        self._logger.debug("Reading mediarec parameters")
        for p in params:
            self._logger.debug("mediarec: %s=%s" % (p.attrib['name'], p.attrib['value']))
            if p.attrib['name'] in self._conf_params:
                self._conf[p.attrib['name']] = p.attrib['value']
            else:
                raise ValueError, "Unknown mediarec XML parameter %s" % p.attrib['name']
        if 'recording-folder' not in self._conf:
            raise ValueError, "Missing mediarec recording-folder XML parameter"

    def _parse_switch(self, params):
        self._logger.debug("Reading switch parameters")
        for p in params:
            self._logger.debug("switch: %s=%s" % (p.attrib['name'], p.attrib['value']))
            if p.attrib['name'] in self._swconf_params:
                self._swconf[p.attrib['name']] = p.attrib['value']
            else:
                raise ValueError, "Unknown switch XML parameter %s" % p.attrib['name']
        if 'connection-string' not in self._swconf:
                raise ValueError, "Missing switch connection-string XML parameter"

    def _parse_ftp(self, params):
        self._logger.debug("Reading ftp parameters")
        for p in params:
            self._logger.debug("ftp: %s=%s" % (p.attrib['name'], p.attrib['value']))
            if p.attrib['name'] in self._ftpconf_params:
                self._ftpconf[p.attrib['name']] = p.attrib['value']
            else:
                raise ValueError, "Unknown switch XML parameter %s" % p.attrib['name']
        if 'server' not in self._ftpconf:
                self._ftp_upload_supported = False
        else:
            if 'base-local-directory' not in self._ftpconf:
                raise ValueError, "Missing ftp local-base-directory XML parameter"
            if 'base-remote-directory' not in self._ftpconf:
                raise ValueError, "Missing ftp remote-base-directory XML parameter"
            self._ftp_upload_supported = True

    def is_daemon_alive(self):
	return self.daemon_alive
    def configure(self):
        ret = True
        try:
            tree = super(MediaRecorder,self).configure()

            conf = tree.find('mediarec')
            if conf is None:
                raise ValueError, "Missing <mediamon> configuration"
            else:
                params = self._get_params(conf)
                self._parse_conf(params)

            swconf = tree.find('switch')
            if swconf is None:
                raise ValueError, "Missing <switch> configuration"
            else:
                params = self._get_params(swconf)
                self._parse_switch(params)

            ftpconf = tree.find('ftp')
            if ftpconf is None:
                raise ValueError, "Missing <ftp> configuration"
            else:
                params = self._get_params(ftpconf)
                self._parse_ftp(params)

        except:
            self._logger.critical("Failed to configure service")
            exc_type, exc_value, exc_traceback = sys.exc_info()
            self._print_exception(exc_type, exc_value, exc_traceback)
            ret = False

        return ret

    def start_recording(self,session_id,direction,local_audio_ip,local_audio_port,remote_audio_ip,remote_audio_port,local_video_ip,local_video_port,remote_video_ip,remote_video_port):
	found = self._recorded_sessions.get(session_id)
	if found is not None:
		self._logger.warning("Recording already initiated on session %s" % session_id)
		return False

	cmd = "tcpdump -i any -s 0 -w %s/%s_%s.pcap " % (self._conf['recording-folder'] ,session_id,direction)
        local_filter = None
        remote_filter = None

        if direction == "rx":
            local_filter = "dst"
            remote_filter = "src"
        elif direction == "tx":
            local_filter = "src"
            remote_filter = "dst"
        elif direction == "rxtx":
            local_filter = "host"
            remote_filter = "host"
        else:
            self._logger.warning("Unsupported recording direction %s" % direction)
            return False


        if local_video_ip is not None:
            if local_filter == "host" and remote_filter == "host":
                if local_video_ip == local_audio_ip and remote_video_ip == remote_audio_ip:
                    cmd += "( (src %s and dst %s) and (port %s or port %s or port %s or port %s)) or "  % \
                            (local_audio_ip,remote_audio_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                    cmd += "( (dst %s and src %s) and (port %s or port %s or port %s or port %s))"  % \
                            (local_audio_ip,remote_audio_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                elif local_video_ip == local_audio_ip and remote_video_ip != remote_audio_ip:
                    cmd += "( (src %s and (dst %s or dst %s)) and (port %s or port %s or port %s or port %s)) or "  % \
                            (local_audio_ip,remote_audio_ip,remote_video_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                    cmd += "( (dst %s and (src %s or src %s)) and (port %s or port %s or port %s or port %s))"  % \
                            (local_audio_ip,remote_audio_ip,remote_video_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                elif local_video_ip != local_audio_ip and remote_video_ip == remote_audio_ip:
                    cmd += "( (src %s or src %s) and dst %s) and (port %s or port %s or port %s or port %s)) or "  % \
                            (local_audio_ip,local_video_ip,remote_audio_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                    cmd += "( ((dst %s or dst %s) and src %s) and (port %s or port %s or port %s or port %s))"  % \
                            (local_audio_ip,local_video_ip,remote_audio_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                else:
                    cmd += "( (src %s or src %s) and (dst %s or dst %s)) and (port %s or port %s or port %s or port %s)) or "  % \
                           (local_audio_ip,local_video_ip,remote_audio_ip,remote_video_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                    cmd += "( ((dst %s or dst %s) and (src %s or src %s)) and (port %s or port %s or port %s or port %s))"  % \
                            (local_audio_ip,local_video_ip,remote_audio_ip,remote_video_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
            else:
                if local_video_ip == local_audio_ip and remote_video_ip == remote_audio_ip:
                    cmd += "( (%s %s and %s %s) and (port %s or port %s or port %s or port %s))"  % \
                            (local_filter,local_audio_ip,remote_filter,remote_audio_ip,local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                elif local_video_ip == local_audio_ip and remote_video_ip != remote_audio_ip:
                    cmd += "( (%s %s and (%s %s or %s %s)) and (port %s or port %s or port %s or port %s))"  % \
                            (local_filter,local_audio_ip,remote_filter,remote_audio_ip,remote_filter,remote_video_ip,\
                             local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                elif local_video_ip != local_audio_ip and remote_video_ip == remote_audio_ip:
                    cmd += "( (%s %s or %s %s) and dst %s) and (port %s or port %s or port %s or port %s))"  % \
                            (local_filter,local_audio_ip,local_filter,local_video_ip,remote_filter,remote_audio_ip,\
                             local_audio_port,remote_audio_port,local_video_port,remote_video_port)
                else:
                    cmd += "( (%s %s or %s %s) and (%s %s or %s %s)) and (port %s or port %s or port %s or port %s))"  % \
                           (local_filter,local_audio_ip,local_filter,local_video_ip,remote_filter,remote_audio_ip,remote_filter,remote_video_ip,\
                            local_audio_port,remote_audio_port,local_video_port,remote_video_port)
        else:
            if local_filter == "host" and remote_filter == "host":
                cmd += "( (src %s and dst %s) and (port %s or port %s )) or "  % (local_audio_ip,remote_audio_ip,local_audio_port,remote_audio_port)
                cmd += "( (dst %s and src %s) and (port %s or port %s ))"  % (local_audio_ip,remote_audio_ip,local_audio_port,remote_audio_port)
            else:
                cmd += "( (%s %s and %s %s) and (port %s or port %s ))"  % (local_filter,local_audio_ip,remote_filter,remote_audio_ip,local_audio_port,remote_audio_port)

        self._logger.debug("Running recording command %s" % cmd)
        args = cmd.split(' ')
        process = subprocess.Popen(args)
        if process is not None:
           self._recorded_sessions[session_id] = process
           self._logger.debug("Started pcap recording on session %s" % session_id)
           file_name = "%s_%s.pcap" % (session_id,direction)
           self._active_recording_list_lock.acquire()
           self._active_recordings.append(file_name)
           self._active_recording_list_lock.release()
           return True

        self._logger.warning("Failed to start pcap recording on session %s" % session_id)
        return False

    def stop_recording(self,session_id,direction,local_audio_ip,local_audio_port,remote_audio_ip,remote_audio_port,local_video_ip,local_video_port,remote_video_ip,remote_video_port):
	found = self._recorded_sessions.get(session_id)
	if found is None:
		self._logger.warning("Recording not initiated on session %s" % session_id)
		return False
        found.terminate()
        del self._recorded_sessions[session_id]
        self._logger.debug("Stopped pcap recording on session %s" % session_id)
        file_name = "%s_%s.pcap" % (session_id,direction)
        self._active_recording_list_lock.acquire()
        self._active_recordings.remove(file_name)
        self._active_recording_list_lock.release()
        return True

    def get_current_active_recordings(self):
        self._active_recording_list_lock.acquire()
        # copy the active list 
        active_recordings = self._active_recordings
        self._active_recording_list_lock.release()
        return active_recordings
          

    def _housekeeping(self):
        self.sched.fast_run()
        
    def _connect(self):
        # Connection loop (wait until we can connect or the daemon is stopped)
        self._oswc_conn = None
        connection_string = self._swconf['connection-string']
        while self.daemon_alive:
            self._housekeeping()
            # connect to the event provider
            if self._oswc_conn is None:
                self._oswc_conn = oswc.create_connection(connection_string, logger=self._logger)
                if self._oswc_conn is None:
                    self._logger.error("Failed to create connection to %s" % (connection_string))
                    time.sleep(self._sleep_interval)
                    continue

                listener = MediaRecorderListener(self._logger)
                listener.set_filter(['RECORD_MEDIA'])
                self._oswc_conn.add_event_listener(listener)

            if not self._oswc_conn.connect():
                self._logger.debug("Failed to connect to %s" % (connection_string))
                time.sleep(self._sleep_interval)
                continue

            self._logger.info("Connected to %s" % (connection_string))
            break

    def _setup_ftp(self):
        if self._ftp_upload_supported == False:
            return

        if not self.daemon_alive:
            return

        self._logger.info("Setting up ftp \n")
        user = None
        passw = None
        timeout = 3600 #by default upload files each hour

        if 'user' in self._ftpconf:
            user = self._ftpconf['user']

        if 'password' in self._ftpconf:
            passw = self._ftpconf['password']

        if 'ftp-upload-period' in self._ftpconf:
            timeout = int(self._ftpconf['ftp-upload-period'])
    
        self._recording_ftp_client = MediaRecorderFtpClient(self._logger,self._ftpconf['server'],\
                                                                user,passw,\
                                                                self._ftpconf['base-local-directory'],\
                                                                self._ftpconf['base-remote-directory'])
        self._ftp_thread = MediaRecorderFtpThread(self._logger,self._recording_ftp_client,timeout)
        self._ftp_thread.start()

    def run(self):
        super(MediaRecorder, self).run()
        
        try:
            self._connect()
            self._setup_ftp()
            self._logger.info("%s is now running\n" % (self._service_name))
        except KeyboardInterrupt:
            self._logger.info("User aborted %s" % (self._service_name))

        # Event loop
        while self.daemon_alive:
            try:
                self._housekeeping()
                sched_sleep = self.sched.next_event_time_delta()
                if sched_sleep is 0:
                    """
                    You'd think we could run a housekeeping
                    if sched_sleep is zero, but apparently
                    sometimes time goes back and we don't
                    want to call receive_event with 0 as
                    that seems to (oddly) block, just wait
                    one more second to be on the safe side
                    """
                    sched_sleep = 1;

                e = self._oswc_conn.receive_event(timeout=sched_sleep*1000)
                if e is not None and str(e) == 'SERVER_DISCONNECTED':
                    self._logger.info("Server connection lost")
                    self._connect()

            except KeyboardInterrupt:
                self._logger.info("Stopping %s. User aborted." %
                    (self._service_name))
                break
            except:
                exc_type, exc_value, exc_traceback = sys.exc_info()
                self._print_exception(exc_type, exc_value, exc_traceback)
                time.sleep(self._sleep_interval)

        if self._ftp_thread is not None:
            self._ftp_thread.join()
        self._logger.info("%s is now terminating" % (self._service_name))


logger = logging.getLogger(MediaRecorder._service_name)
logger.setLevel(logging.DEBUG)

## main() ##
def get_mediarec():
    return mediarec

def get_current_active_recordings():
    return get_mediarec().get_current_active_recordings()

def is_daemon_alive():
    return get_mediarec().is_daemon_alive()

mediarec = None
try:
    mediarec = MediaRecorder(logger)

    if mediarec.options.stop is not None:
        mediarec.stop()
        sys.exit(0)

    if mediarec.options.restart is not None:
        mediarec.restart()
        sys.exit(0)

    if mediarec.options.conf_path is None:
        # convenient way to retrieve -c option when service is running
        mediarec.options.conf_path = sngpy.Service.find_conf_path()

    if mediarec.options.conf_path is None:
        mediarec.parser.print_help()
        mediarec.parser.error("-c is required to find the configuration path")
        sys.exit(1)

    if mediarec.configure() is False:
        mediarec.parser.error("Failed to configure daemon using file %s" % mediarec.options.conf_path)
        sys.exit(1)


    # Following options (either start or run) should not be executed if the pid file exists
    if os.path.exists(mediarec.pidfile):
        logger.error("Service seems to be running already, pid file %s already exists" % mediarec.pidfile)
        sys.exit(1)

    # Decide whether to run in the background (Daemon mode) or foreground
    if mediarec.options.start is not None:
        mediarec.start()
    else:
        mediarec.run()

    sys.exit(0)
except KeyboardInterrupt:
    logger.info("Received keyboard interrupt, exiting.")
    sys.exit(0)
except SystemExit, e:
    """
    We just catch this so it won't end up in the catch-all below
    but we still must raise the exception if we want python to
    exit with the provided sys.exit() return code
    """
    if not sys.stdin.isatty():
        logger.info("SystemExit status %d" % e.code)
    raise
except:
    exc_type, exc_value, exc_traceback = sys.exc_info()
    sngpy.print_exception(exc_type, exc_value, exc_traceback)
    if not sys.stdin.isatty():
        logger.error("Unexpected exception %s/%s, aborting." % exc_type % exc_value)
    sys.exit(1)
