#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# This file is part of the  X2Go Project - https://www.x2go.org
# Copyright (C) 2012-2019 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
#
# X2Go Session Broker is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# X2Go Session Broker is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

import os
import sys
import argparse
import socket
import logging
import atexit
import importlib

try:
    import daemon
    import lockfile
    CAN_DAEMONIZE = True
    if os.path.isdir('/run'):
        RUNDIR = '/run'
    else:
        RUNDIR = '/var/run'
    pidfile = '{run}/x2gobroker/x2gobroker-daemon.pid'.format(run=RUNDIR)
    daemon_logdir = '/var/log/x2gobroker/'
except ImportError:
    CAN_DAEMONIZE = False

from grp import getgrnam

def prep_http_mode():

    global urls
    global settings

    # import classes serving the different web.py URLs
    import x2gobroker.web.plain
    import x2gobroker.web.json
    import x2gobroker.web.uccs
    import x2gobroker.web.extras

    # define the web.py URLs
    urls = ( ('/plain/(.*)', x2gobroker.web.plain.X2GoBrokerWeb,),
             ('/json/(.*)', x2gobroker.web.json.X2GoBrokerWeb,),
             ('/uccs/[a-zA-Z]*(/*)$', x2gobroker.web.uccs.X2GoBrokerWeb,),
             ('/uccs/(.*)/api/([0-9])(/*)$', x2gobroker.web.uccs.X2GoBrokerWebAPI,),
             ('/pubkeys(/*)$', x2gobroker.web.extras.X2GoBrokerPubKeyService,),
             ('/$', x2gobroker.web.extras.X2GoBrokerItWorks,),
           )
    settings = {
        'log_function': tornado_log_request,
    }


def logfile_prelude(mode='HTTP'):
    logger_broker.info('X2Go Session Broker ({version}),'.format(version=__VERSION__))
    logger_broker.info('  written by {author}'.format(author=__AUTHOR__))
    logger_broker.info('Setting up the broker\'s environment...')
    logger_broker.info('  X2GOBROKER_USER: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_USER))
    logger_broker.info('  X2GOBROKER_DAEMON_USER: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_DAEMON_USER))
    logger_broker.info('  X2GOBROKER_DAEMON_GROUP: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_DAEMON_GROUP))
    logger_broker.info('  X2GOBROKER_DEBUG: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_DEBUG))
    logger_broker.info('  X2GOBROKER_CONFIG: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_CONFIG))
    logger_broker.info('  X2GOBROKER_AGENT_CMD: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_AGENT_CMD))
    logger_broker.info('  X2GOBROKER_DEFAULT_BACKEND: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_DEFAULT_BACKEND))
    if mode != 'SSH':
        logger_broker.info('  X2GOBROKER_AUTHSERVICE_SOCKET: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_AUTHSERVICE_SOCKET))
        logger_broker.info('  X2GOBROKER_SSL_CERTFILE: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_SSL_CERTFILE))
        logger_broker.info('  X2GOBROKER_SSL_KEYFILE: {value}'.format(value=x2gobroker.defaults.X2GOBROKER_SSL_KEYFILE))

def cleanup_on_exit():
    try: os.remove(pidfile)
    except: pass

try:
    import x2gobroker.defaults
except ImportError:
    sys.path.insert(0, os.path.join(os.getcwd(), '..'))
    import x2gobroker.defaults

from x2gobroker import __VERSION__
from x2gobroker import __AUTHOR__
import x2gobroker.loggers
tornado_log_request = x2gobroker.loggers.tornado_log_request
PROG_NAME = x2gobroker.loggers.PROG_NAME
from x2gobroker.utils import drop_privileges, split_host_address

interactive_mode_warning = False
# check effective UID the broker runs as and complain appropriately...
if x2gobroker.defaults.X2GOBROKER_USER != x2gobroker.defaults.X2GOBROKER_DAEMON_USER and os.geteuid() != 0:
    interactive_mode_warning = True

# parse-in potential command line options
cmdline_args = None
if __name__ == "__main__":
    import setproctitle
    setproctitle.setproctitle(os.path.basename(sys.argv[0]))

    index = 0
    argv = []
    argv.append(sys.argv[0])
    double_dash_pos = 0
    double_dash_argv = []
    for arg in sys.argv[1:]:
        index += 1
        if double_dash_pos == 0 and arg == "--":
            double_dash_pos = index
            continue

        if double_dash_pos > 0:
            double_dash_argv.append(arg)
        else:
            argv.append(arg)

    sys.argv = argv

    general_options = [
        {'args':['-M','--mode'], 'default': 'SSH', 'metavar': 'BROKER_MODE', 'help': 'Mode of the X2Go Session Broker to run in (available: SSH, HTTP)', },
        {'args':['-C','--config-file'], 'default': None, 'metavar': 'CONFIG_FILE', 'help': 'Specify a special configuration file name, default is: {default}'.format(default=x2gobroker.defaults.X2GOBROKER_CONFIG), },
        {'args':['-d','--debug'], 'default': False, 'action': 'store_true', 'help': 'enable debugging code; also: allow testing in web browser (make http\'s POST method available as GET method, as well)', },
        {'args':['-i','--debug-interactively'], 'default': False, 'action': 'store_true', 'help': 'force output of log message to the stderr (rather than to the log files)', },
    ]
    daemon_options = [
        {'args':['-b', '--bind'], 'default': None, 'metavar': 'BIND_ADDRESS', 'help': 'The [address:]port that the web.py http-engine shall bind to (default: 127.0.0.1:8080)', },
    ]
    if CAN_DAEMONIZE:
        daemon_options.extend([
            {'args':['-D', '--daemonize'], 'default': False, 'action': 'store_true', 'help': 'Detach the X2Go Broker process from the current terminal and fork to background', },
            {'args':['-P', '--pidfile'], 'default': pidfile, 'help': 'Alternative file path for the daemon\'s PID file', },
            {'args':['-L', '--logdir'], 'default': daemon_logdir, 'help': 'Directory where log files for the process\'s stdout and stderr can be created', },
        ])
    if os.getuid() == 0:
        daemon_options.extend([
            {'args':['--drop-privileges'], 'default': False, 'action': 'store_true', 'help': 'Drop privileges to uid X2GOBROKER_DAEMON_USER and gid X2GOBROKER_DAEMON_GROUP', },
        ])

    sshbroker_options = [
        {'args':['--task'], 'default': None, 'metavar': 'BROKER_TASK', 'help': 'broker task (listsessions, selectsession, setpass, testcon)', },
        {'args':['--user'], 'default': None, 'metavar': 'USER_NAME', 'help': 'Operate on behalf of this X2Go Broker user name', },
        {'args':['--login'], 'default': None, 'metavar': 'LOGIN_NAME', 'help': 'Operate on behalf of this X2Go Server user name', },
        {'args':['--event'], 'default': None, 'metavar': 'EVENT_NAME', 'help': 'not-yet supported feature, we simply ignore this option for now...', },
        {'args':['--auth-cookie', '--next-authid', '--authid', ], 'default': None, 'metavar': 'AUTH_ID', 'help': 'Pre-shared (dynamic) authentication ID', },
        {'args':['--profile-id', '--sid', ], 'default': None, 'metavar': 'PROFILE_ID', 'help': 'for task: the profile ID selected from the list of available session profiles', },
        {'args':['--backend'], 'default': None, 'metavar': 'BROKER_BACKEND', 'help': 'select a non-default broker backend', },
    ]
    p = argparse.ArgumentParser(description='X2Go Session Broker (Standalone Daemon)',\
                                formatter_class=argparse.RawDescriptionHelpFormatter, \
                                add_help=True, argument_default=None)
    p_general = p.add_argument_group('general arguments')
    p_daemon = p.add_argument_group('arguments for standalone HTTP(s) daemon mode')
    p_sshbroker = p.add_argument_group('arguments for command line SSH broker mode')

    for (p_group, opts) in ( (p_general, general_options), (p_daemon, daemon_options), (p_sshbroker, sshbroker_options), ):
        for opt in opts:
            args = opt['args']
            del opt['args']
            p_group.add_argument(*args, **opt)

    cmdline_args = p.parse_args()

    if cmdline_args.config_file is not None:
        x2gobroker.defaults.X2GOBROKER_CONFIG = cmdline_args.config_file

    if cmdline_args.debug_interactively:
        # recreate loggers...
        logger_broker, logger_access, logger_error = x2gobroker.loggers.init_console_loggers()
        # define our own debugging loggers
        x2gobroker.loggers.logger_broker = logger_broker
        x2gobroker.loggers.logger_broker = logger_access
        x2gobroker.loggers.logger_error = logger_error
        cmdline_args.debug = True
    else:
        # use already defined loggers from the x2gobroker.loggers module...
        logger_broker = x2gobroker.loggers.logger_broker
        logger_access = x2gobroker.loggers.logger_broker
        logger_error = x2gobroker.loggers.logger_error

    # override X2GOBROKER_DEBUG=0 in os.environ with the command line switch
    if cmdline_args.debug:
        x2gobroker.defaults.X2GOBROKER_DEBUG = cmdline_args.debug

    # daemonizing only makes sense for the HTTP broker mode...
    if cmdline_args.daemonize:
        cmdline_args.mode = 'HTTP'

    # evaluate other cmdline options depending on the broker mode
    if cmdline_args.mode.upper() not in ('SSH', 'HTTP'):
        logger_broker.error('Invalid mode selected. Available: SSH or HTTP.')
        sys.exit(-1)

    # ignore EVENTS for now...
    if cmdline_args.event and cmdline_args.mode == 'SSH':
        logger_broker.warning('X2Go client sent event: {event_name}. Events are not supported, yet. Ignoring that...'.format(event_name=cmdline_args.event))
        logger_broker.warning('X2Go client\'s event info is: {event_info}. Events are not supported, yet. Ignoring that...'.format(event_info=double_dash_argv))
        sys.exit(0)

    ### SSH broker
    elif cmdline_args.mode.upper() == 'SSH' and not PROG_NAME == 'x2gobroker-daemon':
        if cmdline_args.bind: logger_broker.warn('ignoring non-valid option --bind for broker mode SSH...')
        if cmdline_args.daemonize: logger_broker.warn('ignoring non-valid option --daemonize for broker mode SSH...')
        if cmdline_args.profile_id and cmdline_args.task != 'selectsession':
            #logger_broker.warn('ignoring option --sid as it only has a meaning with ,,--task selectsession\'\'')
            pass

        # is a specific X2Go Broker user given on the command line?
        if cmdline_args.user is None:
            cmdline_args.user = os.environ['LOGNAME']
        elif os.environ['LOGNAME'] != x2gobroker.defaults.X2GOBROKER_DAEMON_USER:
            logger_broker.warn('denying context change to user `{user}\', only allowed for magic user `{magic_user}\''.format(user=cmdline_args.user, magic_user=x2gobroker.defaults.X2GOBROKER_DAEMON_USER))
            cmdline_args.user = os.environ['LOGNAME']

        # is a specific X2Go Server login name given on the command line?
        # if not, assume broker user and X2Go Server login are the same...
        if cmdline_args.login is None:
            cmdline_args.login = cmdline_args.user

        # bail out if no task is given on the command line
        if cmdline_args.task is None:
            print("")
            p.print_usage()
            print("No task specified, doing nothing...");
            print("")
            sys.exit(-2)

    ### HTTP broker
    elif cmdline_args.mode.upper() == 'HTTP' or PROG_NAME == 'x2gobroker-daemon':
        logger_broker.info('  DAEMON_BIND_ADDRESS: {value}'.format(value=cmdline_args.bind))
        if interactive_mode_warning:
            logger_broker.warn('X2Go Session Broker has been started interactively by user {username},'.format(username=x2gobroker.defaults.X2GOBROKER_USER))
            logger_broker.warn('  better run as user {daemon_username}.'.format(daemon_username=x2gobroker.defaults.X2GOBROKER_DAEMON_USER))
            logger_broker.warn('Automatically switching to DEBUG mode due to interactive launch of this application.')
            x2gobroker.defaults.X2GOBROKER_DEBUG = True
        if cmdline_args.bind is None:
            cmdline_args.bind = x2gobroker.defaults.DAEMON_BIND_ADDRESS
        if cmdline_args.user:        logger_broker.warn('ignoring non-valid option --user for broker mode HTTP...')
        if cmdline_args.auth_cookie: logger_broker.warn('ignoring non-valid option --auth-cookie for broker mode HTTP...')
        if cmdline_args.task:        logger_broker.warn('ignoring non-valid option --task for broker mode HTTP...')
        if cmdline_args.profile_id:  logger_broker.warn('ignoring non-valid option --profile-id for broker mode HTTP...')

        if CAN_DAEMONIZE and cmdline_args.daemonize:
            pidfile = os.path.expanduser(cmdline_args.pidfile)
            if not os.path.isdir(os.path.dirname(pidfile)):
                try:
                    os.makedirs(os.path.dirname(pidfile))
                except:
                    pass
            try:
                os.chown(os.path.dirname(pidfile), 0, getgrnam(x2gobroker.defaults.X2GOBROKER_DAEMON_GROUP).gr_gid)
                os.chmod(os.path.dirname(pidfile), 0o770)
            except OSError:
                pass

            if not (os.access(os.path.dirname(pidfile), os.W_OK) and os.access(os.path.dirname(pidfile), os.X_OK)) or (os.path.exists(pidfile) and not os.access(pidfile, os.W_OK)):
                print("")
                p.print_usage()
                print("Insufficent privileges. Cannot create PID file {pidfile} path".format(pidfile=pidfile))
                print("")
                sys.exit(-3)

            # the log dir should really be create by distro package maintainers...
            daemon_logdir = os.path.expanduser(cmdline_args.logdir)
            if not os.path.isdir(daemon_logdir):
                try:
                    os.makedirs(daemon_logdir)
                except:
                    pass
            if not (os.access(daemon_logdir, os.W_OK) and os.access(daemon_logdir, os.X_OK)):
                print("")
                p.print_usage()
                print("Insufficent privileges. Cannot create directory for stdout/stderr log files: {logdir}".format(logdir=daemon_logdir))
                print("")
                sys.exit(-3)
            else:
                if not daemon_logdir.endswith('/'):
                    daemon_logdir += '/'

        bind_address, bind_port = split_host_address(cmdline_args.bind, default_address=None, default_port=8080)
        cmdline_args.bind = "[{address}]:{port}".format(address=bind_address, port=bind_port)

    if os.getuid() == 0 and cmdline_args.drop_privileges:
        drop_privileges(uid=x2gobroker.defaults.X2GOBROKER_DAEMON_USER, gid=x2gobroker.defaults.X2GOBROKER_DAEMON_GROUP)


urls = ()
settings = {}




# run the Python Tornado standalone daemon or handle interactive command line execution (via SSH)
if __name__ == "__main__":

    # raise log level to DEBUG if requested...
    if x2gobroker.defaults.X2GOBROKER_DEBUG and not x2gobroker.defaults.X2GOBROKER_TESTSUITE:
        logger_broker.setLevel(logging.DEBUG)
        logger_access.setLevel(logging.DEBUG)
        logger_error.setLevel(logging.DEBUG)

    logfile_prelude(mode=cmdline_args.mode.upper())

    if cmdline_args.mode.upper() == 'HTTP' or PROG_NAME == 'x2gobroker-daemon':

        ### launch as standalone HTTP daemon ###

        prep_http_mode()

        import tornado.web
        import tornado.httpserver
        import tornado.ioloop

        def launch_ioloop():
            tornado.ioloop.IOLoop.instance().start()

        application = tornado.web.Application(urls, **settings)
        try:
            if x2gobroker.defaults.X2GOBROKER_SSL_CERTFILE and x2gobroker.defaults.X2GOBROKER_SSL_KEYFILE:
                # switch on https:// mode
                http_server = tornado.httpserver.HTTPServer(application,
                                                            ssl_options={
                                                                "certfile": x2gobroker.defaults.X2GOBROKER_SSL_CERTFILE,
                                                                "keyfile": x2gobroker.defaults.X2GOBROKER_SSL_KEYFILE,
                                                            },
                )
            else:
                # run without https
                http_server = tornado.httpserver.HTTPServer(application)
            try:
                http_server.listen(bind_port, address=bind_address)
            except OSError as e:
                print (e)
                sys.exit(1)

            if CAN_DAEMONIZE and cmdline_args.daemonize:
                atexit.register(cleanup_on_exit)
                keep_fds = [int(fd) for fd in os.listdir('/proc/self/fd') if fd not in (0,1,2) ]
                daemon_stdout = open(daemon_logdir+'x2gobroker-daemon.stdout', 'w+')
                daemon_stderr = open(daemon_logdir+'x2gobroker-daemon.stderr', 'w+')
                logger_broker.info('Forking daemon to background, PID file is: {pidfile}'.format(pidfile=pidfile))
                with daemon.DaemonContext(stdout=daemon_stdout, stderr=daemon_stderr, files_preserve=keep_fds, umask=0o027, pidfile=lockfile.FileLock(pidfile), detach_process=True):
                    open(pidfile, 'w+').write(str(os.getpid())+'\n')
                    launch_ioloop()
            else:
                launch_ioloop()

        except socket.error as e:
            print (e)

    elif cmdline_args.mode.upper() == 'SSH':

        ### run interactively from the command line (i.e. via SSH) ###

        import x2gobroker.client.plain
        cmdline_client = x2gobroker.client.plain.X2GoBrokerClient()
        output = cmdline_client.get(cmdline_args)
        if output: print(output)

else:

    ### launch as WSGI application ###

    import asyncio
    from tornado.platform.asyncio import AnyThreadEventLoopPolicy
    asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())

    logger_broker = x2gobroker.loggers.logger_broker
    logger_access = x2gobroker.loggers.logger_broker
    logger_error = x2gobroker.loggers.logger_error

    # raise log level to DEBUG if requested...
    if x2gobroker.defaults.X2GOBROKER_DEBUG and not x2gobroker.defaults.X2GOBROKER_TESTSUITE:
        logger_broker.setLevel(logging.DEBUG)
        logger_access.setLevel(logging.DEBUG)
        logger_error.setLevel(logging.DEBUG)

    prep_http_mode()

    import tornado.wsgi
    import wsgilog
    _tornado_application = tornado.wsgi.WSGIApplication(urls, **settings)

    def _application(environ, start_response):

        # some WSGI implementations do not like the SCRIPT_NAME env var
        if 'SCRIPT_NAME' in environ:
            del environ['SCRIPT_NAME']

        # make sure the httpd server's environment is set as os.environ
        for key in environ.keys():
            if key.startswith('X2GOBROKER_'):
                os.environ.update({ key: environ[key] })
        importlib.reload(x2gobroker.defaults)
        logfile_prelude()

        return _tornado_application(environ, start_response)

    application = wsgilog.WsgiLog(_application, tohtml=True, tofile=True, tostream=False, toprint=False, file='/var/log/x2gobroker/wsgi.log', )
