#!/usr/bin/env python
# -*- coding: utf-8 -*-
#

__version__ = '0.3.3'

import sys
import os
import base64
import time
from binascii import hexlify
import sys
import threading
from multiprocessing.process import Process
import traceback
import tty
import termios
import struct
import fcntl
import signal
import socket
import select
import errno
import paramiko
import django

BASE_DIR = os.path.abspath(os.path.dirname(__file__))
APP_DIR = os.path.join(os.path.dirname(BASE_DIR), 'apps')
sys.path.append(APP_DIR)
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'

try:
    django.setup()
except IndexError:
    pass

from django.conf import settings
from users.utils import ssh_key_gen, check_user_is_valid
from utils import get_logger, SSHServerException, control_char


logger = get_logger(__name__)

paramiko.util.log_to_file(os.path.join(BASE_DIR, 'logs', 'paramiko.log'))


class SSHServer(paramiko.ServerInterface):
    host_key_path = os.path.join(BASE_DIR, 'host_rsa_key')
    channel_pools = []

    def __init__(self, client, addr):
        self.event = threading.Event()
        self.change_window_size_event = threading.Event()
        self.client = client
        self.addr = addr
        self.username = None
        self.user = None
        self.channel_width = None
        self.channel_height = None

    @classmethod
    def host_key(cls):
        return cls.get_host_key()

    @classmethod
    def get_host_key(cls):
        logger.debug("Get ssh server host key")
        if not os.path.isfile(cls.host_key_path):
            cls.host_key_gen()
        return paramiko.RSAKey(filename=cls.host_key_path)

    @classmethod
    def host_key_gen(cls):
        logger.debug("Generate ssh server host key")
        ssh_key, ssh_pub_key = ssh_key_gen()
        with open(cls.host_key_path, 'w') as f:
            f.write(ssh_key)

    def check_channel_request(self, kind, chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_auth_password(self, username, password):
        self.user = user = check_user_is_valid(username=username, password=password)
        if self.user:
            self.username = username = user.username
            logger.info('Accepted password for %(username)s from %(host)s port %(port)s ' % {
                'username': username,
                'host': self.addr[0],
                'port': self.addr[1],
            })
            return paramiko.AUTH_SUCCESSFUL
        else:
            logger.info('Authentication password failed for %(username)s from %(host)s port %(port)s ' % {
                'username': username,
                'host': self.addr[0],
                'port': self.addr[1],
            })
        return paramiko.AUTH_FAILED

    def check_auth_publickey(self, username, public_key):
        self.user = user = check_user_is_valid(username=username, public_key=public_key)

        if self.user:
            self.username = username = user.username
            logger.info('Accepted public key for %(username)s from %(host)s port %(port)s ' % {
                'username': username,
                'host': self.addr[0],
                'port': self.addr[1],
            })
            return paramiko.AUTH_SUCCESSFUL
        else:
            logger.info('Authentication public key failed for %(username)s from %(host)s port %(port)s ' % {
                'username': username,
                'host': self.addr[0],
                'port': self.addr[1],
            })
        return paramiko.AUTH_FAILED

    def get_allowed_auths(self, username):
        auth_method_list = []
        if settings.CONFIG.SSH_PASSWORD_AUTH:
            auth_method_list.append('password')
        if settings.CONFIG.SSH_PUBLICK_KEY_AUTH:
            auth_method_list.append('publickey')
        return ','.join(auth_method_list)

    def check_channel_shell_request(self, channel):
        self.event.set()
        self.__class__.channel_pools.append(channel)
        channel.username = self.username
        channel.addr = self.addr
        return True

    def check_channel_pty_request(self, channel, term, width, height, pixelwidth,
                                     pixelheight, modes):
        channel.change_window_size_event = threading.Event()
        channel.width = width
        channel.height = height
        return True

    def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight):
        channel.change_window_size_event.set()
        channel.width = width
        channel.height = height
        return True


class BackendServer:
    def __init__(self, host, port, username):
        self.host = host
        self.port = port
        self.username = username
        self.ssh = None
        self.channel = None

    def connect(self, term='xterm', width=80, height=24, timeout=10):
        self.ssh = ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password,
                        pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True, timeout=timeout)
        except Exception:
            logger.warning('Connect backend server %s failed' % self.host)
            return None

        self.channel = channel = ssh.invoke_shell(term=term, width=width, height=height)
        logger.info('Connect backend server %(username)s@%(host)s:%(port)s successfully' % {
            'username': self.username,
            'host': self.host,
            'port': self.port,
        })
        channel.settimeout(100)
        channel.host = self.host
        channel.port = self.port
        channel.username = self.username
        return channel

    @property
    def host_password(self):
        return 'redhat'

    @property
    def host_private_key(self):
        return None


class Navigation:
    def __init__(self, username, client_channel):
        self.username = username
        self.client_channel = client_channel

    def display_banner(self):
        client_channel = self.client_channel
        client_channel.send(control_char.clear)
        client_channel.send('\r\n\r\n\t\tWelcome to use Jumpserver open source system !\r\n\r\n')
        client_channel.send('If you find some bug please contact us <ibuler@qq.com>\r\n')
        client_channel.send('See more at https://www.jumpserver.org\r\n')
        # client_channel.send(self.username)

    def display(self):
        self.display_banner()

    def return_to_connect(self):
        pass


class ProxyChannel:
    ENTER_CHAR = ['\r', '\n', '\r\n']
    input_data = []
    output_data = []

    def __init__(self, client_channel, backend_channel, client_addr):
        self.client_channel = client_channel
        self.backend_channel = backend_channel
        self.client_addr = client_addr
        self.in_input_mode = True

    def stream_flow(self, input_=None, output_=None):
        if input_:
            self.in_input_mode = True
            if input_ in ['\r', '\n', '\r\n']:
                self.in_input_mode = False

        if output_:
            print(''.join(self.__class__.output_data))
            if not self.in_input_mode:
                command = ''.join(self.__class__.output_data)
                del self.__class__.output_data
                self.__class__.output_data = []
            self.__class__.output_data.append(output_)

    def proxy(self):
        client_channel = self.client_channel
        backend_channel = self.backend_channel
        client_addr = self.client_addr

        while True:
            r, w, x = select.select([client_channel, backend_channel], [], [])

            if client_channel.change_window_size_event.is_set():
                backend_channel.resize_pty(width=client_channel.width, height=client_channel.height)

            if client_channel in r:
                self.in_input_mode = True
                client_data = client_channel.recv(1024)

                if client_data in self.__class__.ENTER_CHAR:
                    self.in_input_mode = False
                    command = ''.join(self.__class__.output_data)
                    print('########### command ##########')
                    print(command)
                    print('########### end command ##########')
                    del self.__class__.output_data
                    self.__class__.output_data = []
                    backend_channel.send(client_data)
                    output = ''.join(self.__class__.output_data)
                    print('>>>>>>>>>>> output <<<<<<<<<<')
                    print(output)
                    print('>>>>>>>>>>> end output <<<<<<<<<<')
                    continue

                if len(client_data) == 0:
                    logger.info('Logout from ssh server %(host)s: %(username)s' % {
                        'host': client_addr[0],
                        'username': client_channel.username,
                    })
                    break
                backend_channel.send(client_data)

            if backend_channel in r:
                backend_data = backend_channel.recv(1024)
                if len(backend_data) == 0:
                    client_channel.send('Disconnect from %s \r\n' % backend_channel.host)
                    client_channel.close()
                    logger.info('Logout from backend server %(host)s: %(username)s' % {
                        'host': backend_channel.host,
                        'username': backend_channel.username,
                    })
                    break
                self.__class__.output_data.append(backend_data)
                client_channel.send(backend_data)


class JumpServer:
    backend_server_pools = []
    backend_channel_pools = []
    client_channel_pools = []

    CONTROL_CHAR = {
        'clear': ''
    }

    def __init__(self):
        self.listen_host = '0.0.0.0'
        self.listen_port = 2222

    def display_navigation(self, username, client_channel):
        nav = Navigation(username, client_channel)
        nav.display()
        return 'j', 22, 'root'

    def get_client_channel(self, client, addr):
        transport = paramiko.Transport(client, gss_kex=False)
        transport.set_gss_host(socket.getfqdn(""))
        try:
            transport.load_server_moduli()
        except:
            logger.warning('Failed to load moduli -- gex will be unsupported.')
            raise

        transport.add_server_key(SSHServer.get_host_key())
        ssh_server = SSHServer(client, addr)

        try:
            transport.start_server(server=ssh_server)
        except paramiko.SSHException:
            logger.warning('SSH negotiation failed.')

        client_channel = transport.accept(20)
        if client_channel is None:
            logger.warning('No ssh channel get.')
            return None

        self.__class__.client_channel_pools.append(client_channel)
        if not ssh_server.event.is_set():
            logger.warning('Client never asked for a shell.')
        return client_channel

    def get_backend_channel(self, host, port, username, term='xterm', width=80, height=24):
        backend_server = BackendServer(host, port, username)
        backend_channel = backend_server.connect(term=term, width=width, height=height)

        if backend_channel is None:
            logger.warning('Connect %(username)s@%(host)s:%(port)s failed' % {
                'username': username,
                'host': host,
                'port': port,
            })
            return None

        self.__class__.backend_server_pools.append(backend_server)
        self.__class__.backend_channel_pools.append(backend_channel)

        return backend_channel

    def command_flow(self, input_=None, output_=None):
        pass

    def handle_ssh_request(self, client, addr):
        logger.info("Get ssh request from %(host)s:%(port)s" % {
            'host': addr[0],
            'port': addr[1],
        })

        try:
            client_channel = self.get_client_channel(client, addr)
            if client_channel is None:
                client.close()
                return

            host, port, username = self.display_navigation('root', client_channel)
            backend_channel = self.get_backend_channel(host, port, username,
                                                       width=client_channel.width,
                                                       height=client_channel.height)
            if backend_channel is None:
                client.shutdown()
                client.close()
                client.send('Close')
                return

            proxy_channel = ProxyChannel(client_channel, backend_channel, addr)
            proxy_channel.proxy()

        # Todo: catch other exception
        except IndexError:
            logger.info('Close with server %s from %s' % (addr[0], addr[1]))
            sys.exit(100)

    def listen(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind((self.listen_host, self.listen_port))
        sock.listen(5)

        print(time.ctime())
        print('Jumpserver version %s, more see https://www.jumpserver.org' % __version__)
        print('Starting ssh server at %(host)s:%(port)s' % {'host': self.listen_host, 'port': self.listen_port})
        print('Quit the server with CONTROL-C.')

        while True:
            try:
                client, addr = sock.accept()
                thread = threading.Thread(target=self.handle_ssh_request, args=(client, addr))
                thread.daemon = True
                thread.start()
            except Exception as e:
                logger.error('Bind failed: ' + str(e))
                traceback.print_exc()
                sys.exit(1)


if __name__ == '__main__':
    server = JumpServer()
    try:
        server.listen()
    except KeyboardInterrupt:
        sys.exit(1)

