├── .gitignore
├── .project
├── .pydevproject
├── COPYING.txt
├── MANIFEST.in
├── README.rst
├── setup.py
├── telnetsrv
├── __init__.py
├── evtlet.py
├── green.py
├── paramiko_ssh.py
├── telnetsrvlib.py
├── test_rsa.key
└── threaded.py
└── test.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 |
3 | # Packages
4 | *.egg
5 | *.egg-info
6 | dist
7 | build
8 | eggs
9 | parts
10 | bin
11 | var
12 | sdist
13 | develop-eggs
14 | .installed.cfg
15 |
16 | # Installer logs
17 | pip-log.txt
18 |
19 | # Unit test / coverage reports
20 | .coverage
21 | .tox
22 |
23 | #Translations
24 | *.mo
25 |
26 | #Mr Developer
27 | .mr.developer.cfg
28 |
29 | .DS_Store
30 |
31 | MANIFEST
32 |
33 | telnetsrv/server_rsa.key
34 |
35 | server_rsa.key
36 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | telnetsrv
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | python 2.7
6 | Default
7 |
8 |
--------------------------------------------------------------------------------
/COPYING.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include COPYING.txt
3 |
4 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | telnetsrvlib
2 | ============
3 |
4 | Telnet server using gevent or threading.
5 |
6 | Copied from http://pytelnetsrvlib.sourceforge.net/
7 | and modified to support gevent and eventlet, better input handling, clean asynchronous messages and much more.
8 | Licensed under the LGPL, as per the SourceForge notes.
9 |
10 | This library allows you to easily create a Telnet or SSH server powered by your Python code.
11 | The library negotiates with a Telnet client, parses commands, provides an automated
12 | help command, optionally provides login queries, then allows you to define your own
13 | commands. An optional SSH handler is provided to wrap the defined Telnet handler into
14 | an SSH handler.
15 |
16 | You use the library to create your own handler, then pass that handler to a StreamServer
17 | or TCPServer to perform the actual connection tasks.
18 |
19 | This library includes two flavors of the server handler, one uses separate threads,
20 | the other uses greenlets (green pseudo-threads) via gevent or eventlet.
21 |
22 | The threaded version uses a separate thread to process the input buffer and
23 | semaphores reading and writing. The provided test server only handles a single
24 | connection at a time.
25 |
26 | The green version moves the input buffer processing into a greenlet to allow
27 | cooperative multi-processing. This results in significantly less memory usage
28 | and nearly no idle processing. The provided test server handles a large number of connections.
29 |
30 |
31 | Install
32 | -------
33 |
34 | telnetsrv is available through the Cheeseshop. You can use easy_install or pip to perform the installation.
35 |
36 | ::
37 |
38 | easy_install telnetsrv
39 |
40 | or
41 |
42 | ::
43 |
44 | pip install telnetsrv
45 |
46 | Note that there are no dependancies defined, but if you want to use the green version, you must also install gevent or eventlet.
47 | If you wish to use the SSH server, you must also install paramiko.
48 |
49 | To Use
50 | ------
51 |
52 | Import the ``TelnetHandler`` base class and ``command`` function decorator from either the green class, evtlet class or threaded class,
53 | then subclass ``TelnetHandler`` to add your own commands which are methods decorated with ``@command``.
54 |
55 | Threaded
56 | ++++++++
57 |
58 | .. code:: python
59 |
60 | from telnetsrv.threaded import TelnetHandler, command
61 | class MyHandler(TelnetHandler):
62 | ...
63 |
64 | Green
65 | +++++
66 |
67 | .. code:: python
68 |
69 | from telnetsrv.green import TelnetHandler, command
70 | class MyHandler(TelnetHandler):
71 | ...
72 |
73 | Eventlet
74 | ++++++++
75 |
76 | .. code:: python
77 |
78 | from telnetsrv.evtlet import TelnetHandler, command
79 | class MyHandler(TelnetHandler):
80 | ...
81 |
82 | Adding Commands
83 | ---------------
84 |
85 | Commands can be defined by using the ``command`` function decorator.
86 |
87 | .. code:: python
88 |
89 | @command('echo')
90 | def command_echo(self, params):
91 | ...
92 |
93 | Old Style
94 | +++++++++
95 |
96 | Commands can also be defined by prefixing any method with "cmd". For example,
97 | this also creates an ``echo`` command:
98 |
99 | .. code:: python
100 |
101 | def cmdECHO(self, params):
102 | ...
103 |
104 | *This method is less flexible and may not be supported in future versions.*
105 |
106 | Command Parameters
107 | ++++++++++++++++++
108 |
109 | Any command parameters will be passed to this function automatically. The parameters are
110 | contained in a list. The user input is parsed similar to the way Bash parses text: space delimited,
111 | quoted parameters are kept together and default behavior can be modified with the ``\`` character.
112 | If you need to access the raw text input, inspect the self.input.raw variable.
113 |
114 | ::
115 |
116 | Telnet Server> echo 1 "2 3"
117 |
118 | .. code:: python
119 |
120 | params == ['1', '2 3']
121 | self.input.raw == 'echo 1 "2 3"\n'
122 |
123 | ::
124 |
125 | Telnet Server> echo 1 \
126 | ... 2 "3
127 | ... 4" "5\
128 | ... 6"
129 |
130 | .. code:: python
131 |
132 | params == ['1', '2', '3\n4', '56']
133 |
134 | ::
135 |
136 | Telnet Server> echo 1\ 2
137 |
138 | .. code:: python
139 |
140 | params == ['1 2']
141 |
142 | Command Help Text
143 | +++++++++++++++++
144 |
145 | The command's docstring is used for generating the console help information, and must be formatted
146 | with at least 3 lines:
147 |
148 | - Line 0: Command parameter(s) if any. (Can be blank line)
149 | - Line 1: Short descriptive text. (Mandatory)
150 | - Line 2+: Long descriptive text. (Can be blank line)
151 |
152 | If there is no line 2, line 1 will be used for the long description as well.
153 |
154 | .. code:: python
155 |
156 | @command('echo')
157 | def command_echo(self, params):
158 | '''
159 | Echo text back to the console.
160 | This command simply echos the provided text
161 | back to the console.
162 | '''
163 | pass
164 |
165 |
166 | ::
167 |
168 | Telnet Server> help
169 | ? [] - Display help
170 | BYE - Exit the command shell
171 | ECHO - Echo text back to the console.
172 | ...
173 |
174 |
175 | Telnet Server> help echo
176 | ECHO
177 |
178 | This command simply echos the provided text
179 | back to the console.
180 | Telnet Server>
181 |
182 |
183 | Command Aliases
184 | +++++++++++++++
185 |
186 | To create an alias for the new command, set the method's name to a list:
187 |
188 | .. code:: python
189 |
190 | @command(['echo', 'copy'])
191 | def command_echo(self, params):
192 | ...
193 |
194 | The decorator may be stacked, which adds each list to the aliases:
195 |
196 | .. code:: python
197 |
198 | @command('echo')
199 | @command(['copy', 'repeat'])
200 | @command('ditto')
201 | def command_echo(self, params):
202 | ...
203 |
204 |
205 |
206 | Hidden Commands
207 | +++++++++++++++
208 |
209 | To hide the command (and any alias for that command) from the help text output, pass in hidden=True to the decorator:
210 |
211 | .. code:: python
212 |
213 | @command('echo', hidden=True)
214 | def command_echo(self, params):
215 | ...
216 |
217 | The command will not show when the user invokes ``help`` by itself, but the detailed help text will show if
218 | the user invokes ``help echo``.
219 |
220 | When stacking decorators, any one of the stack may define the hidden parameter to hide the command.
221 |
222 | Console Information
223 | -------------------
224 |
225 | These will be provided for inspection.
226 |
227 | ``TERM``
228 | String ID describing the currently connected terminal
229 |
230 | ``WIDTH``
231 | Integer describing the width of the terminal at connection time.
232 |
233 | ``HEIGHT``
234 | Integer describing the height of the terminal at connection time.
235 |
236 | ``username``
237 | Set after authentication succeeds, name of the logged in user.
238 | If no authentication was requested, will be ``None``.
239 |
240 | ``history``
241 | List containing the command history. This can be manipulated directly.
242 |
243 |
244 | .. code:: python
245 |
246 | @command('info')
247 | def command_info(self, params):
248 | '''
249 | Provides some information about the current terminal.
250 | '''
251 | self.writeresponse( "Username: %s, terminal type: %s" % (self.username, self.TERM) )
252 | self.writeresponse( "Width: %s, height: %s" % (self.WIDTH, self.HEIGHT) )
253 | self.writeresponse( "Command history:" )
254 | for c in self.history:
255 | self.writeresponse(" %r" % c)
256 |
257 |
258 | Console Communication
259 | ---------------------
260 |
261 | Send Text to the Client
262 | +++++++++++++++++++++++
263 |
264 | Lower level functions:
265 |
266 | ``self.writeline( TEXT )``
267 |
268 | ``self.write( TEXT )``
269 |
270 | Higher level functions:
271 |
272 | ``self.writemessage( TEXT )`` - for clean, asynchronous writing. Any interrupted input is rebuilt.
273 |
274 | ``self.writeresponse( TEXT )`` - to emit a line of expected output
275 |
276 | ``self.writeerror( TEXT )`` - to emit error messages
277 |
278 | The ``writemessage`` method is intended to send messages to the console without
279 | interrupting any current input. If the user has entered text at the prompt,
280 | the prompt and text will be seamlessly regenerated following the message.
281 | It is ideal for asynchronous messages that aren't generated from the direct user input.
282 |
283 | Receive Text from the Client
284 | ++++++++++++++++++++++++++++
285 |
286 | ``self.readline( prompt=TEXT )``
287 |
288 | Setting the prompt is important to recreate the user input following a ``writemessage``
289 | interruption.
290 |
291 | When requesting sensitive information from the user (such as requesting a new password) the input should
292 | not be shown nor should the input line be written to the command history. ``readline`` accepts
293 | two optional parameters to control this, ``echo`` and ``use_history``.
294 |
295 | ``self.readline( prompt=TEXT, echo=False, use_history=False )``
296 |
297 | When ``echo`` is set to False, the input will not echo back to the user. When ``use_history`` is set
298 | to False, the user will not have access to the command history (up arrow) nor will the entered data
299 | be stored in the command history.
300 |
301 | Handler Options
302 | ---------------
303 |
304 | Override these class members to change the handler's behavior.
305 |
306 | ``PROMPT``
307 | Default: ``"Telnet Server> "``
308 |
309 | ``CONTINUE_PROMPT``
310 | Default: ``"... "``
311 |
312 | ``WELCOME``
313 | Displayed after a successful connection, after the username/password is accepted, if configured.
314 |
315 | Default: ``"You have connected to the telnet server."``
316 |
317 | ``session_start(self)``
318 | Called after the ``WELCOME`` text is displayed.
319 |
320 | Default: pass
321 |
322 | ``session_end(self)``
323 | Called after the console is disconnected.
324 |
325 | Default: pass
326 |
327 | ``authCallback(self, username, password)``
328 | Reference to authentication function. If
329 | this is not defined, no username or password is requested. Should
330 | raise an exception if authentication fails
331 |
332 | Default: None
333 |
334 | ``authNeedUser``
335 | Should a username be requested?
336 |
337 | Default: ``False``
338 |
339 | ``authNeedPass``
340 | Should a password be requested?
341 |
342 | Default: ``False``
343 |
344 |
345 | Handler Display Modification
346 | ----------------------------
347 |
348 | If you want to change how the output is displayed, override one or all of the
349 | write classes. Make sure you call back to the base class when doing so.
350 | This is a good way to provide color to your console by using ANSI color commands.
351 | See http://en.wikipedia.org/wiki/ANSI_escape_code
352 |
353 | - writemessage( TEXT )
354 | - writeresponse( TEXT )
355 | - writeerror( TEXT )
356 |
357 | .. code:: python
358 |
359 | def writeerror(self, text):
360 | '''Write errors in red'''
361 | TelnetHandler.writeerror(self, "\x1b[91m%s\x1b[0m" % text )
362 |
363 | Serving the Handler
364 | -------------------
365 |
366 | Now you have a shiny new handler class, but it doesn't serve itself - it must be called
367 | from an appropriate server. The server will create an instance of the TelnetHandler class
368 | for each new connection. The handler class will work with either a gevent StreamServer instance
369 | (for the green version) or with a SocketServer.TCPServer instance (for the threaded version).
370 |
371 | Threaded
372 | ++++++++
373 |
374 | .. code:: python
375 |
376 | import SocketServer
377 | class TelnetServer(SocketServer.TCPServer):
378 | allow_reuse_address = True
379 |
380 | server = TelnetServer(("0.0.0.0", 8023), MyHandler)
381 | server.serve_forever()
382 |
383 | Green
384 | +++++
385 |
386 | The TelnetHandler class includes a streamserver_handle class method to translate the
387 | required fields from a StreamServer, allowing use with the gevent StreamServer (and possibly
388 | others).
389 |
390 | .. code:: python
391 |
392 | import gevent.server
393 | server = gevent.server.StreamServer(("", 8023), MyHandler.streamserver_handle)
394 | server.serve_forever()
395 |
396 |
397 | Short Example
398 | -------------
399 |
400 | .. code:: python
401 |
402 | import gevent, gevent.server
403 | from telnetsrv.green import TelnetHandler, command
404 |
405 | class MyTelnetHandler(TelnetHandler):
406 | WELCOME = "Welcome to my server."
407 |
408 | @command(['echo', 'copy', 'repeat'])
409 | def command_echo(self, params):
410 | '''
411 | Echo text back to the console.
412 |
413 | '''
414 | self.writeresponse( ' '.join(params) )
415 |
416 | @command('timer')
417 | def command_timer(self, params):
418 | '''
419 | In seconds, display .
420 | Send a message after a delay.
421 | is in seconds.
422 | If is more than one word, quotes are required.
423 | example:
424 | > TIMER 5 "hello world!"
425 | '''
426 | try:
427 | timestr, message = params[:2]
428 | time = int(timestr)
429 | except ValueError:
430 | self.writeerror( "Need both a time and a message" )
431 | return
432 | self.writeresponse("Waiting %d seconds...", time)
433 | gevent.spawn_later(time, self.writemessage, message)
434 |
435 |
436 | server = gevent.server.StreamServer(("", 8023), MyTelnetHandler.streamserver_handle)
437 | server.serve_forever()
438 |
439 |
440 | SSH
441 | ---
442 |
443 | If the paramiko library is installed, the TelnetHanlder can be used via an SSH server for significantly
444 | improved security. ``paramiko_ssh`` contains ``SSHHandler`` and ``getRsaKeyFile`` to make setting
445 | up the server trivial. Since the authentication is done prior to invoking the TelnetHandler,
446 | any ``authCallback`` defined in the TelnetHandler is ignored.
447 |
448 | Green
449 | +++++
450 |
451 | If using the green version of the TelnetHandler, you must use Gevent's monkey patch_all prior to
452 | importing from ``paramiko_ssh``.
453 |
454 | .. code:: python
455 |
456 | from gevent import monkey; monkey.patch_all()
457 | from telnetsrv.paramiko_ssh import SSHHandler, getRsaKeyFile
458 |
459 | Eventlet
460 | ++++++++
461 |
462 | If using the eventlet version of the TelnetHandler, you must use Eventlet's monkey patch_all prior to
463 | importing from ``paramiko_ssh``.
464 |
465 | .. code:: python
466 |
467 | import eventlet; eventlet.monkey_patch(all=True)
468 | from telnetsrv.paramiko_ssh import SSHHandler, getRsaKeyFile
469 |
470 |
471 |
472 | Operation Overview
473 | ++++++++++++++++++
474 |
475 | The SocketServer/StreamServer sets up the socket then passes that to an SSHHandler class which
476 | authenticates then starts the SSH transport. Within the SSH transport, the client requests a PTY channel
477 | (and possibly other channel types, which are denied) and the SSHHandler sets up a TelnetHandler class
478 | as the PTY for the channel. If the client never requests a PTY channel, the transport will disconnect
479 | after a timeout.
480 |
481 | SSH Host Key
482 | ++++++++++++
483 |
484 | To thwart man-in-the-middle attacks, every SSH server provides an RSA key as a unique fingerprint. This unique key
485 | should never change, and should be stored in a local file or a database. The ``getRsaKeyFile`` makes this
486 | easy by reading the given key file if it exists, or creating the key if it does not. The result should be
487 | read once and set in the class definition.
488 |
489 | Easy way:
490 |
491 | ``host_key = getRsaKeyFile( FILENAME )``
492 | If the FILENAME can be read, the RSA key is read in and returned as an RSAKey object.
493 | If the file can't be read, it generates a new RSA key and stores it in that file.
494 |
495 | Long way:
496 |
497 | .. code:: python
498 |
499 | from paramiko_ssh import RSAKey
500 |
501 | # Make a new key - should only be done once per server during setup
502 | new_key = RSAKey.generate(1024)
503 | save_to_my_database( 'server_fingerprint', str(new_key) )
504 |
505 | ...
506 |
507 | host_key = RSAKey( data=get_from_my_database('server_fingerprint') )
508 |
509 |
510 | SSH Authentication
511 | ++++++++++++++++++
512 |
513 | Users can authenticate with just a username, a username/publickey or a username/password. Up to three callbacks
514 | can be defined, and if all three are defined, all three will be tried before denying the authentication attempt.
515 | An SSH client will always provide a username. If no ``authCallbackXX`` is defined, the SSH authentication will be
516 | set to "none" and any username will be able to log in.
517 |
518 | ``authCallbackUsername(self, username)``
519 | Reference to username-only authentication function. Define this function to permit specific usernames
520 | to log in without any futher authentication. Raise any exception to deny this authentication attempt.
521 |
522 | If defined, this is always tried first.
523 |
524 | Default: None
525 |
526 | ``authCallbackKey(self, username, key)``
527 | Reference to username/key authentication function. If this is defined,
528 | users can log in the SSH client automatically with a key. Raise any exception to deny this authentication attempt.
529 |
530 | Default: None
531 |
532 | ``authCallback(self, username, password)``
533 | Reference to username/password authentication function. If
534 | this is defined, a password is requested. Raise any exception to deny this authentication attempt.
535 |
536 | If defined, this is always tried last.
537 |
538 | Default: None
539 |
540 |
541 | SSHHandler uses Paramiko's ServerInterface as one of its base classes. If you are familiar with Paramiko, feel free
542 | to instead override the authentication callbacks as needed.
543 |
544 |
545 | Short SSH Example
546 | +++++++++++++++++
547 |
548 | .. code:: python
549 |
550 | from gevent import monkey; monkey.patch_all()
551 | import gevent.server
552 | from telnetsrv.paramiko_ssh import SSHHandler, getRsaKeyFile
553 | from telnetsrv.green import TelnetHandler, command
554 |
555 | class MyTelnetHandler(TelnetHandler):
556 | WELCOME = "Welcome to my server."
557 |
558 | @command(['echo', 'copy', 'repeat'])
559 | def command_echo(self, params):
560 | '''
561 | Echo text back to the console.
562 |
563 | '''
564 | self.writeresponse( ' '.join(params) )
565 |
566 | class MySSHHandler(SSHHandler):
567 | # Set the unique host key
568 | host_key = getRsaKeyFile('server_fingerprint.key')
569 |
570 | # Instruct this SSH handler to use MyTelnetHandler for any PTY connections
571 | telnet_handler = MyTelnetHandler
572 |
573 | def authCallbackUsername(self, username):
574 | # These users do not require a password
575 | if username not in ['john', 'eric', 'terry', 'graham']:
576 | raise RuntimeError('Not a Python!')
577 |
578 | def authCallback(self, username, password):
579 | # Super secret password:
580 | if password != 'concord':
581 | raise RuntimeError('Wrong password!')
582 |
583 | # Start a telnet server for just the localhost on port 8023. (Will not request any authentication.)
584 | telnetserver = gevent.server.StreamServer(('127.0.0.1', 8023), MyTelnetHandler.streamserver_handle)
585 | telnetserver.start()
586 |
587 | # Start an SSH server for any local or remote host on port 8022
588 | sshserver = gevent.server.StreamServer(("", 8022), MySSHHandler.streamserver_handle)
589 | sshserver.serve_forever()
590 |
591 |
592 | Longer Example
593 | --------------
594 |
595 | See https://github.com/ianepperson/telnetsrvlib/blob/master/test.py
596 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup
2 |
3 | def readme():
4 | with open('README.rst') as f:
5 | return f.read()
6 |
7 | setup(
8 | name = "telnetsrv",
9 | packages = ["telnetsrv"],
10 | version = "0.4",
11 | extras_require = {
12 | 'green': ['gevent'],
13 | 'ssh': ['paramiko'],
14 | },
15 | description = "Telnet server handler library",
16 | long_description = readme(),
17 | author = "Ian Epperson",
18 | author_email = "ian@epperson.com",
19 | url = "https://github.com/ianepperson/telnetsrvlib",
20 | keywords = ["gevent", "telnet", "server"],
21 | classifiers = [
22 | "Programming Language :: Python",
23 | "Development Status :: 4 - Beta",
24 | "Environment :: Other Environment",
25 | "Intended Audience :: Developers",
26 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
27 | "Operating System :: OS Independent",
28 | "Topic :: Software Development :: Libraries :: Python Modules",
29 | "Topic :: Communications",
30 | "Topic :: Communications :: BBS",
31 | "Topic :: System :: Shells",
32 | "Topic :: Terminals",
33 | "Topic :: Terminals :: Telnet",
34 | ],
35 | )
36 |
--------------------------------------------------------------------------------
/telnetsrv/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = []
--------------------------------------------------------------------------------
/telnetsrv/evtlet.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Telnet handler concrete class using green threads with eventlet
3 |
4 | import eventlet
5 |
6 | from telnetsrvlib import TelnetHandlerBase, command
7 |
8 | class TelnetHandler(TelnetHandlerBase):
9 | "A telnet server handler using Gevent"
10 | def __init__(self, request, client_address, server):
11 | # Create a green queue for input handling
12 | self.cookedq = eventlet.queue.Queue()
13 | # Call the base class init method
14 | TelnetHandlerBase.__init__(self, request, client_address, server)
15 |
16 | def setup(self):
17 | '''Called after instantiation'''
18 | TelnetHandlerBase.setup(self)
19 | # Spawn a greenlet to handle socket input
20 | self.greenlet_ic = eventlet.spawn(self.inputcooker)
21 | # Note that inputcooker exits on EOF
22 |
23 | # Sleep for 0.5 second to allow options negotiation
24 | eventlet.sleep(0.5)
25 |
26 | def finish(self):
27 | '''Called as the session is ending'''
28 | TelnetHandlerBase.finish(self)
29 | # Ensure the greenlet is dead
30 | self.greenlet_ic.kill()
31 |
32 |
33 | # -- Green input handling functions --
34 |
35 | def getc(self, block=True):
36 | """Return one character from the input queue"""
37 | try:
38 | return self.cookedq.get(block)
39 | except eventlet.queue.Empty:
40 | return ''
41 |
42 | def inputcooker_socket_ready(self):
43 | """Indicate that the socket is ready to be read"""
44 | return eventlet.select.select(
45 | [self.sock.fileno()],
46 | [],
47 | [],
48 | 0
49 | ) != ([], [], [])
50 |
51 | def inputcooker_store_queue(self, char):
52 | """Put the cooked data in the input queue (no locking needed)"""
53 | if type(char) in [type(()), type([]), type("")]:
54 | for v in char:
55 | self.cookedq.put(v)
56 | else:
57 | self.cookedq.put(char)
58 |
59 |
--------------------------------------------------------------------------------
/telnetsrv/green.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Telnet handler concrete class using green threads
3 |
4 | import gevent, gevent.queue
5 |
6 | from telnetsrvlib import TelnetHandlerBase, command
7 |
8 | class TelnetHandler(TelnetHandlerBase):
9 | "A telnet server handler using Gevent"
10 | def __init__(self, request, client_address, server):
11 | # Create a green queue for input handling
12 | self.cookedq = gevent.queue.Queue()
13 | # Call the base class init method
14 | TelnetHandlerBase.__init__(self, request, client_address, server)
15 |
16 | def setup(self):
17 | '''Called after instantiation'''
18 | TelnetHandlerBase.setup(self)
19 | # Spawn a greenlet to handle socket input
20 | self.greenlet_ic = gevent.spawn(self.inputcooker)
21 | # Note that inputcooker exits on EOF
22 |
23 | # Sleep for 0.5 second to allow options negotiation
24 | gevent.sleep(0.5)
25 |
26 | def finish(self):
27 | '''Called as the session is ending'''
28 | TelnetHandlerBase.finish(self)
29 | # Ensure the greenlet is dead
30 | self.greenlet_ic.kill()
31 |
32 |
33 | # -- Green input handling functions --
34 |
35 | def getc(self, block=True):
36 | """Return one character from the input queue"""
37 | try:
38 | return self.cookedq.get(block)
39 | except gevent.queue.Empty:
40 | return ''
41 |
42 | def inputcooker_socket_ready(self):
43 | """Indicate that the socket is ready to be read"""
44 | return gevent.select.select([self.sock.fileno()], [], [], 0) != ([], [], [])
45 |
46 | def inputcooker_store_queue(self, char):
47 | """Put the cooked data in the input queue (no locking needed)"""
48 | if type(char) in [type(()), type([]), type("")]:
49 | for v in char:
50 | self.cookedq.put(v)
51 | else:
52 | self.cookedq.put(char)
53 |
54 |
--------------------------------------------------------------------------------
/telnetsrv/paramiko_ssh.py:
--------------------------------------------------------------------------------
1 | import logging
2 | #from binascii import hexlify
3 | from threading import Thread
4 | from SocketServer import BaseRequestHandler
5 |
6 | from paramiko import Transport, ServerInterface, RSAKey, DSSKey, SSHException, \
7 | AUTH_SUCCESSFUL, AUTH_FAILED, \
8 | OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, \
9 | OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, OPEN_FAILED_RESOURCE_SHORTAGE
10 |
11 |
12 | log = logging.getLogger(__name__)
13 |
14 | def getRsaKeyFile(filename, password=None):
15 | try:
16 | key = RSAKey(filename=filename, password=password)
17 | except IOError:
18 | log.info('Generating new server RSA key and saving in file %r.' % filename)
19 | key = RSAKey.generate(1024)
20 | key.write_private_key_file(filename, password=password)
21 | return key
22 |
23 |
24 | class TelnetToPtyHandler(object):
25 | '''Mixin to turn TelnetHandler into PtyHandler'''
26 | def __init__(self, *args):
27 | super(TelnetToPtyHandler, self).__init__(*args)
28 |
29 | # Don't mention these, client isn't listening for them. Blank the dicts.
30 | DOACK = {}
31 | WILLACK = {}
32 |
33 | # Do not ask for auth in the PTY, it'll be handled via SSH, then passed in with the request
34 | def authentication_ok(self):
35 | '''Checks the authentication and sets the username of the currently connected terminal. Returns True or False'''
36 | # Since authentication already happened, this should always return true
37 | self.username = self.request.username
38 | return True
39 |
40 |
41 | class SSHHandler(ServerInterface, BaseRequestHandler):
42 | telnet_handler = None
43 | pty_handler = None
44 | host_key = None
45 | username = None
46 |
47 | def __init__(self, request, client_address, server):
48 | self.request = request
49 | self.client_address = client_address
50 | self.tcp_server = server
51 |
52 | # Keep track of channel information from the transport
53 | self.channels = {}
54 |
55 | self.client = request._sock
56 | # Transport turns the socket into an SSH transport
57 | self.transport = Transport(self.client)
58 |
59 | # Create the PTY handler class by mixing in
60 | TelnetHandlerClass = self.telnet_handler
61 | class MixedPtyHandler(TelnetToPtyHandler, TelnetHandlerClass):
62 | # BaseRequestHandler does not inherit from object, must call the __init__ directly
63 | def __init__(self, *args):
64 | TelnetHandlerClass.__init__(self, *args)
65 | self.pty_handler = MixedPtyHandler
66 |
67 |
68 | # Call the base class to run the handler
69 | BaseRequestHandler.__init__(self, request, client_address, server)
70 |
71 | def setup(self):
72 | '''Setup the connection.'''
73 | log.debug( 'New request from address %s, port %d', self.client_address )
74 |
75 | try:
76 | self.transport.load_server_moduli()
77 | except:
78 | log.exception( '(Failed to load moduli -- gex will be unsupported.)' )
79 | raise
80 | try:
81 | self.transport.add_server_key(self.host_key)
82 | except:
83 | if self.host_key is None:
84 | log.critical('Host key not set! SSHHandler MUST define the host_key parameter.')
85 | raise NotImplementedError('Host key not set! SSHHandler instance must define the host_key parameter. Try host_key = paramiko_ssh.getRsaKeyFile("server_rsa.key").')
86 |
87 | try:
88 | # Tell transport to use this object as a server
89 | log.debug( 'Starting SSH server-side negotiation' )
90 | self.transport.start_server(server=self)
91 | except SSHException, e:
92 | log.warn('SSH negotiation failed. %s', e)
93 | raise
94 |
95 | # Accept any requested channels
96 | while True:
97 | channel = self.transport.accept(20)
98 | if channel is None:
99 | # check to see if any thread is running
100 | any_running = False
101 | for c, thread in self.channels.items():
102 | if thread.is_alive():
103 | any_running = True
104 | break
105 | if not any_running:
106 | break
107 | else:
108 | log.info( 'Accepted channel %s', channel )
109 | #raise RuntimeError('No channel requested.')
110 |
111 |
112 |
113 | class dummy_request(object):
114 | def __init__(self):
115 | self._sock = None
116 |
117 | @classmethod
118 | def streamserver_handle(cls, socket, address):
119 | '''Translate this class for use in a StreamServer'''
120 | request = cls.dummy_request()
121 | request._sock = socket
122 | server = None
123 | cls(request, address, server)
124 |
125 |
126 | def finish(self):
127 | '''Called when the socket closes from the client.'''
128 | self.transport.close()
129 |
130 |
131 | def check_channel_request(self, kind, chanid):
132 | if kind == 'session':
133 | return OPEN_SUCCEEDED
134 | return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
135 |
136 | def set_username(self, username):
137 | self.username = username
138 | log.info('User logged in: %s' % username)
139 |
140 | ###### Handle User Authentication ######
141 |
142 | # Override these with functions to use for callbacks
143 | authCallback = None
144 | authCallbackKey = None
145 | authCallbackUsername = None
146 |
147 | def get_allowed_auths(self, username):
148 | methods = []
149 | if self.authCallbackUsername is not None:
150 | methods.append('none')
151 | if self.authCallback is not None:
152 | methods.append('password')
153 | if self.authCallbackKey is not None:
154 | methods.append('publickey')
155 |
156 | if methods == []:
157 | # If no methods were defined, use none
158 | methods.append('none')
159 |
160 | log.debug('Configured authentication methods: %r', methods)
161 | return ','.join(methods)
162 |
163 | def check_auth_password(self, username, password):
164 | #print 'check_auth_password(%s, %s)' % (username, password)
165 | try:
166 | self.authCallback(username, password)
167 | except:
168 | return AUTH_FAILED
169 | else:
170 | self.set_username(username)
171 | return AUTH_SUCCESSFUL
172 |
173 |
174 | def check_auth_publickey(self, username, key):
175 | #print 'Auth attempt with key: ' + hexlify(key.get_fingerprint())
176 | try:
177 | self.authCallbackKey(username, key)
178 | except:
179 | return AUTH_FAILED
180 | else:
181 | self.set_username(username)
182 | return AUTH_SUCCESSFUL
183 | #if (username == 'xx') and (key == self.good_pub_key):
184 | # return AUTH_SUCCESSFUL
185 |
186 |
187 | def check_auth_none(self, username):
188 | if self.authCallbackUsername is None:
189 | self.set_username(username)
190 | return AUTH_SUCCESSFUL
191 | try:
192 | self.authCallbackUsername(username)
193 | except:
194 | return AUTH_FAILED
195 | else:
196 | self.set_username(username)
197 | return AUTH_SUCCESSFUL
198 |
199 |
200 | def check_channel_shell_request(self, channel):
201 | '''Request to start a shell on the given channel'''
202 | try:
203 | self.channels[channel].start()
204 | except KeyError:
205 | log.error('Requested to start a channel (%r) that was not previously set up.', channel)
206 | return False
207 | else:
208 | return True
209 |
210 | def check_channel_pty_request(self, channel, term, width, height, pixelwidth,
211 | pixelheight, modes):
212 | '''Request to allocate a PTY terminal.'''
213 | #self.sshterm = term
214 | #print "term: %r, modes: %r" % (term, modes)
215 | log.debug('PTY requested. Setting up %r.', self.telnet_handler)
216 | pty_thread = Thread( target=self.start_pty_request, args=(channel, term, modes) )
217 | self.channels[channel] = pty_thread
218 |
219 | return True
220 |
221 | def start_pty_request(self, channel, term, modes):
222 | '''Start a PTY - intended to run it a (green)thread.'''
223 | request = self.dummy_request()
224 | request._sock = channel
225 | request.modes = modes
226 | request.term = term
227 | request.username = self.username
228 |
229 | # modes = http://www.ietf.org/rfc/rfc4254.txt page 18
230 | # for i in xrange(50):
231 | # print "%r: %r" % (int(m[i*5].encode('hex'), 16), int(''.join(m[i*5+1:i*5+5]).encode('hex'), 16))
232 |
233 |
234 | # This should block until the user quits the pty
235 | self.pty_handler(request, self.client_address, self.tcp_server)
236 |
237 | # Shutdown the entire session
238 | self.transport.close()
239 |
240 |
--------------------------------------------------------------------------------
/telnetsrv/telnetsrvlib.py:
--------------------------------------------------------------------------------
1 | # license: LGPL
2 | # For distribution, see the COPYING.txt file that accompanies this file.
3 | """TELNET server class
4 |
5 | Based on the telnet client in telnetlib.py
6 |
7 | Presents a command line interface to the telnet client.
8 | Various settings can affect the operation of the server:
9 |
10 | authCallback = Reference to authentication function. If
11 | there is none, no un/pw is requested. Should
12 | raise an exception if authentication fails
13 | Default: None
14 | authNeedUser = Should a username be requested?
15 | Default: False
16 | authNeedPass = Should a password be requested?
17 | Default: False
18 | COMMANDS = Dictionary of supported commands
19 | Key = command (Must be upper case)
20 | Value = List of (function, help text)
21 | Function.__doc__ should be long help
22 | Function.aliases may be a list of alternative spellings
23 | """
24 |
25 | import SocketServer
26 | import socket
27 | import struct
28 | import sys
29 | import traceback
30 | import curses.ascii
31 | import curses.has_key
32 | import curses
33 | import logging
34 | #if not hasattr(socket, 'SHUT_RDWR'):
35 | # socket.SHUT_RDWR = 2
36 |
37 | log = logging.getLogger(__name__)
38 |
39 | BELL = chr(7)
40 | ESC = chr(27)
41 | ANSI_START_SEQ = '['
42 | ANSI_KEY_TO_CURSES = {
43 | 'A': curses.KEY_UP,
44 | 'B': curses.KEY_DOWN,
45 | 'C': curses.KEY_RIGHT,
46 | 'D': curses.KEY_LEFT,
47 | }
48 |
49 | IAC = chr(255) # "Interpret As Command"
50 | DONT = chr(254)
51 | DO = chr(253)
52 | WONT = chr(252)
53 | WILL = chr(251)
54 | theNULL = chr(0)
55 |
56 | SE = chr(240) # Subnegotiation End
57 | NOP = chr(241) # No Operation
58 | DM = chr(242) # Data Mark
59 | BRK = chr(243) # Break
60 | IP = chr(244) # Interrupt process
61 | AO = chr(245) # Abort output
62 | AYT = chr(246) # Are You There
63 | EC = chr(247) # Erase Character
64 | EL = chr(248) # Erase Line
65 | GA = chr(249) # Go Ahead
66 | SB = chr(250) # Subnegotiation Begin
67 |
68 | BINARY = chr(0) # 8-bit data path
69 | ECHO = chr(1) # echo
70 | RCP = chr(2) # prepare to reconnect
71 | SGA = chr(3) # suppress go ahead
72 | NAMS = chr(4) # approximate message size
73 | STATUS = chr(5) # give status
74 | TM = chr(6) # timing mark
75 | RCTE = chr(7) # remote controlled transmission and echo
76 | NAOL = chr(8) # negotiate about output line width
77 | NAOP = chr(9) # negotiate about output page size
78 | NAOCRD = chr(10) # negotiate about CR disposition
79 | NAOHTS = chr(11) # negotiate about horizontal tabstops
80 | NAOHTD = chr(12) # negotiate about horizontal tab disposition
81 | NAOFFD = chr(13) # negotiate about formfeed disposition
82 | NAOVTS = chr(14) # negotiate about vertical tab stops
83 | NAOVTD = chr(15) # negotiate about vertical tab disposition
84 | NAOLFD = chr(16) # negotiate about output LF disposition
85 | XASCII = chr(17) # extended ascii character set
86 | LOGOUT = chr(18) # force logout
87 | BM = chr(19) # byte macro
88 | DET = chr(20) # data entry terminal
89 | SUPDUP = chr(21) # supdup protocol
90 | SUPDUPOUTPUT = chr(22) # supdup output
91 | SNDLOC = chr(23) # send location
92 | TTYPE = chr(24) # terminal type
93 | EOR = chr(25) # end or record
94 | TUID = chr(26) # TACACS user identification
95 | OUTMRK = chr(27) # output marking
96 | TTYLOC = chr(28) # terminal location number
97 | VT3270REGIME = chr(29) # 3270 regime
98 | X3PAD = chr(30) # X.3 PAD
99 | NAWS = chr(31) # window size
100 | TSPEED = chr(32) # terminal speed
101 | LFLOW = chr(33) # remote flow control
102 | LINEMODE = chr(34) # Linemode option
103 | XDISPLOC = chr(35) # X Display Location
104 | OLD_ENVIRON = chr(36) # Old - Environment variables
105 | AUTHENTICATION = chr(37) # Authenticate
106 | ENCRYPT = chr(38) # Encryption option
107 | NEW_ENVIRON = chr(39) # New - Environment variables
108 | # the following ones come from
109 | # http://www.iana.org/assignments/telnet-options
110 | # Unfortunately, that document does not assign identifiers
111 | # to all of them, so we are making them up
112 | TN3270E = chr(40) # TN3270E
113 | XAUTH = chr(41) # XAUTH
114 | CHARSET = chr(42) # CHARSET
115 | RSP = chr(43) # Telnet Remote Serial Port
116 | COM_PORT_OPTION = chr(44) # Com Port Control Option
117 | SUPPRESS_LOCAL_ECHO = chr(45) # Telnet Suppress Local Echo
118 | TLS = chr(46) # Telnet Start TLS
119 | KERMIT = chr(47) # KERMIT
120 | SEND_URL = chr(48) # SEND-URL
121 | FORWARD_X = chr(49) # FORWARD_X
122 | PRAGMA_LOGON = chr(138) # TELOPT PRAGMA LOGON
123 | SSPI_LOGON = chr(139) # TELOPT SSPI LOGON
124 | PRAGMA_HEARTBEAT = chr(140) # TELOPT PRAGMA HEARTBEAT
125 | EXOPL = chr(255) # Extended-Options-List
126 | NOOPT = chr(0)
127 |
128 | #Codes used in SB SE data stream for terminal type negotiation
129 | IS = chr(0)
130 | SEND = chr(1)
131 |
132 | CMDS = {
133 | WILL: 'WILL',
134 | WONT: 'WONT',
135 | DO: 'DO',
136 | DONT: 'DONT',
137 | SE: 'Subnegotiation End',
138 | NOP: 'No Operation',
139 | DM: 'Data Mark',
140 | BRK: 'Break',
141 | IP: 'Interrupt process',
142 | AO: 'Abort output',
143 | AYT: 'Are You There',
144 | EC: 'Erase Character',
145 | EL: 'Erase Line',
146 | GA: 'Go Ahead',
147 | SB: 'Subnegotiation Begin',
148 | BINARY: 'Binary',
149 | ECHO: 'Echo',
150 | RCP: 'Prepare to reconnect',
151 | SGA: 'Suppress Go-Ahead',
152 | NAMS: 'Approximate message size',
153 | STATUS: 'Give status',
154 | TM: 'Timing mark',
155 | RCTE: 'Remote controlled transmission and echo',
156 | NAOL: 'Negotiate about output line width',
157 | NAOP: 'Negotiate about output page size',
158 | NAOCRD: 'Negotiate about CR disposition',
159 | NAOHTS: 'Negotiate about horizontal tabstops',
160 | NAOHTD: 'Negotiate about horizontal tab disposition',
161 | NAOFFD: 'Negotiate about formfeed disposition',
162 | NAOVTS: 'Negotiate about vertical tab stops',
163 | NAOVTD: 'Negotiate about vertical tab disposition',
164 | NAOLFD: 'Negotiate about output LF disposition',
165 | XASCII: 'Extended ascii character set',
166 | LOGOUT: 'Force logout',
167 | BM: 'Byte macro',
168 | DET: 'Data entry terminal',
169 | SUPDUP: 'Supdup protocol',
170 | SUPDUPOUTPUT: 'Supdup output',
171 | SNDLOC: 'Send location',
172 | TTYPE: 'Terminal type',
173 | EOR: 'End or record',
174 | TUID: 'TACACS user identification',
175 | OUTMRK: 'Output marking',
176 | TTYLOC: 'Terminal location number',
177 | VT3270REGIME: '3270 regime',
178 | X3PAD: 'X.3 PAD',
179 | NAWS: 'Window size',
180 | TSPEED: 'Terminal speed',
181 | LFLOW: 'Remote flow control',
182 | LINEMODE: 'Linemode option',
183 | XDISPLOC: 'X Display Location',
184 | OLD_ENVIRON: 'Old - Environment variables',
185 | AUTHENTICATION: 'Authenticate',
186 | ENCRYPT: 'Encryption option',
187 | NEW_ENVIRON: 'New - Environment variables',
188 | }
189 |
190 |
191 |
192 | class command():
193 | '''Function decorator to define a telnet command.'''
194 | def __init__(self, names, hidden=False):
195 | if type(names) is str:
196 | self.name = names
197 | self.alias = []
198 | else:
199 | self.name = names[0]
200 | self.alias = names[1:]
201 | self.hidden = hidden
202 |
203 | def __call__(self, fn):
204 | try:
205 | # First, assume there are more than one decorators.
206 | # Try to prepend to the list of aliases.
207 | fn.aliases.append(fn.command_name)
208 | fn.aliases.extend(self.alias)
209 | fn.command_name = self.name
210 | fn.hidden = self.hidden or fn.hidden
211 | except:
212 | # If that didn't work, this method only has one decorator
213 | fn.aliases = self.alias
214 | fn.command_name = self.name
215 | fn.hidden = self.hidden
216 | return fn
217 |
218 |
219 |
220 | class InputSimple(object):
221 | '''Simple line handler. All spaces become one, can have quoted parameters, but not null'''
222 | quote_chars = ['"', "'"]
223 | def __init__(self, handler, line):
224 | self.parts = []
225 | self.process(line)
226 |
227 | @property
228 | def cmd(self):
229 | try:
230 | return self.parts[0]
231 | except IndexError:
232 | return ''
233 |
234 | @property
235 | def params(self):
236 | return self.parts[1:]
237 |
238 |
239 | def process(self, line):
240 | line = line.strip()
241 | self.raw = line
242 | cmdlist = [item.strip() for item in line.split()]
243 | idx = 0
244 | while idx < (len(cmdlist) - 1):
245 | if cmdlist[idx][0] in ["'", '"']:
246 | cmdlist[idx] = cmdlist[idx] + " " + cmdlist.pop(idx+1)
247 | if cmdlist[idx][0] != cmdlist[idx][-1]:
248 | continue
249 | cmdlist[idx] = cmdlist[idx][1:-1]
250 | idx = idx + 1
251 | self.parts = cmdlist
252 |
253 |
254 | class InputBashLike(object):
255 | '''Handles escaped characters, quoted parameters and multi-line input similar to Bash.'''
256 | quote_chars = ['"', "'"]
257 | whitespace = [' ', '\t']
258 | escape_char = "\\"
259 | escape_results = {'\\':'\\', 't':'\t', 'n':'\n', ' ':' ', '"': '"', "'":"'"}
260 | continue_prompt = '... '
261 | eol_char = '\n'
262 |
263 | def __init__(self, handler, line):
264 | self.raw = ''
265 | self.handler = handler
266 | self.complete = False
267 | self.inquote = False
268 | self.parts = []
269 | self.part = []
270 | # Set up the initial processing state.
271 | self.process_char = self.process_delimiter
272 | self.process(line)
273 |
274 | @property
275 | def cmd(self):
276 | try:
277 | return self.parts[0]
278 | except IndexError:
279 | return ''
280 |
281 | @property
282 | def params(self):
283 | return self.parts[1:]
284 |
285 | # The following process_x functions handle different states while stepping through the chars of the line.
286 |
287 | def process_delimiter(self, char):
288 | '''Process chars while not in a part'''
289 | if char in self.whitespace:
290 | return
291 | if char in self.quote_chars:
292 | # Store the quote type (' or ") and switch to quote processing.
293 | self.inquote = char
294 | self.process_char = self.process_quote
295 | return
296 | if char == self.eol_char:
297 | self.complete = True
298 | return
299 | # Switch to processing a part.
300 | self.process_char = self.process_part
301 | self.process_char(char)
302 |
303 | def process_part(self, char):
304 | '''Process chars while in a part'''
305 | if char in self.whitespace or char == self.eol_char:
306 | # End of the part.
307 | self.parts.append( ''.join(self.part) )
308 | self.part = []
309 | # Switch back to processing a delimiter.
310 | self.process_char = self.process_delimiter
311 | if char == self.eol_char:
312 | self.complete = True
313 | return
314 | if char in self.quote_chars:
315 | # Store the quote type (' or ") and switch to quote processing.
316 | self.inquote = char
317 | self.process_char = self.process_quote
318 | return
319 | self.part.append(char)
320 |
321 | def process_quote(self, char):
322 | '''Process character while in a quote'''
323 | if char == self.inquote:
324 | # Quote is finished, switch to part processing.
325 | self.process_char = self.process_part
326 | return
327 | try:
328 | self.part.append(char)
329 | except:
330 | self.part = [ char ]
331 |
332 | def process_escape(self, char):
333 | '''Handle the char after the escape char'''
334 | # Always only run once, switch back to the last processor.
335 | self.process_char = self.last_process_char
336 | if self.part == [] and char in self.whitespace:
337 | # Special case where \ is by itself and not at the EOL.
338 | self.parts.append(self.escape_char)
339 | return
340 | if char == self.eol_char:
341 | # Ignore a cr.
342 | return
343 | unescaped = self.escape_results.get(char, self.escape_char+char)
344 | self.part.append(unescaped)
345 |
346 |
347 | def process(self, line):
348 | '''Step through the line and process each character'''
349 | self.raw = self.raw + line
350 | try:
351 | if not line[-1] == self.eol_char:
352 | # Should always be here, but add it just in case.
353 | line = line + self.eol_char
354 | except IndexError:
355 | # Thrown if line == ''
356 | line = self.eol_char
357 |
358 | for char in line:
359 | if char == self.escape_char:
360 | # Always handle escaped characters.
361 | self.last_process_char = self.process_char
362 | self.process_char = self.process_escape
363 | continue
364 | self.process_char(char)
365 | if not self.complete:
366 | # Ask for more.
367 | self.process( self.handler.readline(prompt=self.handler.CONTINUE_PROMPT) )
368 |
369 |
370 | class TelnetHandlerBase(SocketServer.BaseRequestHandler):
371 | "A telnet server based on the client in telnetlib"
372 |
373 | # Several methods are not fully defined in this class, and are
374 | # very specific to either a threaded or green implementation.
375 | # These methods are noted as #abstracmethods to ensure they are
376 | # properly made concrete.
377 | # (abc doesn't like the BaseRequestHandler - sigh)
378 | #__metaclass__ = ABCMeta
379 |
380 | # What I am prepared to do?
381 | DOACK = {
382 | ECHO: WILL,
383 | SGA: WILL,
384 | NEW_ENVIRON: WONT,
385 | }
386 | # What do I want the client to do?
387 | WILLACK = {
388 | ECHO: DONT,
389 | SGA: DO,
390 | NAWS: DONT,
391 | TTYPE: DO,
392 | LINEMODE: DONT,
393 | NEW_ENVIRON: DO,
394 | }
395 | # Default terminal type - used if client doesn't tell us its termtype
396 | TERM = "ansi"
397 | # Keycode to name mapping - used to decide which keys to query
398 | KEYS = { # Key escape sequences
399 | curses.KEY_UP: 'Up', # Cursor up
400 | curses.KEY_DOWN: 'Down', # Cursor down
401 | curses.KEY_LEFT: 'Left', # Cursor left
402 | curses.KEY_RIGHT: 'Right', # Cursor right
403 | curses.KEY_DC: 'Delete', # Delete right
404 | curses.KEY_BACKSPACE: 'Backspace', # Delete left
405 | }
406 | # Reverse mapping of KEYS - used for cooking key codes
407 | ESCSEQ = {
408 | }
409 | # Terminal output escape sequences
410 | CODES = {
411 | 'DEOL': '', # Delete to end of line
412 | 'DEL': '', # Delete and close up
413 | 'INS': '', # Insert space
414 | 'CSRLEFT': '', # Move cursor left 1 space
415 | 'CSRRIGHT': '', # Move cursor right 1 space
416 | }
417 | # What prompt to display
418 | PROMPT = "Telnet Server> "
419 | # What prompt to use for requesting more input
420 | CONTINUE_PROMPT = "... "
421 | # What to display upon connection
422 | WELCOME = "You have connected to the telnet server."
423 | # The function to call to verify authentication data
424 | authCallback = None
425 | # Does authCallback want a username?
426 | authNeedUser = False
427 | # Does authCallback want a password?
428 | authNeedPass = False
429 | # Default username
430 | username = None
431 | # What will handle our inputs?
432 | #input_reader = InputSimple
433 | input_reader = InputBashLike
434 | # Banner to display prior to telnet login
435 | TELNET_ISSUE = None
436 | # What prompt to use when requesting a telnet username
437 | PROMPT_USER = "Username: "
438 | # What prompt to use when requesting a telnet password
439 | PROMPT_PASS = "Password: "
440 |
441 | # --------------------------- Environment Setup ----------------------------
442 |
443 | def __init__(self, request, client_address, server):
444 | """Constructor.
445 |
446 | When called without arguments, create an unconnected instance.
447 | With a hostname argument, it connects the instance; a port
448 | number is optional.
449 | """
450 | # Am I doing the echoing?
451 | self.DOECHO = True
452 | # What opts have I sent DO/DONT for and what did I send?
453 | self.DOOPTS = {}
454 | # What opts have I sent WILL/WONT for and what did I send?
455 | self.WILLOPTS = {}
456 |
457 | # What commands does this CLI support
458 | self.COMMANDS = {}
459 | self.sock = None # TCP socket
460 | self.rawq = '' # Raw input string
461 | self.sbdataq = '' # Sub-Neg string
462 | self.eof = 0 # Has EOF been reached?
463 | self.iacseq = '' # Buffer for IAC sequence.
464 | self.sb = 0 # Flag for SB and SE sequence.
465 | self.history = [] # Command history
466 | self.RUNSHELL = True
467 | # A little magic - Everything called cmdXXX is a command
468 | # Also, check for decorated functions
469 | for k in dir(self):
470 | method = getattr(self, k)
471 | try:
472 | name = method.command_name
473 | except:
474 | if k[:3] == 'cmd':
475 | name = k[3:]
476 | else:
477 | continue
478 |
479 | name = name.upper()
480 | self.COMMANDS[name] = method
481 | for alias in getattr(method, "aliases", []):
482 | self.COMMANDS[alias.upper()] = self.COMMANDS[name]
483 |
484 | SocketServer.BaseRequestHandler.__init__(self, request, client_address, server)
485 |
486 | class false_request(object):
487 | def __init__(self):
488 | self.sock = None
489 |
490 | @classmethod
491 | def streamserver_handle(cls, sock, address):
492 | '''Translate this class for use in a StreamServer'''
493 | request = cls.false_request()
494 | request._sock = sock
495 | server = None
496 | log.debug("Accepted connection, starting telnet session.")
497 | try:
498 | cls(request, address, server)
499 | except socket.error:
500 | pass
501 |
502 | def setterm(self, term):
503 | "Set the curses structures for this terminal"
504 | log.debug("Setting termtype to %s" % (term, ))
505 | curses.setupterm(term) # This will raise if the termtype is not supported
506 | self.TERM = term
507 | self.ESCSEQ = {}
508 | for k in self.KEYS.keys():
509 | str = curses.tigetstr(curses.has_key._capability_names[k])
510 | if str:
511 | self.ESCSEQ[str] = k
512 | # Create a copy to prevent altering the class
513 | self.CODES = self.CODES.copy()
514 | self.CODES['DEOL'] = curses.tigetstr('el')
515 | self.CODES['DEL'] = curses.tigetstr('dch1')
516 | self.CODES['INS'] = curses.tigetstr('ich1')
517 | self.CODES['CSRLEFT'] = curses.tigetstr('cub1')
518 | self.CODES['CSRRIGHT'] = curses.tigetstr('cuf1')
519 |
520 | def setup(self):
521 | "Connect incoming connection to a telnet session"
522 | try:
523 | self.TERM = self.request.term
524 | except:
525 | pass
526 | self.setterm(self.TERM)
527 | self.sock = self.request._sock
528 | for k in self.DOACK.keys():
529 | self.sendcommand(self.DOACK[k], k)
530 | for k in self.WILLACK.keys():
531 | self.sendcommand(self.WILLACK[k], k)
532 |
533 |
534 | def finish(self):
535 | "End this session"
536 | log.debug("Session disconnected.")
537 | try:
538 | self.sock.shutdown(socket.SHUT_RDWR)
539 | except: pass
540 | self.session_end()
541 |
542 | def session_start(self):
543 | pass
544 |
545 | def session_end(self):
546 | pass
547 |
548 | # ------------------------- Telnet Options Engine --------------------------
549 |
550 | def options_handler(self, sock, cmd, opt):
551 | "Negotiate options"
552 | if cmd == NOP:
553 | self.sendcommand(NOP)
554 | elif cmd == WILL or cmd == WONT:
555 | if self.WILLACK.has_key(opt):
556 | self.sendcommand(self.WILLACK[opt], opt)
557 | else:
558 | self.sendcommand(DONT, opt)
559 | if cmd == WILL and opt == TTYPE:
560 | self.writecooked(IAC + SB + TTYPE + SEND + IAC + SE)
561 | elif cmd == DO or cmd == DONT:
562 | if self.DOACK.has_key(opt):
563 | self.sendcommand(self.DOACK[opt], opt)
564 | else:
565 | self.sendcommand(WONT, opt)
566 | if opt == ECHO:
567 | self.DOECHO = (cmd == DO)
568 | elif cmd == SE:
569 | subreq = self.read_sb_data()
570 | if subreq[0] == TTYPE and subreq[1] == IS:
571 | try:
572 | self.setterm(subreq[2:])
573 | except:
574 | log.debug("Terminal type not known")
575 | elif subreq[0] == NAWS:
576 | self.setnaws(subreq[1:])
577 | elif cmd == SB:
578 | pass
579 | else:
580 | log.debug("Unhandled option: %s %s" % (cmdtxt, opttxt, ))
581 |
582 | def sendcommand(self, cmd, opt=None):
583 | "Send a telnet command (IAC)"
584 | if cmd in [DO, DONT]:
585 | if not self.DOOPTS.has_key(opt):
586 | self.DOOPTS[opt] = None
587 | if (((cmd == DO) and (self.DOOPTS[opt] != True))
588 | or ((cmd == DONT) and (self.DOOPTS[opt] != False))):
589 | self.DOOPTS[opt] = (cmd == DO)
590 | self.writecooked(IAC + cmd + opt)
591 | elif cmd in [WILL, WONT]:
592 | if not self.WILLOPTS.has_key(opt):
593 | self.WILLOPTS[opt] = ''
594 | if (((cmd == WILL) and (self.WILLOPTS[opt] != True))
595 | or ((cmd == WONT) and (self.WILLOPTS[opt] != False))):
596 | self.WILLOPTS[opt] = (cmd == WILL)
597 | self.writecooked(IAC + cmd + opt)
598 | else:
599 | self.writecooked(IAC + cmd)
600 |
601 | def read_sb_data(self):
602 | """Return any data available in the SB ... SE queue.
603 |
604 | Return '' if no SB ... SE available. Should only be called
605 | after seeing a SB or SE command. When a new SB command is
606 | found, old unread SB data will be discarded. Don't block.
607 |
608 | """
609 | buf = self.sbdataq
610 | self.sbdataq = ''
611 | return buf
612 |
613 | # ---------------------------- Input Functions -----------------------------
614 |
615 | def _readline_do_echo(self, echo):
616 | """Determine if we should echo or not"""
617 | return echo == True or (echo == None and self.DOECHO == True)
618 |
619 | def _readline_echo(self, char, echo):
620 | """Echo a recieved character, move cursor etc..."""
621 | if self._readline_do_echo(echo):
622 | self.write(char)
623 |
624 | def _readline_insert(self, char, echo, insptr, line):
625 | """Deal properly with inserted chars in a line."""
626 | if not self._readline_do_echo(echo):
627 | return
628 | # Write out the remainder of the line
629 | self.write(char + ''.join(line[insptr:]))
630 | # Cursor Left to the current insert point
631 | char_count = len(line) - insptr
632 | self.write(self.CODES['CSRLEFT'] * char_count)
633 |
634 | _current_line = ''
635 | _current_prompt = ''
636 |
637 | def ansi_to_curses(self, char):
638 | '''Handles reading ANSI escape sequences'''
639 | # ANSI sequences are:
640 | # ESC [
641 | # If we see ESC, read a char
642 | if char != ESC:
643 | return char
644 | # If we see [, read another char
645 | if self.getc(block=True) != ANSI_START_SEQ:
646 | self._readline_echo(BELL, True)
647 | return theNULL
648 | key = self.getc(block=True)
649 | # Translate the key to curses
650 | try:
651 | return ANSI_KEY_TO_CURSES[key]
652 | except:
653 | self._readline_echo(BELL, True)
654 | return theNULL
655 |
656 | def readline(self, echo=None, prompt='', use_history=True):
657 | """Return a line of text, including the terminating LF
658 | If echo is true always echo, if echo is false never echo
659 | If echo is None follow the negotiated setting.
660 | prompt is the current prompt to write (and rewrite if needed)
661 | use_history controls if this current line uses (and adds to) the command history.
662 | """
663 |
664 | line = []
665 | insptr = 0
666 | ansi = 0
667 | histptr = len(self.history)
668 |
669 | if self.DOECHO:
670 | self.write(prompt)
671 | self._current_prompt = prompt
672 | else:
673 | self._current_prompt = ''
674 |
675 | self._current_line = ''
676 |
677 | while True:
678 | c = self.getc(block=True)
679 | c = self.ansi_to_curses(c)
680 | if c == theNULL:
681 | continue
682 |
683 | elif c == curses.KEY_LEFT:
684 | if insptr > 0:
685 | insptr = insptr - 1
686 | self._readline_echo(self.CODES['CSRLEFT'], echo)
687 | else:
688 | self._readline_echo(BELL, echo)
689 | continue
690 | elif c == curses.KEY_RIGHT:
691 | if insptr < len(line):
692 | insptr = insptr + 1
693 | self._readline_echo(self.CODES['CSRRIGHT'], echo)
694 | else:
695 | self._readline_echo(BELL, echo)
696 | continue
697 | elif c == curses.KEY_UP or c == curses.KEY_DOWN:
698 | if not use_history:
699 | self._readline_echo(BELL, echo)
700 | continue
701 | if c == curses.KEY_UP:
702 | if histptr > 0:
703 | histptr = histptr - 1
704 | else:
705 | self._readline_echo(BELL, echo)
706 | continue
707 | elif c == curses.KEY_DOWN:
708 | if histptr < len(self.history):
709 | histptr = histptr + 1
710 | else:
711 | self._readline_echo(BELL, echo)
712 | continue
713 | line = []
714 | if histptr < len(self.history):
715 | line.extend(self.history[histptr])
716 | for char in range(insptr):
717 | self._readline_echo(self.CODES['CSRLEFT'], echo)
718 | self._readline_echo(self.CODES['DEOL'], echo)
719 | self._readline_echo(''.join(line), echo)
720 | insptr = len(line)
721 | continue
722 | elif c == chr(3):
723 | self._readline_echo('\n' + curses.ascii.unctrl(c) + ' ABORT\n', echo)
724 | return ''
725 | elif c == chr(4):
726 | if len(line) > 0:
727 | self._readline_echo('\n' + curses.ascii.unctrl(c) + ' ABORT (QUIT)\n', echo)
728 | return ''
729 | self._readline_echo('\n' + curses.ascii.unctrl(c) + ' QUIT\n', echo)
730 | return 'QUIT'
731 | elif c == chr(10):
732 | self._readline_echo(c, echo)
733 | result = ''.join(line)
734 | if use_history:
735 | self.history.append(result)
736 | if echo is False:
737 | if prompt:
738 | self.write( chr(10) )
739 | log.debug('readline: %s(hidden text)', prompt)
740 | else:
741 | log.debug('readline: %s%r', prompt, result)
742 | return result
743 | elif c == curses.KEY_BACKSPACE or c == chr(127) or c == chr(8):
744 | if insptr > 0:
745 | self._readline_echo(self.CODES['CSRLEFT'] + self.CODES['DEL'], echo)
746 | insptr = insptr - 1
747 | del line[insptr]
748 | else:
749 | self._readline_echo(BELL, echo)
750 | continue
751 | elif c == curses.KEY_DC:
752 | if insptr < len(line):
753 | self._readline_echo(self.CODES['DEL'], echo)
754 | del line[insptr]
755 | else:
756 | self._readline_echo(BELL, echo)
757 | continue
758 | else:
759 | if ord(c) < 32:
760 | c = curses.ascii.unctrl(c)
761 | if len(line) > insptr:
762 | self._readline_insert(c, echo, insptr, line)
763 | else:
764 | self._readline_echo(c, echo)
765 | line[insptr:insptr] = c
766 | insptr = insptr + len(c)
767 | if self._readline_do_echo(echo):
768 | self._current_line = line
769 |
770 | #abstractmethod
771 | def getc(self, block=True):
772 | """Return one character from the input queue"""
773 | # This is very different between green threads and real threads.
774 | raise NotImplementedError("Please Implement the getc method")
775 |
776 | # --------------------------- Output Functions -----------------------------
777 |
778 | def writeresponse(self, text):
779 | """Write out any valid responses. Easy to override with ANSI codes."""
780 | self.writeline(text)
781 |
782 | def writeerror(self, text):
783 | """Write out any error messages. Easy to override with ANSI codes."""
784 | self.writeline(text)
785 |
786 | def writeline(self, text):
787 | """Send a packet with line ending."""
788 | log.debug('writing line %r' % text)
789 | self.write(text+chr(10))
790 |
791 | def writemessage(self, text):
792 | """Write out an asynchronous message, then reconstruct the prompt and entered text."""
793 | log.debug('writing message %r', text)
794 | self.write(chr(10)+text+chr(10))
795 | self.write(self._current_prompt+''.join(self._current_line))
796 |
797 | def write(self, text):
798 | """Send a packet to the socket. This function cooks output."""
799 | text = str(text) # eliminate any unicode or other snigglets
800 | text = text.replace(IAC, IAC+IAC)
801 | text = text.replace(chr(10), chr(13)+chr(10))
802 | self.writecooked(text)
803 |
804 | def writecooked(self, text):
805 | """Put data directly into the output queue (bypass output cooker)"""
806 | self.sock.sendall(text)
807 |
808 | # ------------------------------- Input Cooker -----------------------------
809 | def _inputcooker_getc(self, block=True):
810 | """Get one character from the raw queue. Optionally blocking.
811 | Raise EOFError on end of stream. SHOULD ONLY BE CALLED FROM THE
812 | INPUT COOKER."""
813 | if self.rawq:
814 | ret = self.rawq[0]
815 | self.rawq = self.rawq[1:]
816 | return ret
817 | if not block:
818 | if not self.inputcooker_socket_ready():
819 | return ''
820 | ret = self.sock.recv(20)
821 | self.eof = not(ret)
822 | self.rawq = self.rawq + ret
823 | if self.eof:
824 | raise EOFError
825 | return self._inputcooker_getc(block)
826 |
827 | #abstractmethod
828 | def inputcooker_socket_ready(self):
829 | """Indicate that the socket is ready to be read"""
830 | # Either use a green select or a real select
831 | #return select([self.sock.fileno()], [], [], 0) != ([], [], [])
832 | raise NotImplementedError("Please Implement the inputcooker_socket_ready method")
833 |
834 | def _inputcooker_ungetc(self, char):
835 | """Put characters back onto the head of the rawq. SHOULD ONLY
836 | BE CALLED FROM THE INPUT COOKER."""
837 | self.rawq = char + self.rawq
838 |
839 | def _inputcooker_store(self, char):
840 | """Put the cooked data in the correct queue"""
841 | if self.sb:
842 | self.sbdataq = self.sbdataq + char
843 | else:
844 | self.inputcooker_store_queue(char)
845 |
846 | #abstractmethod
847 | def inputcooker_store_queue(self, char):
848 | """Put the cooked data in the output queue (possible locking needed)"""
849 | raise NotImplementedError("Please Implement the inputcooker_store_queue method")
850 |
851 | def inputcooker(self):
852 | """Input Cooker - Transfer from raw queue to cooked queue.
853 |
854 | Set self.eof when connection is closed. Don't block unless in
855 | the midst of an IAC sequence.
856 | """
857 | try:
858 | while True:
859 | c = self._inputcooker_getc()
860 | if not self.iacseq:
861 | if c == IAC:
862 | self.iacseq += c
863 | continue
864 | elif c == chr(13) and not(self.sb):
865 | c2 = self._inputcooker_getc(block=False)
866 | if c2 == theNULL or c2 == '':
867 | c = chr(10)
868 | elif c2 == chr(10):
869 | c = c2
870 | else:
871 | self._inputcooker_ungetc(c2)
872 | c = chr(10)
873 | elif c in [x[0] for x in self.ESCSEQ.keys()]:
874 | 'Looks like the begining of a key sequence'
875 | codes = c
876 | for keyseq in self.ESCSEQ.keys():
877 | if len(keyseq) == 0:
878 | continue
879 | while codes == keyseq[:len(codes)] and len(codes) <= keyseq:
880 | if codes == keyseq:
881 | c = self.ESCSEQ[keyseq]
882 | break
883 | codes = codes + self._inputcooker_getc()
884 | if codes == keyseq:
885 | break
886 | self._inputcooker_ungetc(codes[1:])
887 | codes = codes[0]
888 | self._inputcooker_store(c)
889 | elif len(self.iacseq) == 1:
890 | 'IAC: IAC CMD [OPTION only for WILL/WONT/DO/DONT]'
891 | if c in (DO, DONT, WILL, WONT):
892 | self.iacseq += c
893 | continue
894 | self.iacseq = ''
895 | if c == IAC:
896 | self._inputcooker_store(c)
897 | else:
898 | if c == SB: # SB ... SE start.
899 | self.sb = 1
900 | self.sbdataq = ''
901 | elif c == SE: # SB ... SE end.
902 | self.sb = 0
903 | # Callback is supposed to look into
904 | # the sbdataq
905 | self.options_handler(self.sock, c, NOOPT)
906 | elif len(self.iacseq) == 2:
907 | cmd = self.iacseq[1]
908 | self.iacseq = ''
909 | if cmd in (DO, DONT, WILL, WONT):
910 | self.options_handler(self.sock, cmd, c)
911 | except (EOFError, socket.error):
912 | pass
913 |
914 | # ------------------------------- Basic Commands ---------------------------
915 |
916 | # Format of docstrings for command methods:
917 | # Line 0: Command paramater(s) if any. (Can be blank line)
918 | # Line 1: Short descriptive text. (Mandatory)
919 | # Line 2+: Long descriptive text. (Can be blank line)
920 |
921 | def cmdHELP(self, params):
922 | """[]
923 | Display help
924 | Display either brief help on all commands, or detailed
925 | help on a single command passed as a parameter.
926 | """
927 | if params:
928 | cmd = params[0].upper()
929 | if self.COMMANDS.has_key(cmd):
930 | method = self.COMMANDS[cmd]
931 | doc = method.__doc__.split("\n")
932 | docp = doc[0].strip()
933 | docl = '\n'.join( [l.strip() for l in doc[2:]] )
934 | if not docl.strip(): # If there isn't anything here, use line 1
935 | docl = doc[1].strip()
936 | self.writeline(
937 | "%s %s\n\n%s" % (
938 | cmd,
939 | docp,
940 | docl,
941 | )
942 | )
943 | return
944 | else:
945 | self.writeline("Command '%s' not known" % cmd)
946 | else:
947 | self.writeline("Help on built in commands\n")
948 | keys = self.COMMANDS.keys()
949 | keys.sort()
950 | for cmd in keys:
951 | method = self.COMMANDS[cmd]
952 | if getattr(method, 'hidden', False):
953 | continue
954 | if method.__doc__ == None:
955 | self.writeline("no help for command %s" % method)
956 | return
957 | doc = method.__doc__.split("\n")
958 | docp = doc[0].strip()
959 | docs = doc[1].strip()
960 | if len(docp) > 0:
961 | docps = "%s - %s" % (docp, docs, )
962 | else:
963 | docps = "- %s" % (docs, )
964 | self.writeline(
965 | "%s %s" % (
966 | cmd,
967 | docps,
968 | )
969 | )
970 | cmdHELP.aliases = ['?']
971 |
972 | def cmdEXIT(self, params):
973 | """
974 | Exit the command shell
975 | """
976 | self.RUNSHELL = False
977 | self.writeline("Goodbye")
978 | cmdEXIT.aliases = ['QUIT', 'BYE', 'LOGOUT']
979 |
980 | def cmdHISTORY(self, params):
981 | """
982 | Display the command history
983 | """
984 | cnt = 0
985 | self.writeline('Command history\n')
986 | for line in self.history:
987 | cnt = cnt + 1
988 | self.writeline("%-5d : %s" % (cnt, ''.join(line)))
989 |
990 | # ----------------------- Command Line Processor Engine --------------------
991 |
992 | def handleException(self, exc_type, exc_param, exc_tb):
993 | "Exception handler (False to abort)"
994 | self.writeline(''.join( traceback.format_exception(exc_type, exc_param, exc_tb) ))
995 | return True
996 |
997 | def authentication_ok(self):
998 | '''Checks the authentication and sets the username of the currently connected terminal. Returns True or False'''
999 | username = None
1000 | password = None
1001 | if self.authCallback:
1002 | if self.authNeedUser:
1003 | username = self.readline(prompt=self.PROMPT_USER, use_history=False)
1004 | if self.authNeedPass:
1005 | password = self.readline(echo=False, prompt=self.PROMPT_PASS, use_history=False)
1006 | if self.DOECHO:
1007 | self.write("\n")
1008 | try:
1009 | self.authCallback(username, password)
1010 | except:
1011 | self.username = None
1012 | return False
1013 | else:
1014 | # Successful authentication
1015 | self.username = username
1016 | return True
1017 | else:
1018 | # No authentication desired
1019 | self.username = None
1020 | return True
1021 |
1022 |
1023 | def handle(self):
1024 | "The actual service to which the user has connected."
1025 | if self.TELNET_ISSUE:
1026 | self.writeline(self.TELNET_ISSUE)
1027 | if not self.authentication_ok():
1028 | return
1029 | if self.DOECHO:
1030 | self.writeline(self.WELCOME)
1031 |
1032 | self.session_start()
1033 | while self.RUNSHELL:
1034 | raw_input = self.readline(prompt=self.PROMPT).strip()
1035 | self.input = self.input_reader(self, raw_input)
1036 | self.raw_input = self.input.raw
1037 | if self.input.cmd:
1038 | cmd = self.input.cmd.upper()
1039 | params = self.input.params
1040 | if self.COMMANDS.has_key(cmd):
1041 | try:
1042 | self.COMMANDS[cmd](params)
1043 | except:
1044 | log.exception('Error calling %s.' % cmd)
1045 | (t, p, tb) = sys.exc_info()
1046 | if self.handleException(t, p, tb):
1047 | break
1048 | else:
1049 | self.writeerror("Unknown command '%s'" % cmd)
1050 | log.debug("Exiting handler")
1051 |
1052 |
1053 |
1054 | # vim: set syntax=python ai showmatch:
1055 |
--------------------------------------------------------------------------------
/telnetsrv/test_rsa.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
3 | oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
4 | d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
5 | gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
6 | EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
7 | soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
8 | tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
9 | avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
10 | 4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
11 | H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
12 | qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
13 | HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
14 | nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
15 | -----END RSA PRIVATE KEY-----
16 |
--------------------------------------------------------------------------------
/telnetsrv/threaded.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Telnet handler concrete class using true threads.
3 |
4 | import threading
5 | import time
6 | import select
7 |
8 | from telnetsrvlib import TelnetHandlerBase, command
9 |
10 | class TelnetHandler(TelnetHandlerBase):
11 | "A telnet server handler using Threading"
12 | def __init__(self, request, client_address, server):
13 | # This is the cooked input stream (list of charcodes)
14 | self.cookedq = []
15 |
16 | # Create the locks for handing the input/output queues
17 | self.IQUEUELOCK = threading.Lock()
18 | self.OQUEUELOCK = threading.Lock()
19 |
20 | # Call the base class init method
21 | TelnetHandlerBase.__init__(self, request, client_address, server)
22 |
23 | def setup(self):
24 | '''Called after instantiation'''
25 | TelnetHandlerBase.setup(self)
26 | # Spawn a thread to handle socket input
27 | self.thread_ic = threading.Thread(target=self.inputcooker)
28 | self.thread_ic.setDaemon(True)
29 | self.thread_ic.start()
30 | # Note that inputcooker exits on EOF
31 |
32 | # Sleep for 0.5 second to allow options negotiation
33 | time.sleep(0.5)
34 |
35 |
36 | def finish(self):
37 | '''Called as the session is ending'''
38 | TelnetHandlerBase.finish(self)
39 | # Might want to ensure the thread_ic is dead
40 |
41 |
42 | # -- Threaded input handling functions --
43 |
44 | def getc(self, block=True):
45 | """Return one character from the input queue"""
46 | if not block:
47 | if not len(self.cookedq):
48 | return ''
49 | while not len(self.cookedq):
50 | time.sleep(0.05)
51 | self.IQUEUELOCK.acquire()
52 | ret = self.cookedq[0]
53 | self.cookedq = self.cookedq[1:]
54 | self.IQUEUELOCK.release()
55 | return ret
56 |
57 | def inputcooker_socket_ready(self):
58 | """Indicate that the socket is ready to be read"""
59 | return select.select([self.sock.fileno()], [], [], 0) != ([], [], [])
60 |
61 | def inputcooker_store_queue(self, char):
62 | """Put the cooked data in the input queue (with locking)"""
63 | self.IQUEUELOCK.acquire()
64 | if type(char) in [type(()), type([]), type("")]:
65 | for v in char:
66 | self.cookedq.append(v)
67 | else:
68 | self.cookedq.append(char)
69 | self.IQUEUELOCK.release()
70 |
71 |
72 | # -- Threaded output handling functions --
73 |
74 | def writemessage(self, text):
75 | """Put data in output queue, rebuild the prompt and entered data"""
76 | # Need to grab the input queue lock to ensure the entered data doesn't change
77 | # before we're done rebuilding it.
78 | # Note that writemessage will eventually call writecooked
79 | self.IQUEUELOCK.acquire()
80 | TelnetHandlerBase.writemessage(self, text)
81 | self.IQUEUELOCK.release()
82 |
83 | def writecooked(self, text):
84 | """Put data directly into the output queue"""
85 | # Ensure this is the only thread writing
86 | self.OQUEUELOCK.acquire()
87 | TelnetHandlerBase.writecooked(self, text)
88 | self.OQUEUELOCK.release()
89 |
90 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import logging
4 | import argparse
5 |
6 | logging.getLogger('').setLevel(logging.DEBUG)
7 |
8 | TELNET_IP_BINDING = '' #all
9 |
10 | # Parse the input arguments
11 | # Normally, these would be down in "if __name__ == '__main__'", but we need to know green-vs-threaded for the base class and other imports
12 | parser = argparse.ArgumentParser( description='Run a telnet server.')
13 | parser.add_argument( 'port', metavar="PORT", type=int, help="The port on which to listen on." )
14 | parser.add_argument( '-s', '--ssh', action='store_const', const=True, default=False, help="Run as SSH server using Paramiko library.")
15 | parser.add_argument( '-g', '--green', action='store_const', const=True, default=False, help="Run with cooperative multitasking using Gevent library.")
16 | parser.add_argument( '-e', '--eventlet', action='store_const', const=True, default=False, help="Run with cooperative multitasking using Eventlet library.")
17 | console_args = parser.parse_args()
18 |
19 | TELNET_PORT_BINDING = console_args.port
20 |
21 | if console_args.ssh:
22 | SERVERPROTOCOL = 'SSH'
23 | else:
24 | SERVERPROTOCOL = 'telnet'
25 |
26 | if console_args.green:
27 | SERVERTYPE = 'green'
28 | # To run a green server, import gevent and the green version of telnetsrv.
29 | import gevent, gevent.server
30 | from telnetsrv.green import TelnetHandler, command
31 | elif console_args.eventlet:
32 | SERVERTYPE = 'eventlet'
33 | # To run a eventlet server, import eventlet and the eventlet version of telnetsrv.
34 | import eventlet
35 | from telnetsrv.evtlet import TelnetHandler, command
36 | else:
37 | SERVERTYPE = 'threaded'
38 | # To run a threaded server, import threading and other libraries to help out.
39 | import SocketServer
40 | import threading
41 | import time
42 |
43 | from telnetsrv.threaded import TelnetHandler, command
44 |
45 | # The SocketServer needs *all IPs* to be 0.0.0.0
46 | if not TELNET_IP_BINDING:
47 | TELNET_IP_BINDING = '0.0.0.0'
48 |
49 |
50 |
51 | # The TelnetHandler instance is re-created for each connection.
52 | # Therfore, in order to store data between connections, create
53 | # a seperate object to deal with any logic that needs to persist
54 | # after the user logs off.
55 | # Here is a simple example that just counts the number of connections
56 | # as well as the number of times this user has connected.
57 |
58 | class MyServer(object):
59 | '''A simple server class that just keeps track of a connection count.'''
60 | def __init__(self):
61 | # Var to track the total connections.
62 | self.connection_count = 0
63 |
64 | # Dictionary to track individual connections.
65 | self.user_connect = {}
66 |
67 | def new_connection(self, username):
68 | '''Register a new connection by username, return the count of connections.'''
69 | self.connection_count += 1
70 | try:
71 | self.user_connect[username] += 1
72 | except:
73 | self.user_connect[username] = 1
74 | return self.connection_count, self.user_connect[username]
75 |
76 |
77 |
78 | # Subclass TelnetHandler to add our own commands and to call back
79 | # to myserver.
80 |
81 | class TestTelnetHandler(TelnetHandler):
82 | # Create the instance of the server within the class for easy use
83 | myserver = MyServer()
84 |
85 | # -- Override items to customize the server --
86 |
87 | WELCOME = 'You have connected to the test server.'
88 | PROMPT = "TestServer> "
89 | authNeedUser = True
90 | authNeedPass = False
91 |
92 | def authCallback(self, username, password):
93 | '''Called to validate the username/password.'''
94 | # Note that this method will be ignored if the SSH server is invoked.
95 | # We accept everyone here, as long as any name is given!
96 | if not username:
97 | # complain by raising any exception
98 | raise
99 |
100 | def session_start(self):
101 | '''Called after the user successfully logs in.'''
102 | self.writeline('This server is running %s.' % SERVERTYPE)
103 |
104 | # Tell myserver that we have a new connection, and provide the username.
105 | # We get back the login count information.
106 | globalcount, usercount = self.myserver.new_connection( self.username )
107 |
108 | self.writeline('Hello %s!' % self.username)
109 | self.writeline('You are connection #%d, you have logged in %s time(s).' % (globalcount, usercount))
110 |
111 | # Track any asyncronous events registered with the timer command
112 | self.timer_events = []
113 |
114 | def session_end(self):
115 | '''Called after the user logs off.'''
116 |
117 | # Cancel any pending timer events. Done a bit different between Greenlets and threads
118 | if SERVERTYPE == 'green':
119 | for event in self.timer_events:
120 | event.kill()
121 |
122 | if SERVERTYPE == 'eventlet':
123 | for event in self.timer_events:
124 | event.kill()
125 |
126 | if SERVERTYPE == 'threaded':
127 | for event in self.timer_events:
128 | event.cancel()
129 |
130 | def writeerror(self, text):
131 | '''Called to write any error information (like a mistyped command).
132 | Add a splash of color using ANSI to render the error text in red.
133 | see http://en.wikipedia.org/wiki/ANSI_escape_code'''
134 | TelnetHandler.writeerror(self, "\x1b[91m%s\x1b[0m" % text )
135 |
136 |
137 | # -- Custom Commands --
138 | @command('debug')
139 | def command_debug(self, params):
140 | """
141 | Display some debugging data
142 | """
143 | for (v,k) in self.ESCSEQ.items():
144 | line = '%-10s : ' % (self.KEYS[k], )
145 | for c in v:
146 | if ord(c)<32 or ord(c)>126:
147 | line = line + curses.ascii.unctrl(c)
148 | else:
149 | line = line + c
150 | self.writeresponse(line)
151 |
152 | @command('params')
153 | def command_params(self, params):
154 | '''[]*
155 | Echos back the raw received parameters.
156 | '''
157 | self.writeresponse("params == %r" % params)
158 |
159 | @command('info')
160 | def command_info(self, params):
161 | '''
162 | Provides some information about the current terminal.
163 | '''
164 | self.writeresponse( "Username: %s, terminal type: %s" % (self.username, self.TERM) )
165 | self.writeresponse( "Command history:" )
166 | for c in self.history:
167 | self.writeresponse(" %r" % c)
168 |
169 | @command(['timer', 'timeit'])
170 | def command_timer(self, params):
171 | '''
172 | In seconds, display .
173 | Send a message after a delay.
174 | is in seconds.
175 | If is more than one word, quotes are required.
176 |
177 | example: TIMER 5 "hello world!"
178 | '''
179 | try:
180 | timestr, message = params[:2]
181 | delay = int(timestr)
182 | except ValueError:
183 | self.writeerror( "Need both a time and a message" )
184 | return
185 | self.writeresponse("Waiting %d seconds..." % delay)
186 |
187 | if SERVERTYPE == 'green':
188 | event = gevent.spawn_later(delay, self.writemessage, message)
189 |
190 | if SERVERTYPE == 'eventlet':
191 | event = eventlet.spawn_after(delay, self.writemessage, message)
192 |
193 | if SERVERTYPE == 'threaded':
194 | event = threading.Timer(delay, self.writemessage, args=[message])
195 | event.start()
196 |
197 | # Used by session_end to stop all timer events when the user logs off.
198 | self.timer_events.append(event)
199 |
200 | @command('passwd')
201 | def command_set_password(self, params):
202 | '''[]
203 | Pretends to set a console password.
204 | Pretends to set a console password.
205 | Demonostrates how sensative information may be handled
206 | '''
207 | try:
208 | password = params[0]
209 | except:
210 | password = self.readline(prompt="New Password: ", echo=False, use_history=False)
211 | else:
212 | # If the password was a parameter, it will have been stored in the history.
213 | # snip it out to prevent easy snooping
214 | self.history[-1] = 'passwd'
215 |
216 | password2 = self.readline(prompt="Retype New Password: ", echo=False, use_history=False)
217 | if password == password2:
218 | self.writeresponse('Pretending to set new password, but not really.')
219 | else:
220 | self.writeerror('Passwords don\'t match.')
221 |
222 |
223 | # Older method of defining a command
224 | # must start with "cmd" and end wtih the command name.
225 | # Aliases may be attached after the method definitions.
226 | def cmdECHO(self, params):
227 | '''
228 | Echo text back to the console.
229 |
230 | '''
231 | self.writeresponse( ' '.join(params) )
232 | # Create an alias for this command
233 | cmdECHO.aliases = ['REPEAT']
234 |
235 |
236 | def cmdTERM(self, params):
237 | '''
238 | Hidden command to print the current TERM
239 |
240 | '''
241 | self.writeresponse( self.TERM )
242 | # Hide this command, old-style syntax. Will not show in the help list.
243 | cmdTERM.hidden = True
244 |
245 |
246 | @command('hide-me', hidden=True)
247 | @command(['hide-me-too', 'also-me'])
248 | def command_do_nothing(self, params):
249 | '''
250 | Hidden command to perform no action
251 |
252 | '''
253 | self.writeresponse( 'Nope, did nothing.')
254 |
255 |
256 |
257 | if __name__ == '__main__':
258 |
259 | if SERVERPROTOCOL == 'SSH':
260 | # Import the SSH stuff
261 | # If we're using gevent, we have to monkey patch for the paramiko and paramiko_ssh libraries
262 | if SERVERTYPE == 'green':
263 | from gevent import monkey; monkey.patch_all()
264 |
265 | if SERVERTYPE == 'eventlet':
266 | eventlet.monkey_patch(all=True)
267 |
268 | from telnetsrv.paramiko_ssh import SSHHandler, getRsaKeyFile
269 |
270 |
271 | # Create the handler for SSH, register the defined handler for use as the PTY
272 | class TestSSHHandler(SSHHandler):
273 | telnet_handler = TestTelnetHandler
274 | # Create or open the server key file
275 | host_key = getRsaKeyFile("server_rsa.key")
276 |
277 | # Define which handler the server should use:
278 | Handler = TestSSHHandler
279 | else:
280 | Handler = TestTelnetHandler
281 |
282 | if SERVERTYPE == 'green':
283 | # Multi-green-threaded server
284 | server = gevent.server.StreamServer((TELNET_IP_BINDING, TELNET_PORT_BINDING), Handler.streamserver_handle)
285 |
286 | if SERVERTYPE == 'eventlet':
287 |
288 | class EvtletStreamServer(object):
289 | def __init__(self, addr, handle):
290 | self.ip = addr[0]
291 | self.port = addr[1]
292 | self.handle = handle
293 |
294 | def serve_forever(self):
295 | eventlet.serve(
296 | eventlet.listen((self.ip, self.port)),
297 | self.handle
298 | )
299 |
300 | # Multi-eventlet server
301 | server = EvtletStreamServer(
302 | (TELNET_IP_BINDING, TELNET_PORT_BINDING),
303 | Handler.streamserver_handle
304 | )
305 |
306 | if SERVERTYPE == 'threaded':
307 | # Single threaded server - only one session at a time
308 | class TelnetServer(SocketServer.TCPServer):
309 | allow_reuse_address = True
310 |
311 | server = TelnetServer((TELNET_IP_BINDING, TELNET_PORT_BINDING), Handler)
312 |
313 |
314 | logging.info("Starting %s %s server at port %d. (Ctrl-C to stop)" % (SERVERTYPE, SERVERPROTOCOL, TELNET_PORT_BINDING) )
315 | try:
316 | server.serve_forever()
317 | except KeyboardInterrupt:
318 | logging.info("Server shut down.")
319 |
--------------------------------------------------------------------------------