#!/usr/bin/python
# vim: expandtab ts=4 sw=4 sts=4:
# -*- coding: utf-8 -*-

# Parts of this code have been taken from Pyserial project (http://pyserial.sourceforge.net/) under Python license

import sys
import os
import string
import time
import threading
import socket
import select

if sys.platform.startswith("win"):
    import win32pipe
    import win32file

class PipeProxy(threading.Thread):

    def __init__(self, name, pipe, host, port):
        self.devname = name
        self.pipe = pipe
        self.host = host
        self.port = port
        self.server = None
        self.reader_thread = None
        self.use_thread = False
        self._write_lock = threading.Lock()
        self.clients = {}
        self.timeout = 0.1
        self.alive = True

        if sys.platform.startswith("win"):
            # we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select()
            self.use_thread = True

        try:
            if self.host.__contains__(':'):
                # IPv6 address support
                self.server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
            else:
                self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                self.server.bind((self.host, int(self.port)))
                self.server.listen(5)
        except socket.error, msg:
            self.error("unable to create the socket server %s" % msg)
            return

        threading.Thread.__init__(self)
        self.debug("initialized, waiting for clients on %s:%i..." % (self.host, self.port))

    def error(self, msg):

        sys.stderr.write("ERROR -> %s PIPE PROXY: %s\n" % (self.devname, msg))

    def debug(self, msg):

        sys.stdout.write("INFO -> %s PIPE PROXY: %s\n" % (self.devname, msg))

    def run(self):

        while True:

            recv_list = [self.server.fileno()]

            if not self.use_thread:
                recv_list.append(self.pipe.fileno())

            for client in self.clients.values():
                if client.active:
                    recv_list.append(client.fileno)
                else:
                    self.debug("lost client %s" % client.addrport())
                    try:
                        client.sock.close()
                    except:
                        pass
                    del self.clients[client.fileno]

            try:
                rlist, slist, elist = select.select(recv_list, [], [], self.timeout)
            except select.error, err:
                self.error("fatal select error %d:%s" % (err[0], err[1]))
                return False

            if not self.alive:
                self.debug('Exiting ...')
                return True

            for sock_fileno in rlist:
                if sock_fileno == self.server.fileno():

                    try:
                        sock, addr = self.server.accept()
                        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                        self.debug("new client %s:%s" % (addr[0], addr[1]))
                    except socket.error, err:
                        self.error("accept error %d:%s" % (err[0], err[1]))
                        continue

                    new_client = TelnetClient(sock, addr)
                    self.clients[new_client.fileno] = new_client
                    sock.send("%s console is now available ... Press RETURN to get started.\r\n" % self.devname)

                    if self.use_thread and not self.reader_thread:
                        self.reader_thread = threading.Thread(target=self.reader)
                        self.reader_thread.setDaemon(True)
                        self.reader_thread.setName('pipe->socket')
                        self.reader_thread.start()

                elif not self.use_thread and sock_fileno == self.pipe.fileno():

                    data = self.read_from_pipe()
                    if not data:
                        self.debug("pipe has been closed!")
                        return False
                    for client in self.clients.values():
                        try:
                            client.send(data)
                        except:
                            self.debug(msg)
                            client.deactivate()
                elif sock_fileno in self.clients:
                    try:
                        data = self.clients[sock_fileno].socket_recv()

                        # For some reason, windows likes to send "cr/lf" when you send a "cr".
                        # Strip that so we don't get a double prompt.
                        data = string.replace(data, chr(13) + chr(10), chr(13))

                        self.write_to_pipe(data)
                    except Exception, msg:
                        self.debug(msg)
                        self.clients[sock_fileno].deactivate()

    def write_to_pipe(self, data):

        if sys.platform.startswith('win'):
            win32file.WriteFile(self.pipe, data)
        else:
            self.pipe.sendall(data)

    def read_from_pipe(self):

        if sys.platform.startswith('win'):
            (read, num_avail, num_message) = win32pipe.PeekNamedPipe(self.pipe, 0)
            if num_avail > 0:
                (error_code, output) = win32file.ReadFile(self.pipe, num_avail, None)
                return output
            return ""
        else:
            return self.pipe.recv(1024)

    def reader(self):
        """loop forever and copy pipe->socket"""

        self.debug("reader thread started")
        while self.alive:
            try:
                data = self.read_from_pipe()
                if not data and not sys.platform.startswith('win'):
                    self.debug("pipe has been closed!")
                    break
                self._write_lock.acquire()
                try:
                    for client in self.clients.values():
                        client.send(data)
                finally:
                    self._write_lock.release()
                if sys.platform.startswith('win'):
                    # sleep every 10 ms
                    time.sleep(0.01)
            except:
                self.debug("pipe has been closed!")
                break
        self.debug("reader thread exited")
        self.stop()

    def stop(self):
        """Stop copying"""

        if self.alive:
            self.alive = False
        for client in self.clients.values():
            client.sock.close()
            client.deactivate()

# all Python versions prior 3.x convert str([17]) to '[17]' instead of '\x11'
# so a simple bytes(sequence) doesn't work for all versions
def to_bytes(seq):
    """convert a sequence to a bytes type"""
    b = bytearray()
    for item in seq:
        b.append(item)  # this one handles int and str
    return bytes(b)

# telnet protocol characters
IAC  = to_bytes([255]) # Interpret As Command
DONT = to_bytes([254])
DO   = to_bytes([253])
WONT = to_bytes([252])
WILL = to_bytes([251])
IAC_DOUBLED = to_bytes([IAC, IAC])

SE  = to_bytes([240])  # Subnegotiation End
NOP = to_bytes([241])  # No Operation
DM  = to_bytes([242])  # Data Mark
BRK = to_bytes([243])  # Break
IP  = to_bytes([244])  # Interrupt process
AO  = to_bytes([245])  # Abort output
AYT = to_bytes([246])  # Are You There
EC  = to_bytes([247])  # Erase Character
EL  = to_bytes([248])  # Erase Line
GA  = to_bytes([249])  # Go Ahead
SB =  to_bytes([250])  # Subnegotiation Begin

# selected telnet options
ECHO = to_bytes([1])   # echo
SGA = to_bytes([3])    # suppress go ahead
LINEMODE = to_bytes([34]) # line mode
TERMTYPE = to_bytes([24]) # terminal type

# Telnet filter states
M_NORMAL = 0
M_IAC_SEEN = 1
M_NEGOTIATE = 2

# TelnetOption and TelnetSubnegotiation states
REQUESTED = 'REQUESTED'
ACTIVE = 'ACTIVE'
INACTIVE = 'INACTIVE'
REALLY_INACTIVE = 'REALLY_INACTIVE'

class TelnetOption(object):
    """Manage a single telnet option, keeps track of DO/DONT WILL/WONT."""

    def __init__(self, connection, name, option, send_yes, send_no, ack_yes, ack_no, initial_state, activation_callback=None):
        """Init option.
        :param connection: connection used to transmit answers
        :param name: a readable name for debug outputs
        :param send_yes: what to send when option is to be enabled.
        :param send_no: what to send when option is to be disabled.
        :param ack_yes: what to expect when remote agrees on option.
        :param ack_no: what to expect when remote disagrees on option.
        :param initial_state: options initialized with REQUESTED are tried to
            be enabled on startup. use INACTIVE for all others.
        """
        self.connection = connection
        self.name = name
        self.option = option
        self.send_yes = send_yes
        self.send_no = send_no
        self.ack_yes = ack_yes
        self.ack_no = ack_no
        self.state = initial_state
        self.active = False
        self.activation_callback = activation_callback

    def __repr__(self):
        """String for debug outputs"""
        return "%s:%s(%s)" % (self.name, self.active, self.state)

    def process_incoming(self, command):
        """A DO/DONT/WILL/WONT was received for this option, update state and
        answer when needed."""
        if command == self.ack_yes:
            if self.state is REQUESTED:
                self.state = ACTIVE
                self.active = True
                if self.activation_callback is not None:
                    self.activation_callback()
            elif self.state is ACTIVE:
                pass
            elif self.state is INACTIVE:
                self.state = ACTIVE
                self.connection.telnetSendOption(self.send_yes, self.option)
                self.active = True
                if self.activation_callback is not None:
                    self.activation_callback()
            elif self.state is REALLY_INACTIVE:
                self.connection.telnetSendOption(self.send_no, self.option)
            else:
                raise ValueError('option in illegal state %r' % self)
        elif command == self.ack_no:
            if self.state is REQUESTED:
                self.state = INACTIVE
                self.active = False
            elif self.state is ACTIVE:
                self.state = INACTIVE
                self.connection.telnetSendOption(self.send_no, self.option)
                self.active = False
            elif self.state is INACTIVE:
                pass
            elif self.state is REALLY_INACTIVE:
                pass
            else:
                raise ValueError('option in illegal state %r' % self)

class TelnetClient(object):

    """
    Represents a client connection via Telnet.

    First argument is the socket discovered by the Telnet Server.
    Second argument is the tuple (ip address, port number).
    """

    def __init__(self, sock, addr_tup):
        self.active = True          # Turns False when the connection is lost
        self.sock = sock            # The connection's socket
        self.fileno = sock.fileno() # The socket's file descriptor
        self.address = addr_tup[0]  # The client's remote TCP/IP address
        self.port = addr_tup[1]     # The client's remote port

        # filter state machine
        self.mode = M_NORMAL
        self.suboption = None
        self.telnet_command = None

        # all supported telnet options
        self._telnet_options = [
            TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED),
            TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
            TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE),
            TelnetOption(self, 'LINEMODE', LINEMODE, DONT, DONT, WILL, WONT, REQUESTED),
            TelnetOption(self, 'TERMTYPE', TERMTYPE, DO, DONT, WILL, WONT, REQUESTED),
            ]

        for option in self._telnet_options:
            if option.state is REQUESTED:
                self.telnetSendOption(option.send_yes, option.option)

    def telnetSendOption(self, action, option):
        """Send DO, DONT, WILL, WONT."""
        self.sock.sendall(to_bytes([IAC, action, option]))

    def escape(self, data):
        """ All outgoing data has to be properly escaped, so that no IAC character 
        in the data stream messes up the Telnet state machine in the server.
        """
        for byte in data:
            if byte == IAC:
                yield IAC
                yield IAC
            else:
                yield byte

    def filter(self, data):
        """ handle a bunch of incoming bytes. this is a generator. it will yield
        all characters not of interest for Telnet
        """
        for byte in data:
            if self.mode == M_NORMAL:
                # interpret as command or as data
                if byte == IAC:
                    self.mode = M_IAC_SEEN
                else:
                    # store data in sub option buffer or pass it to our
                    # consumer depending on state
                    if self.suboption is not None:
                        self.suboption.append(byte)
                    else:
                        yield byte
            elif self.mode == M_IAC_SEEN:
                if byte == IAC:
                    # interpret as command doubled -> insert character
                    # itself
                    if self.suboption is not None:
                        self.suboption.append(byte)
                    else:
                        yield byte
                    self.mode = M_NORMAL
                elif byte == SB:
                    # sub option start
                    self.suboption = bytearray()
                    self.mode = M_NORMAL
                elif byte == SE:
                    # sub option end -> process it now
                    #self._telnetProcessSubnegotiation(bytes(self.suboption))
                    self.suboption = None
                    self.mode = M_NORMAL
                elif byte in (DO, DONT, WILL, WONT):
                    # negotiation
                    self.telnet_command = byte
                    self.mode = M_NEGOTIATE
                else:
                    # other telnet commands are ignored!
                    self.mode = M_NORMAL
            elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
                self._telnetNegotiateOption(self.telnet_command, byte)
                self.mode = M_NORMAL

    def _telnetNegotiateOption(self, command, option):
        """Process incoming DO, DONT, WILL, WONT."""
        # check our registered telnet options and forward command to them
        # they know themselves if they have to answer or not
        known = False
        for item in self._telnet_options:
            # can have more than one match! as some options are duplicated for
            # 'us' and 'them'
            if item.option == option:
                item.process_incoming(command)
                known = True
        if not known:
            # handle unknown options
            # only answer to positive requests and deny them
            if command == WILL or command == DO:
                self.telnetSendOption((command == WILL and DONT or WONT), option)

    def send(self, data):
        """
        Send data to the distant end.
        """

        try:
            self.sock.sendall(to_bytes(self.escape(data)))
        except socket.error, err:
            self.active = False
            raise Exception, "socket.sendall() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport())

    def deactivate(self):
        """
        Set the client to disconnect on the next server poll.
        """
        self.active = False

    def addrport(self):
        """
        Return the DE's IP address and port number as a string.
        """
        return "%s:%s" % (self.address, self.port)

    def socket_recv(self):
        """
        Called by TelnetServer when recv data is ready.
        """
        try:
            data = self.sock.recv(4096)
        except socket.error, ex:
            raise Exception, "socket.recv() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport())

        ## Did they close the connection?
        size = len(data)
        if size == 0:
            raise Exception, "connection closed by %s" % self.addrport()

        return to_bytes(self.filter(data))

if __name__ == '__main__':

    if sys.platform.startswith('win'):
        import msvcrt
        pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_3.8.2'
        pipe = open(pipe_name, 'a+b')
        pipe_proxy = PipeProxy("VBOX", msvcrt.get_osfhandle(pipe.fileno()), '127.0.0.1', 3900)
    else:
        try:
            unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            #unix_socket.settimeout(0.1)
            unix_socket.connect("/tmp/pipe_test")
        except socket.error, err:
            print "Socket error -> %d:%s" % (err[0], err[1])
            sys.exit(False)
        pipe_proxy = PipeProxy('VBOX', unix_socket, '127.0.0.1', 3900)

    pipe_proxy.setDaemon(True)
    pipe_proxy.start()
    pipe.proxy.stop()
    pipe_proxy.join()
