#!/usr/bin/env python
#
# Copyright (c) SAS Institute, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#


"""
Telnet Server implementation.

Based on telnetlib telnet client - reads in and parses telnet protocol
from the socket, understands window change requests and interrupt requests.
(IP and NAWS).

This server does _NOT_ do LINEMODE, instead it is character based.  This means
to talk to this server using the standard telnet client, you'll need to first
type "CTRL-] mode char\n"
"""
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division
from __future__ import absolute_import

from six.moves.socketserver import TCPServer, BaseRequestHandler
import fcntl
import os
import pty
import select
import signal
import six
import socket
import struct
import sys
import telnetlib
import termios
from telnetlib import IAC, IP, SB, SE, DO, DONT, WILL, TM, NAWS

IPRESP = b'\x03'  # chr(ord('C') & 0x1F)


class TelnetServerProtocolHandler(telnetlib.Telnet):
    """
        Code that actually understands telnet protocol.
        Accepts telnet-coded input from the socket and passes on that
        information to local, which should be the master for a pty controlled
        process.
    """

    def __init__(self, socket, local):
        telnetlib.Telnet.__init__(self)
        self.sock = socket
        self.remote = self.sock.fileno()
        self.local = local
        self.set_option_negotiation_callback(self.process_IAC)

    def process_IAC(self, sock, cmd, option):
        """
            Read in and parse IAC commands as passed by telnetlib.

            SB/SE commands are stored in sbdataq, and passed in w/ a command
            of SE.
        """
        if cmd == DO:
            if option == TM:
                # timing mark - send WILL into outgoing stream
                os.write(self.remote, IAC + WILL + TM)
            else:
                pass
        elif cmd == IP:
            # interrupt process
            os.write(self.local, IPRESP)
        elif cmd == SB:
            pass
        elif cmd == SE:
            option = self.sbdataq[0]
            if option == NAWS[0]:
                # negotiate window size.
                cols = six.indexbytes(self.sbdataq, 1)
                rows = six.indexbytes(self.sbdataq, 2)
                s = struct.pack('HHHH', rows, cols, 0, 0)
                fcntl.ioctl(self.local, termios.TIOCSWINSZ, s)
        elif cmd == DONT:
            pass
        else:
            pass

    def handle(self):
        """
            Performs endless processing of socket input/output, passing
            cooked information onto the local process.
        """
        while True:
            toRead = select.select([self.local, self.remote], [], [], 0.1)[0]
            if self.local in toRead:
                data = os.read(self.local, 4096)
                self.sock.sendall(data)
                continue
            if self.remote in toRead or self.rawq:
                buf = self.read_eager()
                os.write(self.local, buf)
                continue


class TelnetRequestHandler(BaseRequestHandler):
    """
        Request handler that serves up a shell for users who connect.
        Derive from this class to change the execute() method to change how
        what command the request serves to the client.
    """
    command = '/bin/sh'
    args = ['/bin/sh']

    def setup(self):
        pass

    def handle(self):
        """
            Creates a child process that is fully controlled by this
            request handler, and serves data to and from it via the
            protocol handler.
        """
        pid, fd = pty.fork()
        if pid:
            protocol = TelnetServerProtocolHandler(self.request, fd)
            protocol.handle()
        else:
            self.execute()

    def execute(self):
        try:
            os.execv(self.command, self.args)
        finally:
            os._exit(1)

    def finish(self):
        pass


class TelnetServer(TCPServer):
    allow_reuse_address = True

    def __init__(self, server_address=None,
                 requestHandlerClass=TelnetRequestHandler):
        if not server_address:
            server_address = ('', 23)
        TCPServer.__init__(self, server_address, requestHandlerClass)


class TelnetServerForCommand(TelnetServer):
    def __init__(self, server_address=None,
                 requestHandlerClass=TelnetRequestHandler,
                 command=['/bin/sh']):
        class RequestHandler(requestHandlerClass):
            pass
        RequestHandler.command = command[0]
        RequestHandler.args = command
        TelnetServer.__init__(self, server_address, RequestHandler)


class InvertedTelnetRequestHandler(TelnetRequestHandler):
    def handle(self):
        masterFd, slaveFd = pty.openpty()
        try:
            # if we're not in the main thread, this will not work.
            signal.signal(signal.SIGTTOU, signal.SIG_IGN)
        except:  # noqa
            pass
        pid = os.fork()
        if pid:
            os.close(masterFd)
            raise SocketConnected(slaveFd, pid)
            # make parent process the pty slave - the opposite of
            # pty.fork().  In this setup, the parent process continues
            # to act normally, while the child process performs the
            # logging.  This makes it simple to kill the logging process
            # when we are done with it and restore the parent process to
            # normal, unlogged operation.
        else:
            os.close(slaveFd)
            try:
                protocol = TelnetServerProtocolHandler(self.request, masterFd)
                protocol.handle()
            finally:
                os.close(masterFd)
                os._exit(1)


class InvertedTelnetServer(TelnetServer):
    """
        Creates a telnet server that controls the stdin and stdout
        of the current process, instead of serving a subprocess.

        The telnet server can be closed at any time, and when it is
        input and output for the current process will be restored.
    """

    def __init__(self, server_address=None,
                 requestHandlerClass=InvertedTelnetRequestHandler):
        TelnetServer.__init__(self, server_address, requestHandlerClass)
        self.closed = True
        self.oldStdin = self.oldStdout = self.oldStderr = None
        self.oldTermios = None

    def handle_request(self):
        """
            Handle one request - serve current process to one connection.

            Use close_request() to disconnect this process.
        """
        try:
            request, client_address = self.get_request()
        except socket.error:
            return
        if self.verify_request(request, client_address):
            try:
                # we only serve once, and we want to free up the port
                # for future serves.
                self.socket.close()
                self.process_request(request, client_address)
            except SocketConnected as err:
                self._serve_process(err.slaveFd, err.serverPid)
                return
            except Exception:
                self.handle_error(request, client_address)
                self.close_request()

    def _serve_process(self, slaveFd, serverPid):
        """
            Serves a process by connecting its outputs/inputs to the pty
            slaveFd.  serverPid is the process controlling the master fd
            that passes that output over the socket.
        """
        self.serverPid = serverPid
        if sys.stdin.isatty():
            self.oldTermios = termios.tcgetattr(sys.stdin.fileno())
        else:
            self.oldTermios = None
        self.oldStderr = SavedFile(2, sys, 'stderr')
        self.oldStdout = SavedFile(1, sys, 'stdout')
        self.oldStdin = SavedFile(0, sys, 'stdin')
        self.oldStderr.save(slaveFd, mode="w")
        self.oldStdout.save(slaveFd, mode="w")
        self.oldStdin.save(slaveFd, mode="r")
        os.close(slaveFd)
        self.closed = False

    def close_request(self):
        if self.closed:
            pass
        self.closed = True
        # restore old terminal settings before quitting
        self.oldStderr.restore()
        self.oldStdout.restore()
        self.oldStdin.restore()
        self.oldStderr = self.oldStdout = self.oldStdin = None
        if self.oldTermios is not None:
            termios.tcsetattr(0, termios.TCSADRAIN, self.oldTermios)
        os.waitpid(self.serverPid, 0)


class SocketConnected(Exception):
    """
        Control-Flow Exception raised when we have successfully connected
        a socket.

        Used for IntertedTelnetServer
    """

    def __init__(self, slaveFd, serverPid):
        self.slaveFd = slaveFd
        self.serverPid = serverPid


class SavedFile(object):
    def __init__(self, fileno, module, attribute):
        self.fileno = fileno
        self.module = module
        self.attribute = attribute
        self.fileno_saved = None
        self.fileobj_saved = None
        self.fileobj_new = None

    def save(self, newFileno, mode="r"):
        # Save the file object in any case, it may not even have a real
        # underlying descriptor.
        self.fileobj_saved = getattr(self.module, self.attribute)
        # Duplicate the descriptor if possible.
        try:
            self.fileno_saved = os.dup(self.fileno)
        except OSError:
            self.fileno_saved = None
        # Duplicate the new PTY into place and open it as a new object.
        os.dup2(newFileno, self.fileno)
        self.fileobj_new = os.fdopen(self.fileno, mode)
        setattr(self.module, self.attribute, self.fileobj_new)

    def restore(self):
        # First destroy the duplicated PTY object and descriptor
        self.fileobj_new.close()
        self.fileobj_new = None
        # Now restore the original file object
        setattr(self.module, self.attribute, self.fileobj_saved)
        self.fileobj_saved = None
        # And if the descriptor was successfully duplicated earlier, restore
        # it.
        if self.fileno_saved is not None:
            os.dup2(self.fileno_saved, self.fileno)
            try:
                os.close(self.fileno_saved)
            except OSError:
                pass
            self.fileno_saved = None


if __name__ == '__main__':
    print('serving on 8081....')
    t = TelnetServer(('', 8081))
    t.serve_forever()