Initialize
This commit is contained in:
+61
@@ -0,0 +1,61 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# database file
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# temporary file
|
||||||
|
*.swp
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Shengdun Hua
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
## WebSSH
|
||||||
|
A simple web application to be used as an ssh client to connect to your ssh servers. It is written in Python, base on Tornado and Paramiko.
|
||||||
|
|
||||||
|
### Preview
|
||||||
|

|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
```
|
||||||
|
$ pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```
|
||||||
|
$ Python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Help
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python main.py --help
|
||||||
|
```
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import socket
|
||||||
|
import weakref
|
||||||
|
import uuid
|
||||||
|
import paramiko
|
||||||
|
import tornado.web
|
||||||
|
import tornado.websocket
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
from tornado.options import define, options, parse_command_line
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
except ImportError:
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
define('address', default='127.0.0.1', help='listen address')
|
||||||
|
define('port', default=8888, help='listen port', type=int)
|
||||||
|
|
||||||
|
|
||||||
|
BUF_SIZE = 1024
|
||||||
|
DELAY = 3
|
||||||
|
base_dir = os.path.dirname(__file__)
|
||||||
|
workers = {}
|
||||||
|
|
||||||
|
|
||||||
|
def recycle(worker):
|
||||||
|
if worker.handler:
|
||||||
|
return
|
||||||
|
logging.debug('Recycling worker {}'.format(worker.id))
|
||||||
|
workers.pop(worker.id, None)
|
||||||
|
worker.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Worker(object):
|
||||||
|
def __init__(self, ssh, chan, dst_addr):
|
||||||
|
self.loop = IOLoop.current()
|
||||||
|
self.ssh = ssh
|
||||||
|
self.chan = chan
|
||||||
|
self.dst_addr = dst_addr
|
||||||
|
self.fd = chan.fileno()
|
||||||
|
self.id = str(id(self))
|
||||||
|
self.data_to_dst = []
|
||||||
|
self.handler = None
|
||||||
|
|
||||||
|
def __call__(self, fd, events):
|
||||||
|
if events & IOLoop.READ:
|
||||||
|
self.on_read()
|
||||||
|
if events & IOLoop.WRITE:
|
||||||
|
self.on_write()
|
||||||
|
if events & IOLoop.ERROR:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def set_handler(self, handler):
|
||||||
|
if self.handler:
|
||||||
|
return
|
||||||
|
self.handler = handler
|
||||||
|
|
||||||
|
def on_read(self):
|
||||||
|
logging.debug('worker {} on read'.format(self.id))
|
||||||
|
data = self.chan.recv(BUF_SIZE)
|
||||||
|
logging.debug('"{}" from {}'.format(data, self.dst_addr))
|
||||||
|
if not data:
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.debug('"{}" to {}'.format(data, self.handler.src_addr))
|
||||||
|
try:
|
||||||
|
self.handler.write_message(data)
|
||||||
|
except tornado.websocket.WebSocketClosedError:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_write(self):
|
||||||
|
logging.debug('worker {} on write'.format(self.id))
|
||||||
|
if not self.data_to_dst:
|
||||||
|
return
|
||||||
|
data = ''.join(self.data_to_dst)
|
||||||
|
self.data_to_dst = []
|
||||||
|
logging.debug('"{}" to {}'.format(data, self.dst_addr))
|
||||||
|
try:
|
||||||
|
sent = self.chan.send(data)
|
||||||
|
except socket.error as e:
|
||||||
|
logging.error(e)
|
||||||
|
self.close()
|
||||||
|
else:
|
||||||
|
data = data[sent:]
|
||||||
|
if data:
|
||||||
|
self.data_to_dst.append(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
logging.debug('Closing worker {}'.format(self.id))
|
||||||
|
if self.handler:
|
||||||
|
self.loop.remove_handler(self.fd)
|
||||||
|
self.handler.close()
|
||||||
|
self.chan.close()
|
||||||
|
self.ssh.close()
|
||||||
|
logging.info('Connection to {} lost'.format(self.dst_addr))
|
||||||
|
|
||||||
|
|
||||||
|
class IndexHandler(tornado.web.RequestHandler):
|
||||||
|
def get_privatekey(self):
|
||||||
|
try:
|
||||||
|
return self.request.files.get('privatekey')[0]['body']
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_pkey(self, privatekey, password):
|
||||||
|
if not password:
|
||||||
|
password = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
spkey = StringIO(privatekey)
|
||||||
|
except TypeError:
|
||||||
|
spkey = StringIO(privatekey.decode('utf-8'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
pkey = paramiko.RSAKey.from_private_key(spkey, password=password)
|
||||||
|
except paramiko.SSHException:
|
||||||
|
pkey = paramiko.DSSKey.from_private_key(spkey, password=password)
|
||||||
|
return pkey
|
||||||
|
|
||||||
|
def get_port(self):
|
||||||
|
value = self.get_value('port')
|
||||||
|
try:
|
||||||
|
port = int(value)
|
||||||
|
except ValueError:
|
||||||
|
port = 0
|
||||||
|
|
||||||
|
if 0 < port < 65536:
|
||||||
|
return port
|
||||||
|
|
||||||
|
raise ValueError("Invalid port {}".format(value))
|
||||||
|
|
||||||
|
def get_value(self, name):
|
||||||
|
value = self.get_argument(name)
|
||||||
|
if not value:
|
||||||
|
raise ValueError("Empty {}".format(name))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_args(self):
|
||||||
|
hostname = self.get_value('hostname')
|
||||||
|
port = self.get_port()
|
||||||
|
username = self.get_value('username')
|
||||||
|
password = self.get_argument('password')
|
||||||
|
privatekey = self.get_privatekey()
|
||||||
|
pkey = self.get_pkey(privatekey, password) if privatekey else None
|
||||||
|
args = (hostname, port, username, password, pkey)
|
||||||
|
logging.debug(args)
|
||||||
|
return args
|
||||||
|
|
||||||
|
def ssh_connect(self):
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.load_system_host_keys()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
args = self.get_args()
|
||||||
|
dst_addr = '{}:{}'.format(*args[:2])
|
||||||
|
logging.info('Connecting to {}'.format(dst_addr))
|
||||||
|
ssh.connect(*args)
|
||||||
|
chan = ssh.invoke_shell(term='xterm')
|
||||||
|
chan.setblocking(0)
|
||||||
|
worker = Worker(ssh, chan, dst_addr)
|
||||||
|
IOLoop.current().call_later(DELAY, recycle, worker)
|
||||||
|
return worker
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.render('index.html')
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
worker_id = None
|
||||||
|
status = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
worker = self.ssh_connect()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error((type(e), e))
|
||||||
|
status = str(e)
|
||||||
|
# raise
|
||||||
|
else:
|
||||||
|
worker_id = worker.id
|
||||||
|
workers[worker_id] = worker
|
||||||
|
|
||||||
|
self.write(dict(id=worker_id, status=status))
|
||||||
|
|
||||||
|
|
||||||
|
class WsockHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.loop = IOLoop.current()
|
||||||
|
self.worker_ref = None
|
||||||
|
super(self.__class__, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def check_origin(self, origin):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
self.src_addr = '{}:{}'.format(*self.stream.socket.getpeername())
|
||||||
|
logging.info('Connected from {}'.format(self.src_addr))
|
||||||
|
worker = workers.pop(self.get_argument('id'), None)
|
||||||
|
if not worker:
|
||||||
|
self.close(reason='Invalid worker id')
|
||||||
|
return
|
||||||
|
self.set_nodelay(True)
|
||||||
|
worker.set_handler(self)
|
||||||
|
self.worker_ref = weakref.ref(worker)
|
||||||
|
self.loop.add_handler(worker.fd, worker, IOLoop.READ | IOLoop.WRITE)
|
||||||
|
|
||||||
|
def on_message(self, message):
|
||||||
|
logging.debug('"{}" from {}'.format(message, self.src_addr))
|
||||||
|
worker = self.worker_ref()
|
||||||
|
worker.data_to_dst.append(message)
|
||||||
|
worker.on_write()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
logging.info('Disconnected from {}'.format(self.src_addr))
|
||||||
|
worker = self.worker_ref() if self.worker_ref else None
|
||||||
|
if worker:
|
||||||
|
worker.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
settings = {
|
||||||
|
'template_path': os.path.join(base_dir, 'templates'),
|
||||||
|
'static_path': os.path.join(base_dir, 'static'),
|
||||||
|
'cookie_secret': uuid.uuid1().hex,
|
||||||
|
'xsrf_cookies': True,
|
||||||
|
'debug': True
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = [
|
||||||
|
(r'/', IndexHandler),
|
||||||
|
(r'/ws', WsockHandler)
|
||||||
|
]
|
||||||
|
|
||||||
|
parse_command_line()
|
||||||
|
app = tornado.web.Application(handlers, **settings)
|
||||||
|
app.listen(options.port, options.address)
|
||||||
|
logging.info('Listening on {}:{}'.format(options.address, options.port))
|
||||||
|
IOLoop.current().start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
paramiko==2.3.1
|
||||||
|
tornado==4.5.2
|
||||||
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
.xterm.fullscreen{position:fixed;top:0;bottom:0;left:0;right:0;width:auto;height:auto;z-index:255}
|
||||||
|
/*# sourceMappingURL=fullscreen.min.css.map */
|
||||||
File diff suppressed because it is too large
Load Diff
Vendored
+2
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Vendored
+7
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
!function(e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("../../xterm")):"function"==typeof define?define(["../../xterm"],e):e(window.Terminal)}(function(e){var t={};return t.toggleFullScreen=function(e,t){var n;n=void 0===t?e.element.classList.contains("fullscreen")?"remove":"add":t?"add":"remove",e.element.classList[n]("fullscreen")},e.prototype.toggleFullscreen=function(e){t.toggleFullScreen(this,e)},t});
|
||||||
Vendored
+4
File diff suppressed because one or more lines are too long
@@ -0,0 +1,63 @@
|
|||||||
|
jQuery(function($){
|
||||||
|
$('form#connect').submit(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var form = $(this),
|
||||||
|
url = form.attr('action'),
|
||||||
|
type = form.attr('type'),
|
||||||
|
data = new FormData(this);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: type,
|
||||||
|
data: data,
|
||||||
|
success: callback,
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function callback(msg) {
|
||||||
|
// console.log(msg);
|
||||||
|
if (msg.status) {
|
||||||
|
$('#status').text(msg.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ws_url = window.location.href.replace('http', 'ws'),
|
||||||
|
join = (ws_url[ws_url.length-1] == '/' ? '' : '/'),
|
||||||
|
url = ws_url + join + 'ws?id=' + msg.id;
|
||||||
|
socket = new WebSocket(url),
|
||||||
|
terminal = document.getElementById('#terminal'),
|
||||||
|
term = new Terminal({cursorBlink: true});
|
||||||
|
|
||||||
|
console.log(url);
|
||||||
|
term.on('data', function(data) {
|
||||||
|
// console.log(data);
|
||||||
|
socket.send(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.onopen = function(e) {
|
||||||
|
$('.container').hide();
|
||||||
|
term.open(terminal, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = function(msg) {
|
||||||
|
// console.log(msg);
|
||||||
|
term.write(msg.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = function(e) {
|
||||||
|
console.log(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function(e) {
|
||||||
|
console.log(e);
|
||||||
|
term.destroy();
|
||||||
|
$('.container').show();
|
||||||
|
$('#status').text(e.reason);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
Vendored
+4
File diff suppressed because one or more lines are too long
+5132
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title> WebSSH </title>
|
||||||
|
<link href="static/img/favicon.png" rel="icon" type="image/png">
|
||||||
|
<link href="static/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
|
||||||
|
<link href="static/css/xterm.min.css" rel="stylesheet" type="text/css"/>
|
||||||
|
<link href="static/css/fullscreen.min.css" rel="stylesheet" type="text/css"/>
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<form id="connect" action="" type="post" enctype="multipart/form-data">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label for="Hostname">Hostname</label>
|
||||||
|
<input class="form-control" type="text" name="hostname" value="">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="Port">Port</label>
|
||||||
|
<input class="form-control" type="text" name="port" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label for="Username">Username</label>
|
||||||
|
<input class="form-control" type="text" name="username" value="">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="Username">Private Key</label>
|
||||||
|
<input class="form-control" type="file" name="privatekey" value="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label for="Password">Password</label>
|
||||||
|
<input class="form-control" type="password" name="password" placeholder="" value="">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
If Private Key is chosen, password will be used to decrypt the Private Key if it is encrypted, otherwise used as the password of username.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% module xsrf_form_html() %}
|
||||||
|
<button type="submit" class="btn btn-primary">Connect</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="terminal"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="static/js/jquery.min.js"></script>
|
||||||
|
<script src="static/js/popper.min.js"></script>
|
||||||
|
<script src="static/js/bootstrap.min.js"></script>
|
||||||
|
<script src="static/js/xterm.min.js"></script>
|
||||||
|
<script src="static/js/fullscreen.min.js"></script>
|
||||||
|
<script src="static/js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user