Browse Source

Initial commit

Andrea Luzzardi 7 years ago
commit
b39e9d6c53
14 changed files with 1541 additions and 0 deletions
  1. 6 0
      .gitignore
  2. 2 0
      MANIFEST.in
  3. 106 0
      bin/wssh
  4. 99 0
      bin/wsshd
  5. 1 0
      requirements.txt
  6. 4 0
      requirements_server.txt
  7. 18 0
      setup.py
  8. 3 0
      wssh/__init__.py
  9. 87 0
      wssh/client.py
  10. 181 0
      wssh/server.py
  11. 689 0
      wssh/static/bootstrap.min.css
  12. 20 0
      wssh/static/style.css
  13. 97 0
      wssh/static/wssh.js
  14. 228 0
      wssh/templates/index.html

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+*.py[co]
+*.egg
+*.egg-info
+dist
+build
+eggs

+ 2 - 0
MANIFEST.in

@@ -0,0 +1,2 @@
+recursive-include wssh/static/*
+recursive-include wssh/templates/*

+ 106 - 0
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 - 0
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 - 0
requirements.txt

@@ -0,0 +1 @@
+https://github.com/liris/websocket-client/tarball/master#egg=websocket-client

+ 4 - 0
requirements_server.txt

@@ -0,0 +1,4 @@
+gevent
+gevent-websocket
+paramiko
+flask

+ 18 - 0
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 - 0
wssh/__init__.py

@@ -0,0 +1,3 @@
+from server import WSSHProxy
+
+__version__ = '0.1.0'

+ 87 - 0
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)

+ 181 - 0
wssh/server.py

@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+
+"""
+wssh.server
+
+This module provides server capabilities of wssh
+"""
+
+import gevent
+from gevent.socket import wait_read, wait_write
+from gevent.select import select
+from gevent.event import Event
+
+import paramiko
+from paramiko import PasswordRequiredException
+from paramiko.dsskey import DSSKey
+from paramiko.rsakey import RSAKey
+from paramiko.ssh_exception import SSHException
+
+import socket
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from StringIO import StringIO
+
+
+
+class WSSHProxy(object):
+    """ WebSocket to SSH Proxy Server """
+
+    def __init__(self, websocket):
+        """ Initialize a WSSH Proxy
+
+        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._tasks = []
+
+    def _load_private_key(self, private_key, passphrase=None):
+        """ Load a SSH private key (DSA or RSA) from a string
+
+        The private key may be encrypted. In that case, a passphrase
+        must be supplied.
+        """
+        key = None
+        last_exception = None
+        for pkey_class in (RSAKey, DSSKey):
+            try:
+                key = pkey_class.from_private_key(StringIO(private_key),
+                    passphrase)
+            except PasswordRequiredException as e:
+                # The key file is encrypted and no passphrase was provided.
+                # There's no point to continue trying
+                raise
+            except SSHException as e:
+                last_exception = e
+                continue
+            else:
+                break
+        if key is None and last_exception:
+            raise last_exception
+        return key
+
+    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
+
+        In order to connect, either one of these credentials must be
+        supplied:
+            * Password
+                Password-based authentication
+            * Private Key
+                Authenticate using SSH Keys.
+                If the private key is encrypted, it will attempt to
+                load it using the passphrase
+            * Agent
+                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
+
+    def _forward_inbound(self, channel):
+        """ Forward inbound traffic (websockets -> ssh) """
+        try:
+            while True:
+                data = self._websocket.receive()
+                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'])
+        finally:
+            self.close()
+
+    def _forward_outbound(self, channel):
+        """ Forward outbound traffic (ssh -> websockets) """
+        try:
+            while True:
+                wait_read(channel.fileno())
+                data = channel.recv(1024)
+                if not len(data):
+                    return
+                self._websocket.send(json.dumps({'data': data}))
+        finally:
+            self.close()
+
+    def _proxy(self, channel):
+        """ Full-duplex proxy between a websocket and a SSH channel """
+        channel.setblocking(False)
+        channel.settimeout(0.0)
+        self._tasks = [
+            gevent.spawn(self._forward_inbound, channel),
+            gevent.spawn(self._forward_outbound, channel)
+        ]
+        gevent.joinall(self._tasks)
+
+    def close(self):
+        """ Terminate a proxy session """
+        gevent.killall(self._tasks, block=True)
+        self._tasks = []
+        self._ssh.close()
+
+    def execute(self, command, term='xterm'):
+        """ Execute a command on the remote server
+
+        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.
+        """
+        transport = self._ssh.get_transport()
+        channel = transport.open_session()
+        channel.get_pty(term)
+        channel.exec_command(command)
+        self._proxy(channel)
+        channel.close()
+
+    def shell(self, term='xterm'):
+        """ Start an interactive shell session
+
+        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.
+        """
+        channel = self._ssh.invoke_shell(term)
+        self._proxy(channel)
+        channel.close()

File diff suppressed because it is too large
+ 689 - 0
wssh/static/bootstrap.min.css


+ 20 - 0
wssh/static/style.css

@@ -0,0 +1,20 @@
+body {
+    padding-top: 60px;
+    padding-bottom: 40px;
+}
+
+.terminal {
+    border: #000 solid 5px;
+    font-family: "Monaco", "DejaVu Sans Mono", "Liberation Mono", monospace;
+    font-size: 11px;
+    color: #f0f0f0;
+    background: #000;
+    width: 600px;
+    box-shadow: rgba(0, 0, 0, 0.8) 2px 2px 20px;
+}
+
+.reverse-video {
+    color: #000;
+    background: #f0f0f0;
+}
+

+ 97 - 0
wssh/static/wssh.js

@@ -0,0 +1,97 @@
+/*
+WSSH Javascript Client
+
+Usage:
+
+var client = new WSSHClient();
+
+client.connect({
+    // Connection and authentication parameters
+    username: 'root',
+    hostname: 'localhost',
+    authentication_method: 'password', // can either be password or private_key
+    password: 'secretpassword', // do not provide when using private_key
+    key_passphrase: 'secretpassphrase', // *may* be provided if the private_key is encrypted
+
+    // Callbacks
+    onError: function(error) {
+        // Called upon an error
+        console.error(error);
+    },
+    onConnect: function() {
+        // Called after a successful connection to the server
+        console.debug('Connected!');
+
+        client.send('ls\n'); // You can send data back to the server by using WSSHClient.send()
+    },
+    onClose: function() {
+        // Called when the remote closes the connection
+        console.debug('Connection Reset By Peer');
+    },
+    onData: function(data) {
+        // Called when data is received from the server
+        console.debug('Received: ' + data);
+    }
+});
+
+*/
+
+function WSSHClient() {
+};
+
+WSSHClient.prototype._generateEndpoint = function(options) {
+    if (window.location.protocol == 'https:') {
+        var protocol = 'wss://';
+    } else {
+        var protocol = 'ws://';
+    }
+    var endpoint = protocol + window.location.host +
+        '/wssh/' + encodeURIComponent(options.hostname) + '/' +
+        encodeURIComponent(options.username);
+    if (options.authentication_method == 'password') {
+        endpoint += '?password=' + encodeURIComponent(options.password);
+    } else if (options.authentication_method == 'private_key') {
+        endpoint += '?private_key=' + encodeURIComponent(options.private_key);
+        if (options.key_passphrase !== undefined)
+            endpoint += '&key_passphrase=' + encodeURIComponent(
+                options.key_passphrase);
+    }
+    return endpoint;
+};
+
+WSSHClient.prototype.connect = function(options) {
+    var endpoint = this._generateEndpoint(options);
+
+    if (window.WebSocket) {
+        this._connection = new WebSocket(endpoint);
+    }
+    else if (window.MozWebSocket) {
+        this._connection = MozWebSocket(endpoint);
+    }
+    else {
+        options.onError('WebSocket Not Supported');
+        return ;
+    }
+
+    this._connection.onopen = function() {
+        options.onConnect();
+    };
+
+    this._connection.onmessage = function (evt) {
+        var data = JSON.parse(evt.data.toString());
+        if (data.error !== undefined) {
+            options.onError(data.error);
+        }
+        else {
+            options.onData(data.data);
+        }
+    };
+
+    this._connection.onclose = function(evt) {
+        options.onClose();
+    };
+};
+
+WSSHClient.prototype.send = function(data) {
+    this._connection.send(JSON.stringify({'data': data}));
+};

+ 228 - 0
wssh/templates/index.html

@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>wssh</title>
+
+        <link href="{{url_for('static', filename='bootstrap.min.css')}}" rel="stylesheet" />
+        <link href="{{url_for('static', filename='style.css')}}" rel="stylesheet" />
+    </head>
+
+    <body>
+        <div class="navbar navbar-fixed-top">
+            <div class="navbar-inner">
+                <div class="container">
+                    <a class="brand" href="#">wssh</a>
+                </div>
+            </div>
+        </div>
+
+        <div class="container">
+            <!-- Connection form -->
+
+            <form id="connect" class="form-horizontal">
+                <fieldset>
+                    <legend>Connect to a remote SSH server</legend>
+
+                    <div class="control-group">
+                        <label class="control-label">
+                            Destination
+                        </label>
+                        <div class="controls">
+                            <input type="text" id="username"
+                                class="input-small"
+                                placeholder="root" />
+                            <div class="input-prepend">
+                                <span class="add-on">@</span><input
+                                    type="text"
+                                    id="hostname"
+                                    class="input-large"
+                                    placeholder="localhost" />
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="control-group">
+                        <label class="control-label">
+                            Authentication method
+                        </label>
+
+                        <div class="controls">
+                            <label class="radio">
+                                <input type="radio" name="authentication_method"
+                                    value="password" checked />
+                                    Password
+                            </label>
+
+                            <label class="radio">
+                                <input type="radio" name="authentication_method"
+                                    value="private_key" />
+                                    Private Key
+                            </label>
+                        </div>
+                    </div>
+
+                    <div class="control-group" id="password_authentication">
+                        <label class="control-label">
+                            Password
+                        </label>
+                        <div class="controls">
+                            <input type="password" id="password"
+                                class="input-large" />
+                        </div>
+                    </div>
+
+                    <div id="private_key_authentication">
+                        <div class="control-group">
+                            <label class="control-label">
+                                Private Key
+                            </label>
+                            <div class="controls">
+                                <textarea id="private_key" rows="6"
+                                    class="input-xxlarge"></textarea>
+                                <p class="help-block">
+                                    Copy &amp; Paste your SSH private from
+                                    <code>~/.ssh/id_rsa</code> or
+                                    <code>~/.ssh/id_dsa</code>
+                                </p>
+                            </div>
+                        </div>
+
+                        <div class="control-group">
+                            <label class="control-label">
+                                Key Passphrase
+                            </label>
+                            <div class="controls">
+                                <input type="password" id="key_passphrase"
+                                    class="input-large" />
+                                <p class="help-block">
+                                    Enter your private key passphrase if it
+                                    is encrypted. Leave empty otherwise.
+                                </p>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="form-actions">
+                        <button type="submit" class="btn btn-primary">
+                            Connect
+                        </button>
+                    </div>
+
+                </fieldset>
+            </form>
+
+            <div id="term">
+            </div>
+
+        </div>
+
+        <script type="application/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js">
+        </script>
+        <script type="application/javascript" src="https://raw.github.com/chjj/tty.js/36717df8e96f35f4e2bd3fd585e9361f1439fc7e/static/term.js">
+        </script>
+        <script type="application/javascript" src="{{url_for('static', filename='wssh.js')}}">
+        </script>
+        <script type="application/javascript">
+            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.write('Connecting...');
+                client.connect($.extend(options, {
+                    onError: function(error) {
+                        term.write('Error: ' + error + '\r\n');
+                    },
+                    onConnect: function() {
+                        // Erase our connecting message
+                        term.write('\r');
+                    },
+                    onClose: function() {
+                        term.write('Connection Reset By Peer');
+                    },
+                    onData: function(data) {
+                        term.write(data);
+                    }
+                }));
+            }
+        </script>
+
+        <script type='application/javascript'>
+            $(document).ready(function() {
+                $('#ssh').hide();
+                $('#private_key_authentication', '#connect').hide();
+
+                $('input:radio[value=private_key]', '#connect').click(
+                    function() {
+                        $('#password_authentication').hide();
+                        $('#private_key_authentication').show();
+                    }
+                );
+
+                $('input:radio[value=password]', '#connect').click(
+                    function() {
+                        $('#password_authentication').show();
+                        $('#private_key_authentication').hide();
+                    }
+                );
+
+                $('#connect').submit(function(ev) {
+                    ev.preventDefault();
+
+                    function validate(fields) {
+                        var success = true;
+                        fields.forEach(function(field) {
+                            if (!field.val()) {
+                                field.closest('.control-group')
+                                    .addClass('error');
+                                success = false;
+                            }
+                        });
+                        return success;
+                    }
+
+                    // Clear errors
+                    $('.error').removeClass('error');
+
+                    var username = $('input:text#username');
+                    var hostname = $('input:text#hostname');
+
+                    var authentication = $(
+                        'input[name=authentication_method]:checked',
+                        '#connect').val();
+
+                    var options = {
+                        username: username.val(),
+                        hostname: hostname.val(),
+                        authentication_method: authentication
+                    };
+
+                    if (authentication == 'password') {
+                        var password = $('input:password#password');
+                        if (!validate([username, hostname, password]))
+                            return false;
+                        $.extend(options, {password: password.val()});
+                    } else if (authentication == 'private_key') {
+                        var private_key = $('textarea#private_key');
+                        if (!validate([username, hostname, private_key]))
+                            return false;
+                        $.extend(options, {private_key: private_key.val()});
+                        var key_passphrase = $('input:password#key_passphrase');
+                        if (key_passphrase.val()) {
+                            $.extend(options,
+                                {key_passphrase: key_passphrase.val()});
+                        }
+                    }
+
+                    $('#connect').hide();
+                    $('#ssh').show();
+                    openTerminal(options);
+                });
+            });
+        </script>
+    </body>
+</html>