3 """Back door shell server
5 This exposes an shell terminal on a socket.
7 --hostname : sets the remote host name to open an ssh connection to.
8 --username : sets the user name to login with
9 --password : (optional) sets the password to login with
10 --port : set the local port for the server to listen on
11 --watch : show the virtual screen after each client request
14 # Having the password on the command line is not a good idea, but
15 # then this entire project is probably not the most security concious thing
16 # I've ever built. This should be considered an experimental tool -- at best.
17 import pxssh, pexpect, ANSI
18 import time, sys, os, getopt, getpass, traceback, threading, socket
20 def exit_with_usage(exit_code=1):
22 print globals()['__doc__']
25 class roller (threading.Thread):
27 """This runs a function in a loop in a thread."""
29 def __init__(self, interval, function, args=[], kwargs={}):
31 """The interval parameter defines time between each call to the function.
34 threading.Thread.__init__(self)
35 self.interval = interval
36 self.function = function
39 self.finished = threading.Event()
43 """Stop the roller."""
49 while not self.finished.isSet():
50 # self.finished.wait(self.interval)
51 self.function(*self.args, **self.kwargs)
53 def endless_poll (child, prompt, screen, refresh_timeout=0.1):
55 """This keeps the screen updated with the output of the child. This runs in
56 a separate thread. See roller(). """
58 #child.logfile_read = screen
60 s = child.read_nonblocking(4000, 0.1)
65 # #child.prompt (timeout=refresh_timeout)
67 # #child.read_nonblocking(1,timeout=refresh_timeout)
68 # child.read_nonblocking(4000, 0.1)
72 def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
74 '''This forks the current process into a daemon. Almost none of this is
75 necessary (or advisable) if your daemon is being started by inetd. In that
76 case, stdin, stdout and stderr are all set up for you to refer to the
77 network connection, and the fork()s and session manipulation should not be
78 done (to avoid confusing inetd). Only the chdir() and umask() steps remain
83 1.7 How do I get my program to act like a daemon?
84 http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
86 Advanced Programming in the Unix Environment
87 W. Richard Stevens, 1992, Addison-Wesley, ISBN 0-201-56317-7.
89 The stdin, stdout, and stderr arguments are file names that will be opened
90 and be used to replace the standard file descriptors in sys.stdin,
91 sys.stdout, and sys.stderr. These arguments are optional and default to
92 /dev/null. Note that stderr is opened unbuffered, so if it shares a file
93 with stdout then interleaved output may not appear in the order that you
100 sys.exit(0) # Exit first parent.
102 sys.stderr.write ("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror) )
105 # Decouple from parent environment.
114 sys.exit(0) # Exit second parent.
116 sys.stderr.write ("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror) )
121 # Redirect standard file descriptors.
122 si = open(stdin, 'r')
123 so = open(stdout, 'a+')
124 se = open(stderr, 'a+', 0)
125 os.dup2(si.fileno(), sys.stdin.fileno())
126 os.dup2(so.fileno(), sys.stdout.fileno())
127 os.dup2(se.fileno(), sys.stderr.fileno())
129 # I now return as the daemon
132 def add_cursor_blink (response, row, col):
134 i = (row-1) * 80 + col
135 return response[:i]+'<img src="http://www.noah.org/cursor.gif">'+response[i:]
140 optlist, args = getopt.getopt(sys.argv[1:], 'h?d', ['help','h','?', 'hostname=', 'username=', 'password=', 'port=', 'watch'])
145 command_line_options = dict(optlist)
146 options = dict(optlist)
147 # There are a million ways to cry for help. These are but a few of them.
148 if [elem for elem in command_line_options if elem in ['-h','--h','-?','--?','--help']]:
151 hostname = "127.0.0.1"
153 username = os.getenv('USER')
158 if '--watch' in options:
162 if '--hostname' in options:
163 hostname = options['--hostname']
164 if '--port' in options:
165 port = int(options['--port'])
166 if '--username' in options:
167 username = options['--username']
168 print "Login for %s@%s:%s" % (username, hostname, port)
169 if '--password' in options:
170 password = options['--password']
172 password = getpass.getpass('password: ')
175 print "daemonizing server"
177 #daemonize('/dev/null','/tmp/daemon.log','/tmp/daemon.log')
179 sys.stdout.write ('server started with pid %d\n' % os.getpid() )
181 virtual_screen = ANSI.ANSI (24,80)
182 child = pxssh.pxssh()
183 child.login (hostname, username, password)
184 print 'created shell. command line prompt is', child.PROMPT
185 #child.sendline ('stty -echo')
186 #child.setecho(False)
187 virtual_screen.write (child.before)
188 virtual_screen.write (child.after)
190 if os.path.exists("/tmp/mysock"): os.remove("/tmp/mysock")
191 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
192 localhost = '127.0.0.1'
193 s.bind('/tmp/mysock')
194 os.chmod('/tmp/mysock',0777)
198 #s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
199 #localhost = '127.0.0.1'
200 #s.bind((localhost, port))
204 r = roller (0.01, endless_poll, (child, child.PROMPT, virtual_screen))
206 print "screen poll updater started in background thread"
211 conn, addr = s.accept()
212 print 'Connected by', addr
213 data = conn.recv(1024)
218 request = data.split(' ', 1)
220 cmd = request[0].strip()
221 arg = request[1].strip()
223 cmd = request[0].strip()
227 elif cmd == ':sendline':
229 #child.prompt(timeout=2)
231 shell_window = str(virtual_screen)
232 elif cmd == ':send' or cmd==':xsend':
234 arg = arg.decode("hex")
237 shell_window = str(virtual_screen)
238 elif cmd == ':cursor':
239 shell_window = '%x%x' % (virtual_screen.cur_r, virtual_screen.cur_c)
240 elif cmd == ':refresh':
241 shell_window = str(virtual_screen)
244 response.append (shell_window)
245 #response = add_cursor_blink (response, row, col)
246 sent = conn.send('\n'.join(response))
247 if watch_mode: print '\n'.join(response)
248 if sent < len (response):
249 print "Sent is too short. Some data was cut off."
253 print "cleaning up socket"
255 if os.path.exists("/tmp/mysock"): os.remove("/tmp/mysock")
258 def pretty_box (rows, cols, s):
260 """This puts an ASCII text box around the given string, s.
263 top_bot = '+' + '-'*cols + '+\n'
264 return top_bot + '\n'.join(['|'+line+'|' for line in s.split('\n')]) + '\n' + top_bot
266 def error_response (msg):
269 response.append ("""All commands start with :
270 :{REQUEST} {ARGUMENT}
271 {REQUEST} may be one of the following:
272 :sendline: Run the ARGUMENT followed by a line feed.
273 :send : send the characters in the ARGUMENT without a line feed.
274 :refresh : Use to catch up the screen with the shell if state gets out of sync.
277 You may also leave off :command and it will be assumed.
283 response.append (msg)
284 return '\n'.join(response)
286 def parse_host_connect_string (hcs):
288 """This parses a host connection string in the form
289 username:password@hostname:port. All fields are options expcet hostname. A
290 dictionary is returned with all four keys. Keys that were not included are
291 set to empty strings ''. Note that if your password has the '@' character
292 then you must backslash escape it. """
295 p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
297 p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
300 d['password'] = d['password'].replace('\\@','@')
303 if __name__ == "__main__":
306 start_time = time.time()
310 print "TOTAL TIME IN MINUTES:",
311 print (time.time() - start_time) / 60.0
314 tb_dump = traceback.format_exc()