Source code for opennode.oms.endpoint.webterm.root

import json
import time
import uuid

from grokcore.component import baseclass, context, name
from twisted.conch.insults.insults import ServerProtocol
from twisted.internet import reactor
from twisted.web.server import NOT_DONE_YET

from opennode.oms.endpoint.httprest.base import HttpRestView
from opennode.oms.endpoint.ssh.protocol import OmsShellProtocol
from opennode.oms.endpoint.webterm.ssh import ssh_connect_interactive_shell
from opennode.oms.model.model.bin import Command


[docs]class OmsShellTerminalProtocol(object): """Connect a OmsShellProtocol to a web terminal session."""
[docs] def logged_in(self, principal): self.principal = principal
[docs] def connection_made(self, terminal, size): self.shell = OmsShellProtocol() self.shell.set_terminal(terminal) self.shell.connectionMade() self.shell.terminalSize(size[0], size[1]) self.shell.logged_in(self.principal)
[docs] def handle_key(self, key): self.shell.terminal.dataReceived(key)
[docs] def terminalSize(self, width, height): # Insults terminal doesn't work well after resizes # also disabled in the oms shell over ssh. # # self.shell.terminalSize(width, height) pass
[docs]class SSHClientTerminalProtocol(object): """Connect a ssh client session to a web terminal session. Can be used to connect to hosts or to services and guis exposed via ssh interfaces, tunnels etc"""
[docs] def logged_in(self, principal): self.principal = principal
def __init__(self, user, host, port=22): self.user = user self.host = host self.port = port
[docs] def connection_made(self, terminal, size): self.transport = terminal.transport ssh_connect_interactive_shell(self.user, self.host, self.port, self.transport, self.set_channel, size)
[docs] def set_channel(self, channel): self.channel = channel
[docs] def handle_key(self, key): self.channel.write(key)
[docs] def terminalSize(self, width, height): self.channel.terminalSize(width, height)
[docs]class WebTransport(object): """Used by WebTerminal to actually send the data through the http transport.""" def __init__(self, session): self.session = session
[docs] def write(self, text): # Group together writes so that we reduce the number of http roundtrips. # Kind of Nagle's algorithm. self.session.buffer += text reactor.callLater(0.05, self.session.process_queue)
[docs] def loseConnection(self): """Close the connection ensuring the the web client will properly detect this close. The name of the method was chosen to implement the twisted convention.""" del TerminalServerMixin.sessions[self.session.id] self.write('\r\n')
[docs]class WebTerminal(ServerProtocol): """Used by TerminalProtocols (like OmsShellProtocol) to actually manipulate the terminal.""" def __init__(self, session): ServerProtocol.__init__(self) self.session = session self.transport = WebTransport(session)
[docs]class TerminalSession(object): """A session for our ajax terminal emulator.""" def __init__(self, terminal_protocol, terminal_size): self.id = str(uuid.uuid4()) self.queue = [] self.buffer = "" # TODO: handle session timeouts self.timestamp = time.time() self.terminal_size = terminal_size self.terminal_protocol = terminal_protocol self.terminal_protocol.connection_made(WebTerminal(self), terminal_size)
[docs] def parse_keys(self, key_stream): """The ajax protocol encodes keystrokes as a string of hex bytes, so each char code occupies to characters in the encoded form.""" while key_stream: yield chr(int(key_stream[0:2], 16)) key_stream = key_stream[2:]
[docs] def handle_keys(self, key_stream): """Send each input key the terminal.""" for key in self.parse_keys(key_stream): self.terminal_protocol.handle_key(key)
[docs] def handle_resize(self, size): if self.terminal_size != size: self.terminal_protocol.terminalSize(size[0], size[1])
[docs] def windowChanged(self, *args): """Called back by insults on terminalSize.""" pass
[docs] def enqueue(self, request): self.queue.append(request) if self.buffer: self.process_queue()
[docs] def process_queue(self): # Only one ongoing polling request should be live. # But I'm not sure if this can be guaranteed so let's keep temporarily keep them all. if self.queue: for r in self.queue: self.write(r) self.queue = []
[docs] def write(self, request): # chunk writes because the javascript renderer is very slow # this avoids long pauses to the user. chunk_size = 4000 unicode_buffer = self.buffer.decode('utf-8') chunk = unicode_buffer[0:chunk_size] request.write(json.dumps(dict(session=self.id, data=chunk))) request.finish() self.buffer = unicode_buffer[chunk_size:].encode('utf-8')
def __repr__(self): return 'TerminalSession(%s, %s, %s, %s)' % (self.id, self.queue, self.buffer, self.timestamp)
[docs]class TerminalServerMixin(object): """Common code for view-based and twisted-resource based rendering of ShellInABox protocol.""" sessions = {}
[docs] def render_POST(self, request): session_id = request.args.get('session', [None])[0] size = (int(request.args['width'][0]), int(request.args['height'][0])) # The handshake consists of the session id and initial data to be rendered. if not session_id: session = TerminalSession(self.get_terminal_protocol(request), size) session_id = session.id self.sessions[session.id] = session if session_id not in self.sessions: # Session interruption is defined using a success status # but with empty session (that's the protocol, I didn't design it). request.setResponseCode(200) return json.dumps(dict(session='', data='')) session = self.sessions[session_id] session.handle_resize(size) # There are two types of requests: # 1) user type keystrokes, return synchronously # 2) long polling requests are suspended until there is activity from the terminal keys = request.args.get('keys', None) if keys: session.handle_keys(keys[0]) return "" # responsed to this kind of requests are ignored else: session.enqueue(request) return NOT_DONE_YET
[docs] def get_terminal_protocol(self, request): protocol = self.terminal_protocol protocol.logged_in(request.interaction.participations[0].principal) return protocol
[docs]class ConsoleView(HttpRestView, TerminalServerMixin): baseclass()
[docs]class OmsShellConsoleView(ConsoleView): context(Command) name('webterm') @property
[docs] def terminal_protocol(self): # TODO: pass the self.context.cmd so that we can execute this particular command # instead of hardcoding the oms shell. return OmsShellTerminalProtocol()

This Page