5 This exposes a shell terminal on a web page.
6 It uses AJAX to send keys and receive screen updates.
7 The client web browser needs nothing but CSS and Javascript.
9 --hostname : sets the remote host name to open an ssh connection to.
10 --username : sets the user name to login with
11 --password : (optional) sets the password to login with
12 --port : set the local port for the server to listen on
13 --watch : show the virtual screen after each client request
15 This project is probably not the most security concious thing I've ever built.
16 This should be considered an experimental tool -- at best.
19 sys.path.insert (0,os.getcwd()) # let local modules precede any installed modules
20 import socket, random, string, traceback, cgi, time, getopt, getpass, threading, resource, signal
21 import pxssh, pexpect, ANSI
23 def exit_with_usage(exit_code=1):
24 print globals()['__doc__']
27 def client (command, host='localhost', port=-1):
28 """This sends a request to the server and returns the response.
29 If port <= 0 then host is assumed to be the filename of a Unix domain socket.
30 If port > 0 then host is an inet hostname.
33 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
36 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
37 s.connect((host, port))
43 def server (hostname, username, password, socket_filename='/tmp/server_sock', daemon_mode = True, verbose=False):
44 """This starts and services requests from a client.
45 If daemon_mode is True then this forks off a separate daemon process and returns the daemon's pid.
46 If daemon_mode is False then this does not return until the server is done.
49 mypid_name = '/tmp/%d.pid' % os.getpid()
50 daemon_pid = daemonize(daemon_pid_filename=mypid_name)
56 virtual_screen = ANSI.ANSI (24,80)
59 child.login (hostname, username, password, login_naked=True)
62 if verbose: print 'login OK'
63 virtual_screen.write (child.before)
64 virtual_screen.write (child.after)
66 if os.path.exists(socket_filename): os.remove(socket_filename)
67 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
68 s.bind(socket_filename)
69 os.chmod(socket_filename, 0777)
70 if verbose: print 'Listen'
73 r = roller (endless_poll, (child, child.PROMPT, virtual_screen))
75 if verbose: print "started screen-poll-updater in background thread"
79 conn, addr = s.accept()
80 if verbose: print 'Connected by', addr
81 data = conn.recv(1024)
82 request = data.split(' ', 1)
84 cmd = request[0].strip()
85 arg = request[1].strip()
87 cmd = request[0].strip()
93 elif cmd == 'sendline':
96 shell_window = str(virtual_screen)
97 elif cmd == 'send' or cmd=='xsend':
99 arg = arg.decode("hex")
102 shell_window = str(virtual_screen)
103 elif cmd == 'cursor':
104 shell_window = '%x,%x' % (virtual_screen.cur_r, virtual_screen.cur_c)
105 elif cmd == 'refresh':
106 shell_window = str(virtual_screen)
108 shell_window = str(hash(str(virtual_screen)))
111 response.append (shell_window)
112 if verbose: print '\n'.join(response)
113 sent = conn.send('\n'.join(response))
114 if sent < len (response):
115 if verbose: print "Sent is too short. Some data was cut off."
120 if verbose: print "cleaning up socket"
122 if os.path.exists(socket_filename): os.remove(socket_filename)
123 if verbose: print "server done!"
125 class roller (threading.Thread):
126 """This class continuously loops a function in a thread.
127 This is basically a thin layer around Thread with a
128 while loop and a cancel.
130 def __init__(self, function, args=[], kwargs={}):
131 threading.Thread.__init__(self)
132 self.function = function
135 self.finished = threading.Event()
137 """Stop the roller."""
140 while not self.finished.isSet():
141 self.function(*self.args, **self.kwargs)
143 def endless_poll (child, prompt, screen, refresh_timeout=0.1):
144 """This keeps the screen updated with the output of the child.
145 This will be run in a separate thread. See roller class.
147 #child.logfile_read = screen
149 s = child.read_nonblocking(4000, 0.1)
154 def daemonize (stdin=None, stdout=None, stderr=None, daemon_pid_filename=None):
155 """This runs the current process in the background as a daemon.
156 The arguments stdin, stdout, stderr allow you to set the filename that the daemon reads and writes to.
157 If they are set to None then all stdio for the daemon will be directed to /dev/null.
158 If daemon_pid_filename is set then the pid of the daemon will be written to it as plain text
159 and the pid will be returned. If daemon_pid_filename is None then this will return None.
165 # The stdio file descriptors are redirected to /dev/null by default.
166 if hasattr(os, "devnull"):
169 DEVNULL = "/dev/null"
170 if stdin is None: stdin = DEVNULL
171 if stdout is None: stdout = DEVNULL
172 if stderr is None: stderr = DEVNULL
177 raise Exception, "%s [%d]" % (e.strerror, e.errno)
179 if pid != 0: # The first child.
181 if daemon_pid_filename is not None:
182 daemon_pid = int(file(daemon_pid_filename,'r').read())
189 signal.signal(signal.SIGHUP, signal.SIG_IGN)
192 pid = os.fork() # fork second child
194 raise Exception, "%s [%d]" % (e.strerror, e.errno)
197 if daemon_pid_filename is not None:
198 file(daemon_pid_filename,'w').write(str(pid))
199 os._exit(0) # exit parent (the first child) of the second child.
205 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
206 if maxfd == resource.RLIM_INFINITY:
209 # close all file descriptors
210 for fd in xrange(0, maxfd):
213 except OSError: # fd wasn't open to begin with (ignored)
216 os.open (DEVNULL, os.O_RDWR) # standard input
218 # redirect standard file descriptors
219 si = open(stdin, 'r')
220 so = open(stdout, 'a+')
221 se = open(stderr, 'a+', 0)
222 os.dup2(si.fileno(), sys.stdin.fileno())
223 os.dup2(so.fileno(), sys.stdout.fileno())
224 os.dup2(se.fileno(), sys.stderr.fileno())
229 """This handles the request if this script was called as a cgi.
231 sys.stderr = sys.stdout
236 print "Content-type: text/html;charset=utf-8\r\n"
238 form = cgi.FieldStorage()
239 if form.has_key('ajax'):
241 ajax_cmd = form['ajax'].value
242 SID=form['sid'].value
243 if ajax_cmd == 'send':
245 arg = form['arg'].value.encode('hex')
246 result = client (command + ' ' + arg, '/tmp/'+SID)
248 elif ajax_cmd == 'refresh':
250 result = client (command, '/tmp/'+SID)
252 elif ajax_cmd == 'cursor':
254 result = client (command, '/tmp/'+SID)
256 elif ajax_cmd == 'exit':
258 result = client (command, '/tmp/'+SID)
260 elif ajax_cmd == 'hash':
262 result = client (command, '/tmp/'+SID)
264 elif not form.has_key('sid'):
266 print LOGIN_HTML % locals();
268 SID=form['sid'].value
269 if form.has_key('start_server'):
270 USERNAME = form['username'].value
271 PASSWORD = form['password'].value
272 dpid = server ('127.0.0.1', USERNAME, PASSWORD, '/tmp/'+SID)
273 SHELL_OUTPUT="daemon pid: " + str(dpid)
275 if form.has_key('cli'):
276 command = 'sendline ' + form['cli'].value
279 SHELL_OUTPUT = client (command, '/tmp/'+SID)
280 print CGISH_HTML % locals()
282 tb_dump = traceback.format_exc()
286 SHELL_OUTPUT=str(tb_dump)
287 print CGISH_HTML % locals()
290 """This is the command line interface to starting the server.
291 This handles things if the script was not called as a CGI
292 (if you run it from the command line).
295 optlist, args = getopt.getopt(sys.argv[1:], 'h?d', ['help','h','?', 'hostname=', 'username=', 'password=', 'port=', 'watch'])
300 command_line_options = dict(optlist)
301 options = dict(optlist)
302 # There are a million ways to cry for help. These are but a few of them.
303 if [elem for elem in command_line_options if elem in ['-h','--h','-?','--?','--help']]:
306 hostname = "127.0.0.1"
308 username = os.getenv('USER')
313 if '--watch' in options:
317 if '--hostname' in options:
318 hostname = options['--hostname']
319 if '--port' in options:
320 port = int(options['--port'])
321 if '--username' in options:
322 username = options['--username']
323 if '--password' in options:
324 password = options['--password']
326 password = getpass.getpass('password: ')
328 server (hostname, username, password, '/tmp/mysock', daemon_mode)
331 a=random.randint(0,65535)
332 b=random.randint(0,65535)
333 return '%04x%04x.sid' % (a,b)
335 def parse_host_connect_string (hcs):
336 """This parses a host connection string in the form
337 username:password@hostname:port. All fields are options expcet hostname. A
338 dictionary is returned with all four keys. Keys that were not included are
339 set to empty strings ''. Note that if your password has the '@' character
340 then you must backslash escape it.
343 p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
345 p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)')
348 d['password'] = d['password'].replace('\\@','@')
351 def pretty_box (s, rows=24, cols=80):
352 """This puts an ASCII text box around the given string.
354 top_bot = '+' + '-'*cols + '+\n'
355 return top_bot + '\n'.join(['|'+line+'|' for line in s.split('\n')]) + '\n' + top_bot
358 if os.getenv('REQUEST_METHOD') is None:
363 # It's mostly HTML and Javascript from here on out.
364 CGISH_HTML="""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
367 <title>%(TITLE)s %(SID)s</title>
368 <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
369 <style type=text/css>
370 a {color: #9f9; text-decoration: none}
371 a:hover {color: #0f0}
373 html,body,textarea,input,form
375 font-family: "Courier New", Courier, mono;
378 background-color: #020;
383 input { background-color: #010; }
393 <script language="JavaScript">
394 function focus_first()
395 {if (document.forms.length > 0)
396 {var TForm = document.forms[0];
397 for (i=0;i<TForm.length;i++){
398 if ((TForm.elements[i].type=="text")||
399 (TForm.elements[i].type=="textarea")||
400 (TForm.elements[i].type.toString().charAt(0)=="s"))
401 {document.forms[0].elements[i].focus();break;}}}}
403 // JavaScript Virtual Keyboard
404 // If you like this code then buy me a sandwich.
407 var flag_shiftlock=0;
409 var ButtonOnColor="#ee0";
413 // hack to set quote key to show both single quote and double quote
414 document.form['quote'].value = "'" + ' "';
417 document.form["cli"].focus();
419 function get_password ()
421 var username = prompt("username?","");
422 var password = prompt("password?","");
423 start_server (username, password);
425 function multibrowser_ajax ()
429 /*@if (@_jscript_version >= 5)
432 xmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
438 xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
447 if (!xmlHttp && typeof XMLHttpRequest != 'undefined')
449 xmlHttp = new XMLHttpRequest();
453 function load_url_to_screen(url)
455 xmlhttp = multibrowser_ajax();
456 //window.XMLHttpRequest?new XMLHttpRequest(): new ActiveXObject("Microsoft.XMLHTTP");
457 xmlhttp.onreadystatechange = update_virtual_screen;
458 xmlhttp.open("GET", url);
459 xmlhttp.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
462 function update_virtual_screen()
464 if ((xmlhttp.readyState == 4) && (xmlhttp.status == 200))
466 var screen_text = xmlhttp.responseText;
467 document.form["screen_text"].value = screen_text;
468 //var json_data = json_parse(xmlhttp.responseText);
474 timerID = setTimeout("poll()", 2000);
475 // clearTimeout(timerID);
477 //function start_server (username, password)
479 // load_url_to_screen('cgishell.cgi?ajax=serverstart&username=' + escape(username) + '&password=' + escape(password);
481 function refresh_screen()
483 load_url_to_screen('cgishell.cgi?ajax=refresh&sid=%(SID)s');
485 function query_hash()
487 load_url_to_screen('cgishell.cgi?ajax=hash&sid=%(SID)s');
489 function query_cursor()
491 load_url_to_screen('cgishell.cgi?ajax=cursor&sid=%(SID)s');
493 function exit_server()
495 load_url_to_screen('cgishell.cgi?ajax=exit&sid=%(SID)s');
497 function type_key (chars)
500 if (flag_shiftlock || flag_shift)
502 ch = chars.substr(1,1);
506 ch = chars.substr(2,1);
510 ch = chars.substr(0,1);
512 load_url_to_screen('cgishell.cgi?ajax=send&sid=%(SID)s&arg=' + escape(ch));
513 if (flag_shift || flag_ctrl)
518 update_button_colors();
521 function key_shiftlock()
533 update_button_colors();
548 update_button_colors();
563 update_button_colors();
565 function update_button_colors ()
569 document.form['Ctrl'].style.backgroundColor = ButtonOnColor;
570 document.form['Ctrl2'].style.backgroundColor = ButtonOnColor;
574 document.form['Ctrl'].style.backgroundColor = document.form.style.backgroundColor;
575 document.form['Ctrl2'].style.backgroundColor = document.form.style.backgroundColor;
579 document.form['Shift'].style.backgroundColor = ButtonOnColor;
580 document.form['Shift2'].style.backgroundColor = ButtonOnColor;
584 document.form['Shift'].style.backgroundColor = document.form.style.backgroundColor;
585 document.form['Shift2'].style.backgroundColor = document.form.style.backgroundColor;
589 document.form['ShiftLock'].style.backgroundColor = ButtonOnColor;
593 document.form['ShiftLock'].style.backgroundColor = document.form.style.backgroundColor;
597 function keyHandler(e)
600 if (document.all) { e = window.event; }
601 if (document.layers) { pressedKey = e.which; }
602 if (document.all) { pressedKey = e.keyCode; }
603 pressedCharacter = String.fromCharCode(pressedKey);
604 type_key(pressedCharacter+pressedCharacter+pressedCharacter);
605 alert(pressedCharacter);
606 // alert(' Character = ' + pressedCharacter + ' [Decimal value = ' + pressedKey + ']');
608 //document.onkeypress = keyHandler;
609 //if (document.layers)
610 // document.captureEvents(Event.KEYPRESS);
611 //http://sniptools.com/jskeys
612 //document.onkeyup = KeyCheck;
615 var KeyID = (window.event) ? event.keyCode : e.keyCode;
616 type_key(String.fromCharCode(KeyID));
617 e.cancelBubble = true;
618 window.event.cancelBubble = true;
624 <body onload="init()">
625 <form id="form" name="form" action="/cgi-bin/cgishell.cgi" method="POST">
626 <input name="sid" value="%(SID)s" type="hidden">
627 <textarea name="screen_text" cols="81" rows="25">%(SHELL_OUTPUT)s</textarea>
629 <input name="cli" id="cli" type="text" size="80"><br>
630 <table border="0" align="left">
632 <td width="86%%" align="center">
633 <input name="submit" type="submit" value="Submit">
634 <input name="refresh" type="button" value="REFRESH" onclick="refresh_screen()">
635 <input name="refresh" type="button" value="CURSOR" onclick="query_cursor()">
636 <input name="hash" type="button" value="HASH" onclick="query_hash()">
637 <input name="exit" type="button" value="EXIT" onclick="exit_server()">
639 <input type="button" value="Esc" onclick="type_key('\\x1b\\x1b')" />
640 <input type="button" value="` ~" onclick="type_key('`~')" />
641 <input type="button" value="1!" onclick="type_key('1!')" />
642 <input type="button" value="2@" onclick="type_key('2@\\x00')" />
643 <input type="button" value="3#" onclick="type_key('3#')" />
644 <input type="button" value="4$" onclick="type_key('4$')" />
645 <input type="button" value="5%%" onclick="type_key('5%%')" />
646 <input type="button" value="6^" onclick="type_key('6^\\x1E')" />
647 <input type="button" value="7&" onclick="type_key('7&')" />
648 <input type="button" value="8*" onclick="type_key('8*')" />
649 <input type="button" value="9(" onclick="type_key('9(')" />
650 <input type="button" value="0)" onclick="type_key('0)')" />
651 <input type="button" value="-_" onclick="type_key('-_\\x1F')" />
652 <input type="button" value="=+" onclick="type_key('=+')" />
653 <input type="button" value="BkSp" onclick="type_key('\\x08\\x08\\x08')" />
655 <input type="button" value="Tab" onclick="type_key('\\t\\t')" />
656 <input type="button" value="Q" onclick="type_key('qQ\\x11')" />
657 <input type="button" value="W" onclick="type_key('wW\\x17')" />
658 <input type="button" value="E" onclick="type_key('eE\\x05')" />
659 <input type="button" value="R" onclick="type_key('rR\\x12')" />
660 <input type="button" value="T" onclick="type_key('tT\\x14')" />
661 <input type="button" value="Y" onclick="type_key('yY\\x19')" />
662 <input type="button" value="U" onclick="type_key('uU\\x15')" />
663 <input type="button" value="I" onclick="type_key('iI\\x09')" />
664 <input type="button" value="O" onclick="type_key('oO\\x0F')" />
665 <input type="button" value="P" onclick="type_key('pP\\x10')" />
666 <input type="button" value="[ {" onclick="type_key('[{\\x1b')" />
667 <input type="button" value="] }" onclick="type_key(']}\\x1d')" />
668 <input type="button" value="\\ |" onclick="type_key('\\\\|\\x1c')" />
670 <input type="button" id="Ctrl" value="Ctrl" onclick="key_ctrl()" />
671 <input type="button" value="A" onclick="type_key('aA\\x01')" />
672 <input type="button" value="S" onclick="type_key('sS\\x13')" />
673 <input type="button" value="D" onclick="type_key('dD\\x04')" />
674 <input type="button" value="F" onclick="type_key('fF\\x06')" />
675 <input type="button" value="G" onclick="type_key('gG\\x07')" />
676 <input type="button" value="H" onclick="type_key('hH\\x08')" />
677 <input type="button" value="J" onclick="type_key('jJ\\x0A')" />
678 <input type="button" value="K" onclick="type_key('kK\\x0B')" />
679 <input type="button" value="L" onclick="type_key('lL\\x0C')" />
680 <input type="button" value="; :" onclick="type_key(';:')" />
681 <input type="button" id="quote" value="'" onclick="type_key('\\x27\\x22')" />
682 <input type="button" value="Enter" onclick="type_key('\\n\\n')" />
684 <input type="button" id="ShiftLock" value="Caps Lock" onclick="key_shiftlock()" />
685 <input type="button" id="Shift" value="Shift" onclick="key_shift()" />
686 <input type="button" value="Z" onclick="type_key('zZ\\x1A')" />
687 <input type="button" value="X" onclick="type_key('xX\\x18')" />
688 <input type="button" value="C" onclick="type_key('cC\\x03')" />
689 <input type="button" value="V" onclick="type_key('vV\\x16')" />
690 <input type="button" value="B" onclick="type_key('bB\\x02')" />
691 <input type="button" value="N" onclick="type_key('nN\\x0E')" />
692 <input type="button" value="M" onclick="type_key('mM\\x0D')" />
693 <input type="button" value=", <" onclick="type_key(',<')" />
694 <input type="button" value=". >" onclick="type_key('.>')" />
695 <input type="button" value="/ ?" onclick="type_key('/?')" />
696 <input type="button" id="Shift2" value="Shift" onclick="key_shift()" />
697 <input type="button" id="Ctrl2" value="Ctrl" onclick="key_ctrl()" />
699 <input type="button" value=" FINAL FRONTIER " onclick="type_key(' ')" />
710 <title>Shell Login</title>
711 <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
712 <style type=text/css>
713 a {color: #9f9; text-decoration: none}
714 a:hover {color: #0f0}
716 html,body,textarea,input,form
718 font-family: "Courier New", Courier, mono;
721 background-color: #020;
726 input { background-color: #010; }
735 <script language="JavaScript">
738 document.login_form["username"].focus();
742 <body onload="init()">
743 <form name="login_form" method="POST">
744 <input name="start_server" value="1" type="hidden">
745 <input name="sid" value="%(SID)s" type="hidden">
746 username: <input name="username" type="text" size="30"><br>
747 password: <input name="password" type="password" size="30"><br>
748 <input name="submit" type="submit" value="enter">
755 if __name__ == "__main__":
760 tb_dump = traceback.format_exc()