diff --git a/bin/wssh b/bin/wssh index 55a7f61..fadddd5 100755 --- a/bin/wssh +++ b/bin/wssh @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python if __name__ == '__main__': from wssh import client @@ -51,6 +51,11 @@ if __name__ == '__main__': parser.add_argument('command', nargs='*', help='optional command to be executed') + + parser.add_argument('logintype', + nargs='*', + default='ssh', + help='logintype, support ssh/telnet') args = parser.parse_args() @@ -91,6 +96,7 @@ if __name__ == '__main__': params = { 'password': password, 'port': str(args.ssh_port), + 'logintype': logintype, 'private_key': key, 'key_passphrase': key_passphrase, 'run': ' '.join(args.command) if args.command else None, diff --git a/bin/wsshd b/bin/wsshd index 97fbd09..e7b7ae2 100755 --- a/bin/wsshd +++ b/bin/wsshd @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python from gevent import monkey monkey.patch_all() @@ -32,7 +32,7 @@ def connect(hostname, username): app.logger.error('Abort: Request is not WebSocket upgradable') raise BadRequest() - bridge = wssh.WSSHBridge(request.environ['wsgi.websocket']) + bridge = wssh.WSSHBridge(request.environ['wsgi.websocket'], logintype=request.args.get('logintype'),width=request.args.get('widths')) try: bridge.open( hostname=hostname, diff --git a/wssh/server.py b/wssh/server.py index 17a4965..23427f6 100644 --- a/wssh/server.py +++ b/wssh/server.py @@ -17,29 +17,42 @@ from paramiko.rsakey import RSAKey from paramiko.ssh_exception import SSHException +import struct +import telnetlib import socket - -try: - import simplejson as json -except ImportError: - import json +import json +import os +import re +import time from StringIO import StringIO - class WSSHBridge(object): - """ WebSocket to SSH Bridge Server """ + """ WebSocket to SSH Bridge Server - def __init__(self, websocket): + support telnet protocol + """ + + def __init__(self, websocket, logintype='ssh', width=80): """ Initialize a WSSH Bridge The websocket must be the one created by gevent-websocket """ self._websocket = websocket - self._ssh = paramiko.SSHClient() - self._ssh.set_missing_host_key_policy( - paramiko.AutoAddPolicy()) + self._logintype = logintype + self._buffsize = 10240 + self._width = width + if self._width and self._width.isdigit(): + self._width = int(self._width) + else: + self._width = 80 self._tasks = [] + if self._logintype == 'telnet': + self._cli = telnetlib + else: + self._cli = paramiko.SSHClient() + self._cli.set_missing_host_key_policy( + paramiko.AutoAddPolicy()) def _load_private_key(self, private_key, passphrase=None): """ Load a SSH private key (DSA or RSA) from a string @@ -70,6 +83,8 @@ def open(self, hostname, port=22, username=None, password=None, private_key=None, key_passphrase=None, allow_agent=False, timeout=None): """ Open a connection to a remote SSH server + or + Open a connection to a remote telnet server In order to connect, either one of these credentials must be supplied: @@ -83,27 +98,43 @@ def open(self, hostname, port=22, username=None, password=None, Authenticate using the *local* SSH agent. This is the one running alongside wsshd on the server side. """ - try: - pkey = None - if private_key: - pkey = self._load_private_key(private_key, key_passphrase) - self._ssh.connect( - hostname=hostname, - port=port, - username=username, - password=password, - pkey=pkey, - timeout=timeout, - allow_agent=allow_agent, - look_for_keys=False) - except socket.gaierror as e: - self._websocket.send(json.dumps({'error': - 'Could not resolve hostname {0}: {1}'.format( - hostname, e.args[1])})) - raise - except Exception as e: - self._websocket.send(json.dumps({'error': e.message or str(e)})) - raise + if self._logintype == 'telnet': + try: + self._cli = telnetlib.Telnet(hostname,port) + self._cli.sock.sendall(telnetlib.IAC + telnetlib.WILL + telnetlib.NAWS) + self._cli.sock.sendall(telnetlib.IAC + telnetlib.SB + telnetlib.NAWS + struct.pack(">H", self._width) + struct.pack(">H", 24) + telnetlib.IAC + telnetlib.SE) + self.invoke_telnetlogin(self._cli, username, password) + ### telnetlib, process_rawq, line-478,mod,self.sock.sendall(IAC + DONT + opt)->self.sock.sendall(IAC + DO + opt) + except socket.gaierror as e: + self._websocket.send(json.dumps({'error': + 'Could not resolve hostname {0}: {1}'.format( + hostname, e.args[1])})) + raise + except Exception as e: + self._websocket.send(json.dumps({'error': e.message or str(e)})) + raise + else: + try: + pkey = None + if private_key: + pkey = self._load_private_key(private_key, key_passphrase) + self._cli.connect( + hostname=hostname, + port=port, + username=username, + password=password, + pkey=pkey, + timeout=timeout, + allow_agent=allow_agent, + look_for_keys=False) + except socket.gaierror as e: + self._websocket.send(json.dumps({'error': + 'Could not resolve hostname {0}: {1}'.format( + hostname, e.args[1])})) + raise + except Exception as e: + self._websocket.send(json.dumps({'error': e.message or str(e)})) + raise def _forward_inbound(self, channel): """ Forward inbound traffic (websockets -> ssh) """ @@ -113,12 +144,17 @@ def _forward_inbound(self, channel): if not data: return data = json.loads(str(data)) - if 'resize' in data: - channel.resize_pty( - data['resize'].get('width', 80), - data['resize'].get('height', 24)) - if 'data' in data: - channel.send(data['data']) + + if self._logintype == 'telnet': + if 'data' in data: + channel.write(data['data'].encode('ascii')) + else: + if 'resize' in data: + channel.resize_pty( + data['resize'].get('width', self._width), + data['resize'].get('height', 24)) + if 'data' in data: + channel.send(data['data']) finally: self.close() @@ -127,17 +163,22 @@ def _forward_outbound(self, channel): try: while True: wait_read(channel.fileno()) - data = channel.recv(1024) + if self._logintype == 'telnet': + data = channel.read_very_eager() + else: + data = channel.recv(self._buffsize) if not len(data): return + self._websocket.send(json.dumps({'data': data})) finally: self.close() def _bridge(self, channel): """ Full-duplex bridge between a websocket and a SSH channel """ - channel.setblocking(False) - channel.settimeout(0.0) + if self._logintype != 'telnet': + channel.setblocking(False) + channel.settimeout(0.0) self._tasks = [ gevent.spawn(self._forward_inbound, channel), gevent.spawn(self._forward_outbound, channel) @@ -148,33 +189,57 @@ def close(self): """ Terminate a bridge session """ gevent.killall(self._tasks, block=True) self._tasks = [] - self._ssh.close() + self._cli.close() - def execute(self, command, term='xterm'): - """ Execute a command on the remote server + def shell(self, term='xterm'): + """ Start an interactive shell session - This method will forward traffic from the websocket to the SSH server - and the other way around. + This method invokes a shell on the remote SSH server and proxies + traffic to/from both peers. You must connect to a SSH server using ssh_connect() prior to starting the session. """ - transport = self._ssh.get_transport() - channel = transport.open_session() - channel.get_pty(term) - channel.exec_command(command) + if self._logintype == 'telnet': + channel = self._cli + else: + channel = self._cli.invoke_shell(term, width=self._width) self._bridge(channel) channel.close() - def shell(self, term='xterm'): - """ Start an interactive shell session + def execute(self, command, term='xterm'): + """ Execute a command on the remote server - This method invokes a shell on the remote SSH server and proxies - traffic to/from both peers. + This method will forward traffic from the websocket to the SSH server + and the other way around. You must connect to a SSH server using ssh_connect() prior to starting the session. """ - channel = self._ssh.invoke_shell(term) + if self._logintype == 'telnet': + channel = self._cli + else: + channel = self._cli.invoke_shell(term, width=self._width) + + if command: + self.executecmd(channel, command) + self._bridge(channel) channel.close() + + def invoke_telnetlogin(self, channel, username=None, password=None, buff=''): + if username: + while not re.search("(name|ogin):\s*$", buff, re.M|re.I): + buff += channel.read_eager() + channel.write(username.encode('ascii') + b'\n') + buff = '' + if password: + while not re.search("assword:\s*$", buff, re.M|re.I): + buff += channel.read_eager() + channel.write(password.encode('ascii') + b'\n') + + def executecmd(self, channel, command): + if self._logintype == 'telnet': + channel.write(command.encode('ascii') + b'\n') + else: + channel.send(command.encode('ascii') + b'\n') diff --git a/wssh/setup.py b/wssh/setup.py new file mode 100644 index 0000000..148557d --- /dev/null +++ b/wssh/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup + + +setup( + name='wssh', + version='0.1.0', + author='Andrea Luzzardi ', + packages=[ + 'wssh' + ], + scripts=[ + 'bin/wssh', + 'bin/wsshd' + ], + package_data={'': ['static/*', 'templates/*']}, + include_package_data=True, + zip_safe=False +) diff --git a/wssh/static/wssh.js b/wssh/static/wssh.js index 076557d..56c7958 100644 --- a/wssh/static/wssh.js +++ b/wssh/static/wssh.js @@ -50,7 +50,7 @@ WSSHClient.prototype._generateEndpoint = function(options) { encodeURIComponent(options.username); if (options.authentication_method == 'password') { endpoint += '?password=' + encodeURIComponent(options.password) + - '&port=' + encodeURIComponent(options.port); + '&port=' + encodeURIComponent(options.port) + '&logintype=' + encodeURIComponent(options.logintype); } else if (options.authentication_method == 'private_key') { endpoint += '?private_key=' + encodeURIComponent(options.private_key) + '&port=' + encodeURIComponent(options.port); @@ -62,6 +62,9 @@ WSSHClient.prototype._generateEndpoint = function(options) { endpoint += '&run=' + encodeURIComponent( options.command); } + if (options.widths != "") { + endpoint += '&widths=' + encodeURIComponent(options.widths); + } return endpoint; }; diff --git a/wssh/templates/index.html b/wssh/templates/index.html index f585e6e..7a3c5d4 100644 --- a/wssh/templates/index.html +++ b/wssh/templates/index.html @@ -38,6 +38,10 @@ id="hostname" class="input-large" placeholder="localhost" /> + port