Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
aluzzardi committed Jul 24, 2012
0 parents commit b39e9d6
Show file tree
Hide file tree
Showing 14 changed files with 1,541 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
@@ -0,0 +1,6 @@
*.py[co]
*.egg
*.egg-info
dist
build
eggs
2 changes: 2 additions & 0 deletions MANIFEST.in
@@ -0,0 +1,2 @@
recursive-include wssh/static/*
recursive-include wssh/templates/*
106 changes: 106 additions & 0 deletions bin/wssh
@@ -0,0 +1,106 @@
#!/usr/bin/env python

if __name__ == '__main__':
from wssh import client

import os
import sys
import argparse
import getpass
import urllib2


parser = argparse.ArgumentParser(
description='wssh - SSH Over WebSockets Client')

parser.add_argument('--host', '-H',
help='WSSH server host (default: 127.0.0.1)',
default='127.0.0.1')

parser.add_argument('--port', '-P',
help='WSSH server port (default: 5000)',
type=int,
default=5000)

parser.add_argument('--password', '-p',
nargs='?',
const='',
help='Password-based authentication. ' \
'If no password is provided you will be prompted for one')

parser.add_argument('--key', '-k',
nargs='?',
const='',
help='Private key authentication. ' \
'Selects a file from which the private key ' \
'for RSA or DSA authentication is read. ' \
'The default is ~/.ssh/id_rsa and ~/.ssh/id_dsa.')

parser.add_argument('--key-passphrase', '-K',
nargs='?',
const='',
help='Provide a passphrase for encrypted private key files.')

parser.add_argument('destination',
help='[user@]hostname')

args = parser.parse_args()

if '@' in args.destination:
(username, hostname) = args.destination.split('@', 1)
else:
(username, hostname) = (getpass.getuser(), args.destination)

if args.password == '':
password = getpass.getpass('Password: ')
else:
password = args.password

if args.key_passphrase == '':
key_passphrase = getpass.getpass('Enter passphrase for private key: ')
else:
key_passphrase = args.key_passphrase


key = None
if args.key == '':
key_files = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
for path in key_files:
path = os.path.expanduser(path)
if os.path.exists(path):
key = file(path).read()
break
if key is None:
print >> sys.stderr, 'Error: Unable to locate identity file {0}' \
.format(' or '.join(key_files))
sys.exit(1)
elif args.key is not None:
if not os.path.exists(args.key):
print >> sys.stderr, 'Error: Identity file "{0}" does not exist' \
.format(args.key)
sys.exit(1)
key = file(args.key).read()

params = {
'password': password,
'private_key': key,
'key_passphrase': key_passphrase
}

# Filter empty parameters
params = dict(filter(lambda (k, v): v is not None, params.iteritems()))

endpoint = 'ws://{serv_host}:{serv_port}/wssh/{host}/{user}?{params}'.format(
serv_host=args.host,
serv_port=args.port,
host=urllib2.quote(hostname),
user=urllib2.quote(username),
params='&'.join(['{0}={1}'.format(k, urllib2.quote(v))
for (k, v) in params.iteritems()]))

try:
client.invoke_shell(endpoint)
except client.ConnectionError as e:
print >>sys.stderr, 'wssh: {0}'.format(e.message or 'Connection error')
else:
print >>sys.stderr, 'Connection to {0} closed.'.format(hostname)
99 changes: 99 additions & 0 deletions bin/wsshd
@@ -0,0 +1,99 @@
#!/usr/bin/env python

from gevent import monkey; monkey.patch_all()
from flask import Flask, request, abort, render_template
from werkzeug.exceptions import BadRequest
import gevent
import wssh


app = Flask(__name__)

@app.route('/')
def index():
return render_template('index.html')

@app.route('/wssh/<hostname>/<username>')
def connect(hostname, username):
app.logger.debug('{remote} -> {username}@{hostname}: {command}'.format(
remote=request.remote_addr,
username=username,
hostname=hostname,
command=request.args['run'] if 'run' in request.args else
'[interactive shell]'
))

# Abort if this is not a websocket request
if not request.environ.get('wsgi.websocket'):
app.logger.error('Abort: Request is not WebSocket upgradable')
raise BadRequest()

proxy = wssh.WSSHProxy(request.environ['wsgi.websocket'])
try:
proxy.open(
hostname=hostname,
username=username,
password=request.args.get('password'),
private_key=request.args.get('private_key'),
key_passphrase=request.args.get('key_passphrase'),
allow_agent=app.config.get('WSSH_ALLOW_SSH_AGENT', False))
except Exception as e:
app.logger.exception('Error while connecting to {0}: {1}'.format(
hostname, e.message))
request.environ['wsgi.websocket'].close()
return str()
if 'run' in request.args:
proxy.execute(request.args)
else:
proxy.shell()

# We have to manually close the websocket and return an empty response,
# otherwise flask will complain about not returning a response and will
# throw a 500 at our websocket client
request.environ['wsgi.websocket'].close()
return str()

if __name__ == '__main__':
import argparse
from gevent.pywsgi import WSGIServer
from geventwebsocket import WebSocketHandler
from jinja2 import FileSystemLoader
import os

root_path = os.path.dirname(wssh.__file__)
app.jinja_loader = FileSystemLoader(os.path.join(root_path, 'templates'))
app.static_folder = os.path.join(root_path, 'static')

parser = argparse.ArgumentParser(
description='wsshd - SSH Over WebSockets Daemon')

parser.add_argument('--port', '-p',
default=5000,
help='Port to bind (default: 5000)')

parser.add_argument('--host', '-H',
default='0.0.0.0',
help='Host to listen to (default: 0.0.0.0)')

parser.add_argument('--allow-agent', '-A',
action='store_true',
default=False,
help='Allow the use of the local (where wsshd is running) ' \
'ssh-agent to authenticate. Dangerous.')

args = parser.parse_args()

app.config['WSSH_ALLOW_SSH_AGENT'] = args.allow_agent

agent = 'wsshd/{0}'.format('0.1.0')

print '{0} running on {1}:{2}'.format(agent, args.host, args.port)

app.debug = True
http_server = WSGIServer((args.host, args.port), app,
log=None,
handler_class=WebSocketHandler)
try:
http_server.serve_forever()
except KeyboardInterrupt:
pass
1 change: 1 addition & 0 deletions requirements.txt
@@ -0,0 +1 @@
https://github.com/liris/websocket-client/tarball/master#egg=websocket-client
4 changes: 4 additions & 0 deletions requirements_server.txt
@@ -0,0 +1,4 @@
gevent
gevent-websocket
paramiko
flask
18 changes: 18 additions & 0 deletions setup.py
@@ -0,0 +1,18 @@
from setuptools import setup


setup(
name='wssh',
version='0.0.1',
author='Andrea Luzzardi <aluzzardi@gmail.com>',
packages=[
'wssh'
],
scripts=[
'bin/wssh',
'bin/wsshd'
],
package_data={'': ['static/*', 'templates/*']},
include_package_data=True,
zip_safe=False
)
3 changes: 3 additions & 0 deletions wssh/__init__.py
@@ -0,0 +1,3 @@
from server import WSSHProxy

__version__ = '0.1.0'
87 changes: 87 additions & 0 deletions wssh/client.py
@@ -0,0 +1,87 @@
import os
import sys
import signal
import errno

import websocket
import select

import termios
import tty
import fcntl
import struct

import platform

try:
import simplejson as json
except ImportError:
import json


class ConnectionError(Exception):
pass


def _pty_size():
rows, cols = 24, 80
# Can't do much for Windows
if platform.system() == 'Windows':
return rows, cols
fmt = 'HH'
buffer = struct.pack(fmt, 0, 0)
result = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ,
buffer)
rows, cols = struct.unpack(fmt, result)
return rows, cols


def _resize(ws):
rows, cols = _pty_size()
ws.send(json.dumps({'resize': {'width': cols, 'height': rows}}))


def invoke_shell(endpoint):
ssh = websocket.create_connection(endpoint)
_resize(ssh)
oldtty = termios.tcgetattr(sys.stdin)
old_handler = signal.getsignal(signal.SIGWINCH)

def on_term_resize(signum, frame):
_resize(ssh)
signal.signal(signal.SIGWINCH, on_term_resize)

try:
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())

rows, cols = _pty_size()
ssh.send(json.dumps({'resize': {'width': cols, 'height': rows}}))

while True:
try:
r, w, e = select.select([ssh.sock, sys.stdin], [], [])
if ssh.sock in r:
data = ssh.recv()
if not data:
break
message = json.loads(data)
if 'error' in message:
raise ConnectionError(message['error'])
sys.stdout.write(message['data'])
sys.stdout.flush()
if sys.stdin in r:
x = sys.stdin.read(1)
if len(x) == 0:
break
ssh.send(json.dumps({'data': x}))
except (select.error, IOError) as e:
if e.args and e.args[0] == errno.EINTR:
pass
else:
raise
except websocket.WebSocketException:
raise
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty)
signal.signal(signal.SIGWINCH, old_handler)

0 comments on commit b39e9d6

Please sign in to comment.