#!/usr/bin/env python """ An rxvt-based terminal console from your browser. This depends on MochiKit for proper quoting. You need to provide the files as ./MochiKit/*.js. (Symlinks are your friends.) Note that this is known not to work with the mochikit trunk; I am successfully using revision 1148. """ # ____________________________________________________________ PASSWD_FILE = "secret/passwd" #PROGRAM = "../../rxvt-metadata/rxbuffer/rxbuffer -sl 2000" PROGRAM = "../../rxvt-metadata/rxbuffer/rxbuffer -sl 2000 -e ssh 127.0.0.1" PORT = 8058 # Server key and certificate files: # see help in https.py or use None and None for plain http HTTPS_SSL_KEY = 'secret/server.key' HTTPS_SSL_CRT = 'secret/server.crt' # ____________________________________________________________ import autopath import sys, os, cStringIO, select, array, thread, struct import traceback, htmlentitydefs, socket, sha, time, zlib from cgi import parse_qs from pypy.translator.js.modules.dom import setTimeout, document, window, alert from pypy.translator.js.modules import mochikit from pypy.rpython.ootypesystem.bltregistry import MethodDesc, BasicExternal from pypy.translator.js import commproxy from pypy.rpython.extfunc import genericcallable from pypy.translator.js.lib import support commproxy.USE_MOCHIKIT = True from SocketServer import ThreadingMixIn assert (HTTPS_SSL_KEY is None) == (HTTPS_SSL_CRT is None) if HTTPS_SSL_KEY is None: from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler else: from https import SecureHTTPServer as HTTPServer from https import SecureHTTPRequestHandler as BaseHTTPRequestHandler import keysymdefs KEYMAP = { 8: keysymdefs.XK_BackSpace, 9: keysymdefs.XK_Tab, 13: keysymdefs.XK_Return, 19: keysymdefs.XK_Break, 20: keysymdefs.XK_Caps_Lock, 27: keysymdefs.XK_Escape, 33: keysymdefs.XK_Prior, 34: keysymdefs.XK_Next, 35: keysymdefs.XK_End, 36: keysymdefs.XK_Home, 37: keysymdefs.XK_Left, 38: keysymdefs.XK_Up, 39: keysymdefs.XK_Right, 40: keysymdefs.XK_Down, 45: keysymdefs.XK_Insert, 46: keysymdefs.XK_Delete, 112: keysymdefs.XK_F1, 113: keysymdefs.XK_F2, 114: keysymdefs.XK_F3, 115: keysymdefs.XK_F4, 116: keysymdefs.XK_F5, 117: keysymdefs.XK_F6, 118: keysymdefs.XK_F7, 119: keysymdefs.XK_F8, 120: keysymdefs.XK_F9, 121: keysymdefs.XK_F10, 122: keysymdefs.XK_F11, 123: keysymdefs.XK_F12, } HTML_PAGE = """
""" CSS_PAGE_TEMPLATE = """ body,body.body { color: black; background: $BODYBG; } table.vtermtable { color: $FG; background: $BG; } div.vterm { display: block; font-family: monospace; } .fg1 { color: $BG } .fg2 { color: black } .fg3 { color: red } .fg4 { color: green } .fg5 { color: yellow } .fg6 { color: blue } .fg7 { color: fuchsia } .fg8 { color: aqua } .fg9 { color: white } .bg0 { background: $FG } .bg2 { background: black } .bg3 { background: red } .bg4 { background: green } .bg5 { background: yellow } .bg6 { background: blue } .bg7 { background: fuchsia } .bg8 { background: aqua } .bg9 { background: white } .bo { font-weight: bold } .ul { text-decoration: underline } """ CSS_PAGE_W = (CSS_PAGE_TEMPLATE.replace('$BG', 'white') .replace('$FG', 'black') .replace('$BODYBG', 'white')) CSS_PAGE_B = (CSS_PAGE_TEMPLATE.replace('$BG', 'black') .replace('$FG', '#E0E0E0') .replace('$BODYBG', '#C0C0C0')) # ____________________________________________________________ # Client code class ClientState(object): def __init__(self): self.password = "" self.error = -1 self.key_request_pending = False self.keybuffer = [] self.sessid = "" self.snum = -1 def make_key_request(self): k = 'x'.join([str(n) for n in self.keybuffer]) self.keybuffer = [] self.key_request_pending = True httpd.sendkey(self.sessid, k, key_request_comeback) def post_next_request(self): if not self.error: httpd.update(self.sessid, comeback) def setpwstatus(self): if self.error == -1: msg = "Password: " + "*" * len(self.password) else: msg = '' msg += ' ' * (80 - len(msg)) setdata(htmlquote(msg)) def screen_update(self, data): #alert('num=%s self.snum=%s scr=%s' % (data.get('num', ''), self.snum, # data.get('scr', '')[:20])) num = data.get('num', '') if num: num = int(num) if num > self.snum: scr = data.get('scr', '') if scr: self.snum = num setdata(scr) err = data.get('error', '') if err: setkeyhandler_ignored() setstatus(err) self.error = 1 clientstate = ClientState() httpd = None def setdata(scr): data_elem = document.getElementById("data") data_elem.innerHTML = scr def setstatus(msg): status_elem = document.getElementById("status") status_elem.innerHTML = msg class Global(object): pass g = Global() g.charcode = 0 def keyhandler(evt): event = evt._event buf = clientstate.keybuffer expect_keypress = True if event.type == 'keydown': if not window.navigator.appName.startswith('Microsoft'): return # Mozilla's charcode will be saved later kk = event.keyCode g.charcode = kk # keypress is not emitted for certain keys in IE if kk in KEYMAP: expect_keypress = False if not expect_keypress or event.type == 'keypress': evt.preventDefault() event.cancelBubble = True kk = event.keyCode kc = event.charCode if not (kk or kc): return modifiers = 0 if event.shiftKey: modifiers |= keysymdefs.ShiftMask if event.ctrlKey: modifiers |= keysymdefs.ControlMask if event.altKey: modifiers |= keysymdefs.Mod1Mask if window.navigator.appName.startswith('Microsoft'): buf.append(kk) buf.append(g.charcode) else: buf.append(kc) buf.append(kk) buf.append(modifiers) if not clientstate.key_request_pending: clientstate.make_key_request() def handle_pw_backspace(clientstate): if clientstate.password: clientstate.password = clientstate.password[:-1] def handle_pw_enter(clientstate): setkeyhandler_ignored() pwd = clientstate.password clientstate.password = "" clientstate.error = 0 httpd.screenshot(pwd, login_comeback) def keyhandler_pw(evt): event = evt._event if event.type == 'keydown': if window.navigator.appName == 'Netscape': return # Mozilla's charcode will be saved later kk = event.keyCode if kk == 8: handle_pw_backspace(clientstate) elif kk == 13: handle_pw_enter(clientstate) elif event.type == 'keypress': evt.preventDefault() kk = event.keyCode kc = event.charCode if not (kk or kc): return if window.navigator.appName != 'Netscape': kc = kk if kc >= 32: clientstate.password += chr(kc) if window.navigator.appName == 'Netscape': if kk == 8: # backspace handle_pw_backspace(clientstate) elif kk == 13: # enter handle_pw_enter(clientstate) clientstate.setpwstatus() def setkeyhandler_ignored(): mochikit.disconnectAll(document, 'onkeydown') mochikit.disconnectAll(document, 'onkeypress') def setkeyhandler_normal(): mochikit.disconnectAll(document, 'onkeydown') mochikit.disconnectAll(document, 'onkeypress') mochikit.connect(document, 'onkeydown', keyhandler) mochikit.connect(document, 'onkeypress', keyhandler) def setkeyhandler_pw(): mochikit.disconnectAll(document, 'onkeydown') mochikit.disconnectAll(document, 'onkeypress') mochikit.connect(document, 'onkeydown', keyhandler_pw) mochikit.connect(document, 'onkeypress', keyhandler_pw) def blackelem(): return document.getElementById("black") def comeback(data): clientstate.screen_update(data) clientstate.post_next_request() def key_request_comeback(data): clientstate.screen_update(data) clientstate.key_request_pending = False if clientstate.keybuffer: clientstate.make_key_request() if not clientstate.error: status_elem = document.getElementById("status") status_elem.innerHTML = data.get('status', '') def login_comeback(data): clientstate.screen_update(data) sessid = data.get('sid', '') if sessid: setkeyhandler_normal() clientstate.sessid = sessid clientstate.post_next_request() def black_change(event): set_css_link() def set_css_link(): if blackelem().checked: url = "css/black.css" else: url = "css/style.css" csslnk = document.getElementById("csslnk") csslnk.setAttribute('href', url) blackelem().blur() def setup_page(): setkeyhandler_pw() clientstate.setpwstatus() mochikit.connect(blackelem(), 'onchange', black_change) mochikit.connect(blackelem(), 'onfocus', black_change) set_css_link() # ____________________________________________________________ # Server code ##class MyLock: ## def __init__(self): ## self._lock = thread.allocate_lock() ## def acquire(self): ## if not self._lock.acquire(False): ## print 'WAITING ON LOCK...' ## self._lock.acquire() ## print 'ACQUIRED' ## def release(self): ## self._lock.release() class Server(ThreadingMixIn, HTTPServer, BasicExternal): # Methods and signatures how they are rendered for JS _methods = { 'screenshot' : MethodDesc([('pwd', str), ('callback', genericcallable([{str:str}]))], {str:str}), 'update' : MethodDesc([('sid', str), ('callback', genericcallable([{str:str}]))], {str:str}), 'sendkey' : MethodDesc([('sid', str), ('k', str), ('callback', genericcallable([{str:str}]))], {str:str}), } _render_xmlhttp = True server_key_file = HTTPS_SSL_KEY certificate_file = HTTPS_SSL_CRT def __init__(self, *args, **kwargs): HTTPServer.__init__(self, *args, **kwargs) self.source = None self.lock = thread.allocate_lock() self.reset() def reset(self): self.io = None self.io_inbuf = '' self.session_id = None self.next_snum = 0 self.last_sshot = None def get_source(self): if self.source is None: self.lock.acquire() try: self.source = support.js_source([setup_page]) finally: self.lock.release() return self.source def io_wait_for_data(self, timeout): id = ord(os.urandom(1)) #print '[%d] io_wait_for_data timeout=%s' % (id, timeout) try: child_in, child_out = self.io except TypeError: # self.io is None raise EOFError("subprocess finished") if self.io_inbuf: #print '[%d] True (io_inbuf)' % (id,) return True else: iwtd, owtd, ewtd = select.select([child_out], [], [], timeout) #print '[%d] %s' % (id, bool(iwtd)) return len(iwtd) def io_readline(self): child_in, child_out = self.io fd = child_out.fileno() inbuf = self.io_inbuf while '\n' not in inbuf: t = os.read(fd, 512) if not t: length = len(inbuf) break inbuf += t else: length = inbuf.index('\n') + 1 self.io_inbuf = inbuf[length:] return inbuf[:length] def io_write(self, buf): child_in, child_out = self.io child_in.write(buf) def newsession(self, passwd): self.lock.acquire() try: passwd = sha.sha(passwd).hexdigest() f = open(PASSWD_FILE) # run ./passwd.py to set a password expected = f.read().strip() f.close() ok = passwd == expected del passwd, expected # hide them from potential tracebacks if not ok: time.sleep(1.0) raise EOFError("invalid password") if self.io is None: self.io = os.popen2(PROGRAM, bufsize=0) self.session_id = os.urandom(13).encode('hex') return self.session_id finally: self.lock.release() def checksession(self, sid): if sid != self.session_id: raise EOFError("broken session") def unlocked_read_next_screenshot(self): sshot = ScreenShot(self.next_snum, self.io_readline) self.next_snum += 1 if not sshot.valid(): self.reset() raise EOFError("subprocess finished") self.last_sshot = sshot return sshot def get_update(self, timeout): sshot = None endtime = time.time() + timeout while self.io_wait_for_data(timeout): self.lock.acquire() try: if self.io_wait_for_data(0.0): sshot = self.unlocked_read_next_screenshot() finally: self.lock.release() if sshot is not None: timeout = 0.0 else: timeout = endtime - time.time() if timeout < 0.0: timeout = 0.0 if sshot is None: return {} else: return sshot.render_answer() def unlocked_sendkey(self, char, keysym, modifiers): #print '========= %02x === %02x === %02x ==========' % (ord(char), # keysym, # modifiers) a = array.array('i', [1, ord(char or '\x00'), keysym, modifiers]) self.io_write(a.tostring()) class ScreenShot: def __init__(self, snum, readline): self.snum = snum data = [readline() for i in range(24)] last = data[-1].rstrip() last += ' ' * (80 - len(last)) data[-1] = last self.data = data self.attrs = readline() def valid(self): return bool(self.attrs) def render_html(self): last_attributes = [] assert self.attrs[-1] == '\n' for attr in self.attrs[:-1].split('/'): i = attr.rfind(' ') last_attributes.append((int(attr[i+1:]), attr[:i+1][:-1])) lst = [text_to_html.get(c, c) for line in self.data for c in line] for i in range(len(last_attributes)-1): startpos, cls = last_attributes[i] endpos, _ = last_attributes[i+1] lst[startpos] = '%s' % (cls, lst[startpos]) lst[endpos-1] += '' #print lst return ''.join(lst) def render_answer(self): return {'scr': self.render_html(), 'num': str(self.snum)} # HTML quoting text_to_html = {'\n': '