Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion bin/wssh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python

if __name__ == '__main__':
from wssh import client
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions bin/wsshd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python

from gevent import monkey
monkey.patch_all()
Expand Down Expand Up @@ -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,
Expand Down
175 changes: 120 additions & 55 deletions wssh/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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) """
Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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')
18 changes: 18 additions & 0 deletions wssh/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from setuptools import setup


setup(
name='wssh',
version='0.1.0',
author='Andrea Luzzardi <[email protected]>',
packages=[
'wssh'
],
scripts=[
'bin/wssh',
'bin/wsshd'
],
package_data={'': ['static/*', 'templates/*']},
include_package_data=True,
zip_safe=False
)
5 changes: 4 additions & 1 deletion wssh/static/wssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -62,6 +62,9 @@ WSSHClient.prototype._generateEndpoint = function(options) {
endpoint += '&run=' + encodeURIComponent(
options.command);
}
if (options.widths != "") {
endpoint += '&widths=' + encodeURIComponent(options.widths);
}
return endpoint;
};

Expand Down
20 changes: 19 additions & 1 deletion wssh/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
id="hostname"
class="input-large"
placeholder="localhost" />
<select id="logintype" class="input-small">
<option value = "ssh">ssh</option>
<option value = "telnet">telnet</option>
</select>
<span class="add-on">port</span><input
type="text"
id="portnumber"
Expand Down Expand Up @@ -142,14 +146,16 @@
<script type="application/javascript" src="{{url_for('static', filename='wssh.js')}}">
</script>
<script type="application/javascript">
var Width = Math.ceil(($(window).width()/2)/488*80) ;

function openTerminal(options) {
var client = new WSSHClient();
var term = new Terminal(80, 24, function(key) {
client.send(key);
});
term.open();
$('.terminal').detach().appendTo('#term');
term.resize(80, 24);
term.resize(Width, Math.floor($('.terminal').height()/18));
term.write('Connecting...');
client.connect($.extend(options, {
onError: function(error) {
Expand Down Expand Up @@ -188,6 +194,15 @@
}
);

$("#logintype").change(function(){
var logintype = $('#logintype').val();
if (logintype == 'ssh') {
$("#portnumber").attr("value",'22');
} else {
$("#portnumber").attr("value",'23');
}
});

$('#connect').submit(function(ev) {
ev.preventDefault();

Expand All @@ -210,6 +225,7 @@
var hostname = $('input:text#hostname');
var portnumber = $('input:text#portnumber');
var command = $('input:text#command');
var logintype = $('#logintype');

var authentication = $(
'input[name=authentication_method]:checked',
Expand All @@ -218,6 +234,8 @@
username: username.val(),
hostname: hostname.val(),
command: command.val(),
logintype: logintype.val(),
widths: Width,
authentication_method: authentication
};

Expand Down