Ticket #6335: manhole.py

File manhole.py, 9.8 KB (added by Vsevolod Novikov, 7 years ago)

The code inheriting and overriding existent conch manhole classes to provide requested functionality

Line 
1from twisted.internet import reactor
2from twisted.python import log
3
4import codecs
5import datetime
6import copy
7import sys
8
9from twisted.internet.protocol import ServerFactory
10from twisted.conch import manhole, telnet, insults, recvline
11
12class TelnetBootstrapProtocol(telnet.TelnetBootstrapProtocol):
13    def enableRemote(self, opt):
14        # FIX: The sigtrap flag messes up the client processing multibyte utf-8 characters
15        # (particularly russian lowercase 'ya' and some others) and probably other 8-bit
16        # encodings, so the sigtrap is disabled here, instead of the original code.
17        if opt == telnet.LINEMODE:
18            self.transport.requestNegotiation(telnet.LINEMODE, telnet.MODE + chr(0))
19            return True
20        else:
21            return telnet.TelnetBootstrapProtocol.enableRemote(self, opt)
22
23from exceptions import UnicodeDecodeError
24
25class UManholeMixin:
26    decode_buffer = ''
27    input_encoding = 'utf-8'
28    output_encoding = 'utf-8'
29
30    def connectionMade(self):
31        # ENHANCEMENT: adding some number of calls for the command line
32        # to change current encoding easy.
33        #
34        # use it like:
35        # >>> get_input_encoding()
36        # 'utf-8'
37        # >>> set_encoding('cp1251')
38        # >>> get_input_encoding()
39        # 'cp1251'
40        #
41        # TODO: automatically recognize
42        # the client encoding.
43        for n in (
44            'set_input_encoding',
45            'get_input_encoding',
46            'set_output_encoding',
47            'get_output_encoding',
48            'set_encoding',
49        ):
50            self.namespace[n] = getattr(self,n)
51        self.parent.connectionMade(self)
52        # FIX: the Windows TELNET client sends 0x08 byte instead of
53        # 0x7F which is used by the Unix telnet client for BACKSPACE key.
54        #
55        # TODO: it probably might be fixed using client handshaking instead
56        # of processing this code.
57        self.keyHandlers.update({
58            '\x08':self.handle_BACKSPACE,
59        })
60
61    def safe_encoding(self,encoding):
62        try:
63            ''.decode(encoding)
64        except:
65            log.msg('The encoding %s is wrong' % repr(encoding))
66            return 'ascii'
67        return encoding
68
69    def safe_input_encoding(self):
70        return self.safe_encoding(self.input_encoding)
71
72    def safe_output_encoding(self):
73        return self.safe_encoding(self.output_encoding)
74
75    def set_input_encoding(self,encoding):
76        self.input_encoding = self.safe_encoding(encoding)
77
78    def set_output_encoding(self,encoding):
79        self.output_encoding = self.safe_encoding(encoding)
80
81    def set_encoding(self,encoding):
82        self.set_input_encoding(encoding)
83        self.set_output_encoding(encoding)
84
85    def get_input_encoding(self):
86        return self.input_encoding
87
88    def get_output_encoding(self):
89        return self.output_encoding
90
91    def keystrokeReceived(self, keyID, modifier):
92        # uncomment the following to see original codes sent to the function
93        #if not isinstance(keyID,str):
94        #    print 'received keyID:%s' % keyID
95        #else:
96        #    print 'received key code:%s' % ord(keyID)
97        m = self.keyHandlers.get(keyID)
98        if m is not None:
99            m()
100        else:
101            # FIX: processing of multibyte encodings like utf-8
102            # recognizing incomplete sequences by the exception thrown.
103            self.decode_buffer += keyID
104            c = None
105            try:
106                c = self.decode_buffer.decode(self.safe_input_encoding()).encode(self.safe_output_encoding())
107            except UnicodeDecodeError,ex:
108                # The exception here may mean several significantly different circumstations:
109                # either error while recognizing one-byte encoding, or long byte sequence
110                # received incompletely, or error while receiving long byte sequence.
111                # TODO: these circumstances should be differentiated from the codecs module, but
112                # there is no any proper way found in the implemented python library code.
113                enc = self.safe_input_encoding()
114                if not enc.startswith('ut') or len(self.decode_buffer) > 5:
115                    log.msg("Can not handle byte sequence in %s: %s" % (self.safe_input_encoding(),''.join(['%02X' % ord(c) for c in self.decode_buffer])))
116                    self.decode_buffer = ''
117                return
118            self.decode_buffer = ''
119            keyID = c
120            # FIX: the keyID 'character' sent to the following call
121            # may be multibyte in case of multibyte output encoding.
122            # It helps backend code to process current cursor
123            # position properly.
124            #
125            # TODO: ideally the keyID here should be of the unicode type
126            # but it requires a lot of changes in the backend classes.
127            self.characterReceived(keyID, False)
128
129    def lineReceived(self,data):
130        # FIX: when the characterReceived method code sends a data here,
131        # we are ready to change the type of the passed data to unicode
132        # for the proper processing in the interpreter later.
133        self.parent.lineReceived(self,data.decode(self.safe_output_encoding()))
134
135    def getSource(self):
136        # FIX: there are two sources of lines to return - the interpreter with
137        # incomplete statement set, and the current line buffer.
138        #
139        # Just because we have sent unicode for processing in the interpreter before,
140        # it stores seqence of the unicode strings, while the line buffer
141        # of self is filled by the sequence of multybyte characters sent as keyID.
142        #
143        # So we need convert the both sources of lines to the unified form.
144        # Using str with output encoding instead of unicode just because
145        # of the backend code which requires a lot of changes to be unicode-ready.
146        ibuffer = u'\n'.join(self.interpreter.buffer).encode(self.safe_output_encoding())
147        lbuffer = ''.join(self.lineBuffer)
148        return ibuffer + '\n' + lbuffer
149
150    def addOutput(self, data, async = False):
151        # FIX: this method is called by the FileWrapper in case of
152        # print statements and error handling in the evaluated code.
153        #
154        # When the code is passed to the interpreter, the output
155        # stream is always utf-8-encoded for unknown reason. It was checked
156        # experimentally on the Windows platform with cp1251 system encoding.
157        if isinstance(data,str):
158            data = data.decode('utf-8','ignore')
159        # Sometimes the print statement sends unicode instead of the str string
160        # to the output stream. It happens f.e. when the print statement
161        # prints unicode string. So we need to process unicode data
162        # sent to the stream. The str data was converted to the unicode just above.
163        # Explicit unicode convertion below is for safety purposes.
164        data = unicode(data).encode(self.safe_output_encoding(),'backslashreplace')
165        self.parent.addOutput(self, data, async)
166
167    def handle_RETURN(self):
168        # FIX: we can safely put the line buffer to the history directly,
169        # instead of the byte concatenation in the original code.
170        #
171        # It helps the backend code to process current cursor position
172        # properly when the history line is used in case of multibyte
173        # encoding like utf-8
174        if self.lineBuffer:
175            self.historyLines.append(self.lineBuffer)
176        self.historyPosition = len(self.historyLines)
177        return recvline.RecvLine.handle_RETURN(self)
178
179class ColoredManhole(UManholeMixin,manhole.ColoredManhole):
180    parent = manhole.ColoredManhole
181    pass
182
183class Manhole(UManholeMixin,manhole.Manhole):
184    parent = manhole.Manhole
185    pass
186
187class TelnetTransport(telnet.TelnetTransport):
188    # to see what happens just uncomment the following
189    #my_stdin = sys.stdin
190    #my_stdout = sys.stdout
191    #my_stderr = sys.stderr
192    def dataReceived(self,data):
193        # to see what happens just uncomment the following
194        #print >>self.my_stdout,"RECEIVED DATA:%s" % ' '.join(['%02X' % ord(c) for c in data])
195        #print >>self.my_stdout,"RECEIVED CHAR:%s" % ' '.join(['%2s' % (c if ord(c) >= 32 and ord(c) < 128 else '?') for c in data])
196        telnet.TelnetTransport.dataReceived(self,data)
197        # FIX: The Windows TELNET implementation sends '\r' alone instead of '\r\00' sequence when
198        # the RETURN key is pressed. Here is the 'overwrite' fix to make the backend code
199        # RETURN key processing bug even.
200        if self.state == 'newline':
201            self.applicationDataReceived('\r')
202            self.state = 'data'
203
204    def write(self,data):
205        # to see what happens just uncomment the following
206        #print >>self.my_stdout,"WRITE DATA:%s" % ' '.join(['%02X' % ord(c) for c in data])
207        #print >>self.my_stdout,"WRITE CHAR:%s" % ' '.join(['%2s' % (c if ord(c) >= 32 and ord(c) < 128 else '?') for c in data])
208        return telnet.TelnetTransport.write(self,data)
209
210if __name__ == '__main__':
211    # The code below creates unsafe telnet server (without authentication)
212    # providing manhole interface on the 23457 port. It can be used for testing purposes,
213    # when this module is called directly.
214    #
215    # You can safely import the module to get fixed manhole classes for your own.
216    import logging
217
218    class CMDFactory(ServerFactory):
219        def __init__(self,colored=True):
220            self.colored = colored
221
222        def buildProtocol(self,addr):
223            namespace = {}
224            manhole = ColoredManhole if self.colored else Manhole
225            return TelnetTransport(TelnetBootstrapProtocol,insults.insults.ServerProtocol,manhole,namespace)
226
227    logging.basicConfig(level=logging.DEBUG)
228    observer = log.PythonLoggingObserver()
229    observer.start()
230    cmdport = 23457
231
232    namespace = {}
233
234    cmdlistener = reactor.listenTCP(cmdport, CMDFactory(bool('colored' in sys.argv)))
235
236    reactor.run()