This repository was archived by the owner on Dec 21, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathtelnet_server.py
More file actions
348 lines (269 loc) · 10.9 KB
/
telnet_server.py
File metadata and controls
348 lines (269 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
"""
Web and telnet server for the deployment scripts.
Note that this module is using global variables, it should
never be initialized more than once.
"""
from twisted.conch import telnet
from twisted.conch.telnet import StatefulTelnetProtocol, TelnetTransport
from twisted.internet import reactor, abstract, fdesc
from twisted.internet.protocol import ServerFactory
from twisted.internet.threads import deferToThread
from deployer import std
from deployer.cli import HandlerType
from deployer.console import NoInput, Console
from deployer.loggers import LoggerInterface
from deployer.loggers.default import DefaultLogger
from deployer.options import Options
from deployer.pseudo_terminal import Pty
from deployer.shell import Shell, ShellHandler
from contextlib import nested
from termcolor import colored
from setproctitle import setproctitle
import logging
import os
import signal
import struct
import sys
__all__ = ('start',)
# =================[ Authentication backend ]=================
class Backend(object):
def authenticate(self, username, password):
# Return True when this username/password combination is correct.
raise NotImplementedError
# =================[ Shell extensions ]=================
class WebHandlerType(HandlerType):
color = 'cyan'
postfix = '~'
class ActiveSessionsHandler(ShellHandler):
"""
List active deployment sessions. (Telnet + HTTP)
"""
is_leaf = True
handler_type = WebHandlerType()
def __call__(self):
print colored('%-12s %s' % ('Username', 'What'), 'cyan')
for session in self.shell.session.protocol.factory.connectionPool:
print '%-12s' % (session.username or 'somebody'),
print '%s' % ((session.shell.currently_running if session.shell else None) or '(Idle)')
class WebShell(Shell):
"""
The shell that we provide via telnet exposes some additional commands for
session and user management and logging.
"""
@property
def extensions(self):
return { 'w': ActiveSessionsHandler, }
def __init__(self, *a, **kw):
username = kw.pop('username')
Shell.__init__(self, *a, **kw)
self.username = username
@property
def prompt(self):
"""
Return a list of [ (text, color) ] tuples representing the prompt.
"""
if self.username:
return [ (self.username, 'cyan'), ('@', None) ] + super(WebShell, self).prompt
else:
return super(WebShell, self).prompt
# =================[ Text based authentication ]=================
class NotAuthenticated(Exception):
pass
def pty_based_auth(auth_backend, pty):
"""
Show a username/password login prompt.
Return username when authentication succeeded.
"""
tries = 0
while True:
# Password authentication required for this session?
sys.stdout.write('\033[2J\033[0;0H') # Clear screen
sys.stdout.write(colored('Please authenticate\r\n\r\n', 'cyan'))
if tries:
sys.stdout.write(colored(' Authentication failed, try again\r\n', 'red'))
try:
console = Console(pty)
username = console.input('Username', False)
password = console.input('Password', True)
except NoInput:
raise NotAuthenticated
if auth_backend.authenticate(username, password):
sys.stdout.write(colored(' ' * 40 + 'Authentication successful\r\n\r\n', 'green'))
return username
else:
tries += 1
if tries == 3:
raise NotAuthenticated
# =================[ Session handling ]=================
class Session(object):
"""
Create a pseudo terminal and run a deployment session in it.
(using a separate thread.)
"""
def __init__(self, protocol, writeCallback=None, doneCallback=None):
self.protocol = protocol
self.root_node = protocol.transport.factory.root_node
self.auth_backend = protocol.transport.factory.auth_backend
self.extra_loggers = protocol.transport.factory.extra_loggers
self.doneCallback = doneCallback
self.writeCallback = writeCallback
self.username = None
# Create PTY
self.master, self.slave = os.openpty()
# File descriptors for the shell
self.shell_in = self.shell_out = os.fdopen(self.master, 'r+w', 0)
# File descriptors for slave pty.
stdin = stdout = os.fdopen(self.slave, 'r+w', 0)
# Create pty object, for passing to deployment environment.
self.pty = Pty(stdin, stdout)
def start(self):
def thread():
"""
Run the shell in a normal synchronous thread.
"""
# Set stdin/out pair for this thread.
sys.stdout.set_handler(self.pty.stdout)
sys.stdin.set_handler(self.pty.stdin)
# Authentication
try:
self.username = pty_based_auth(self.auth_backend, self.pty) if self.auth_backend else ''
authenticated = True
except NotAuthenticated:
authenticated = False
if authenticated:
# Create loggers
logger_interface = LoggerInterface()
in_shell_logger = DefaultLogger(self.pty.stdout, print_group=False)
# Create options.
options = Options()
# Run shell
shell = WebShell(self.root_node, self.pty, options, logger_interface, username=self.username)
shell.session = self # Assign session to shell
self.shell = shell
with logger_interface.attach_in_block(in_shell_logger):
with nested(* [logger_interface.attach_in_block(l) for l in self.extra_loggers]):
shell.cmdloop()
# Remove references (shell and session had circular reference)
self.shell = None
shell.session = None
# Write last dummy character to trigger the session_closed.
# (telnet session will otherwise wait for enter keypress.)
sys.stdout.write(' ')
# Remove std handlers for this thread.
sys.stdout.del_handler()
sys.stdin.del_handler()
if self.doneCallback:
self.doneCallback()
# Stop IO reader
reactor.callFromThread(self.reader.stopReading)
deferToThread(thread)
# Start IO reader
self.reader = SelectableFile(self.shell_out, self.writeCallback)
self.reader.startReading()
class SelectableFile(abstract.FileDescriptor):
"""
Monitor a file descriptor, and call the callback
when something is ready to read from this file.
"""
def __init__(self, fp, callback):
self.fp = fp
fdesc.setNonBlocking(fp)
self.callback = callback
self.fileno = self.fp.fileno
abstract.FileDescriptor.__init__(self, reactor)
def doRead(self):
buf = self.fp.read(4096)
if buf:
self.callback(buf)
# =================[ Telnet interface ]=================
class TelnetDeployer(StatefulTelnetProtocol):
"""
Telnet interface
"""
def connectionMade(self):
logging.info('Connection made, starting new session')
# Start raw (for the line receiver)
self.setRawMode()
# Handle window size answers
self.transport.negotiationMap[telnet.NAWS] = self.telnet_NAWS
# Use a raw connection for ANSI terminals, more info:
# http://tools.ietf.org/html/rfc111/6
# http://s344.codeinspot.com/q/1492309
# 255, 253, 34, /* IAC DO LINEMODE */
self.transport.do(telnet.LINEMODE)
# 255, 250, 34, 1, 0, 255, 240 /* IAC SB LINEMODE MODE 0 IAC SE */
self.transport.requestNegotiation(telnet.LINEMODE, '\0')
# 255, 251, 1 /* IAC WILL ECHO */
self.transport.will(telnet.ECHO)
# Negotiate about window size
self.transport.do(telnet.NAWS)
# Start session
self.session = Session(self,
writeCallback=lambda data: self.transport.write(data),
doneCallback=lambda: self.transport.loseConnection())
self.factory.connectionPool.add(self.session)
self.session.start()
def connectionLost(self, reason):
self.factory.connectionPool.remove(self.session)
logging.info('Connection lost. Session ended')
def enableRemote(self, option):
#self.transport.write("You tried to enable %r (I rejected it)\r\n" % (option,))
return True # TODO:only return True for the values that we accept
def disableRemote(self, option):
#self.transport.write("You disabled %r\r\n" % (option,))
pass
#return True
def enableLocal(self, option):
#self.transport.write("You tried to make me enable %r (I rejected it)\r\n" % (option,))
return True
def disableLocal(self, option):
#self.transport.write("You asked me to disable %r\r\n" % (option,))
pass
#return True
def rawDataReceived(self, data):
self.session.shell_in.write(data)
self.session.shell_in.flush()
def telnet_NAWS(self, bytes):
# When terminal size is received from telnet client,
# set terminal size on pty object.
if len(bytes) == 4:
width, height = struct.unpack('!HH', ''.join(bytes))
self.session.pty.set_size(height, width)
else:
print "Wrong number of NAWS bytes"
# =================[ Startup]=================
def start(root_node, auth_backend=None, port=8023, logfile=None, extra_loggers=None):
"""
Start telnet server
"""
# Set logging
if logfile:
logging.basicConfig(filename=logfile, level=logging.DEBUG)
else:
logging.basicConfig(filename='/dev/stdout', level=logging.DEBUG)
# Thread sensitive interface for stdout/stdin
std.setup()
# Telnet
factory = ServerFactory()
factory.connectionPool = set() # List of currently, active connections
factory.protocol = lambda: TelnetTransport(TelnetDeployer)
factory.root_node = root_node()
factory.auth_backend = auth_backend
factory.extra_loggers = extra_loggers or []
# Handle signals
def handle_sigint(signal, frame):
if factory.connectionPool:
logging.info('Running, %i active session(s).' % len(factory.connectionPool))
else:
logging.info('No active sessions, exiting')
reactor.stop()
signal.signal(signal.SIGINT, handle_sigint)
# Run the reactor!
logging.info('Listening for incoming telnet connections on localhost:%s...' % port)
# Set process name
suffix = (' --log "%s"' % logfile if logfile else '')
setproctitle('deploy:%s telnet-server --port %i%s' %
(root_node.__class__.__name__, port, suffix))
# Run server
reactor.listenTCP(port, factory)
reactor.run()