webssh

Web based ssh client https://github.com/huashengdun/webssh webssh.huashengdun.org/
git clone http://git.hanabi.in/repos/webssh.git
Log | Files | Refs | README | LICENSE

commit d6de1340c486b2c424a97e90e93e0aac8753f9a5
parent d38453fd0bc622de940cddd6054995052aff96f8
Author: Sheng <webmaster0115@gmail.com>
Date:   Sat, 18 Aug 2018 16:33:21 +0800

Added max_body_size for limiting the size of post form

Diffstat:
Mtests/sshserver.py | 3++-
Mtests/test_app.py | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Atests/user_rsa_key | 15+++++++++++++++
Atests/utils.py | 38++++++++++++++++++++++++++++++++++++++
Mwebssh/main.py | 4++--
Mwebssh/settings.py | 1+
6 files changed, 144 insertions(+), 5 deletions(-)

diff --git a/tests/sshserver.py b/tests/sshserver.py @@ -64,12 +64,13 @@ class Server(paramiko.ServerInterface): return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): + print('Auth attempt with username: {!r} & password: {!r}'.format(username, password)) # noqa if (username in ['robey', 'bar']) and (password == 'foo'): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): - print('Auth attempt with key: ' + u(hexlify(key.get_fingerprint()))) + print('Auth attempt with username: {!r} & key: {!r}'.format(username, u(hexlify(key.get_fingerprint())))) # noqa if (username == 'robey') and (key == self.good_pub_key): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED diff --git a/tests/test_app.py b/tests/test_app.py @@ -1,15 +1,17 @@ import json -import webssh.handler as handler import random import threading import tornado.websocket import tornado.gen +import webssh.handler as handler from tornado.testing import AsyncHTTPTestCase +from tornado.httpclient import HTTPError from tornado.options import options from webssh.main import make_app, make_handlers -from webssh.settings import get_app_settings +from webssh.settings import get_app_settings, max_body_size from tests.sshserver import run_ssh_server, banner +from tests.utils import encode_multipart_formdata handler.DELAY = 0.1 @@ -20,6 +22,12 @@ class TestApp(AsyncHTTPTestCase): running = [True] sshserver_port = 2200 body = u'hostname=127.0.0.1&port={}&username=robey&password=foo'.format(sshserver_port) # noqa + body_dict = { + 'hostname': '127.0.0.1', + 'port': str(sshserver_port), + 'username': 'robey', + 'password': '' + } def get_app(self): loop = self.io_loop @@ -44,6 +52,14 @@ class TestApp(AsyncHTTPTestCase): cls.running.pop() print('='*20) + def read_privatekey(self, filename): + return open(filename, 'rb').read().decode('utf-8') + + def get_httpserver_options(self): + options = super(TestApp, self).get_httpserver_options() + options.update(max_body_size=max_body_size) + return options + def test_app_with_invalid_form(self): response = self.fetch('/') self.assertEqual(response.code, 200) @@ -104,6 +120,74 @@ class TestApp(AsyncHTTPTestCase): ws.close() @tornado.testing.gen_test + def test_app_auth_with_valid_pubkey_for_user_robey(self): + url = self.get_url('/') + client = self.get_http_client() + response = yield client.fetch(url) + self.assertEqual(response.code, 200) + + privatekey = self.read_privatekey('tests/user_rsa_key') + files = [('privatekey', 'user_rsa_key', privatekey)] + content_type, body = encode_multipart_formdata(self.body_dict.items(), + files) + headers = { + "Content-Type": content_type, 'content-length': str(len(body)) + } + response = yield client.fetch(url, method="POST", headers=headers, + body=body) + data = json.loads(response.body.decode('utf-8')) + self.assertIsNone(data['status']) + self.assertIsNotNone(data['id']) + self.assertIsNotNone(data['encoding']) + + url = url.replace('http', 'ws') + ws_url = url + 'ws?id=' + data['id'] + ws = yield tornado.websocket.websocket_connect(ws_url) + msg = yield ws.read_message() + self.assertEqual(msg.decode(data['encoding']), banner) + ws.close() + + @tornado.testing.gen_test + def test_app_auth_with_invalid_pubkey_for_user_robey(self): + url = self.get_url('/') + client = self.get_http_client() + response = yield client.fetch(url) + self.assertEqual(response.code, 200) + + privatekey = self.read_privatekey('tests/user_rsa_key') + privatekey = privatekey[:100] + u'bad' + privatekey[100:] + files = [('privatekey', 'user_rsa_key', privatekey)] + content_type, body = encode_multipart_formdata(self.body_dict.items(), + files) + headers = { + "Content-Type": content_type, 'content-length': str(len(body)) + } + response = yield client.fetch(url, method="POST", headers=headers, + body=body) + data = json.loads(response.body.decode('utf-8')) + self.assertIsNotNone(data['status']) + self.assertIsNone(data['id']) + self.assertIsNone(data['encoding']) + + @tornado.testing.gen_test + def test_app_post_form_with_large_body_size(self): + url = self.get_url('/') + client = self.get_http_client() + response = yield client.fetch(url) + self.assertEqual(response.code, 200) + + privatekey = u'h' * (2 * max_body_size) + files = [('privatekey', 'user_rsa_key', privatekey)] + content_type, body = encode_multipart_formdata(self.body_dict.items(), + files) + headers = { + "Content-Type": content_type, 'content-length': str(len(body)) + } + + with self.assertRaises(HTTPError): + yield client.fetch(url, method="POST", headers=headers, body=body) + + @tornado.testing.gen_test def test_app_with_correct_credentials_user_robey(self): url = self.get_url('/') client = self.get_http_client() diff --git a/tests/user_rsa_key b/tests/user_rsa_key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99 +66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq ++adIl/Rvlby/mHakj+y05LBND6nFWHAn1y1gOFFKUXSJNRZPXSFy47gqzwIBIwKB +gQCbANjz7q/pCXZLp1Hz6tYHqOvlEmjK1iabB1oqafrMpJ0eibUX/u+FMHq6StR5 +M5413BaDWHokPdEJUnabfWXXR3SMlBUKrck0eAer1O8m78yxu3OEdpRk+znVo4DL +guMeCdJB/qcF0kEsx+Q8HP42MZU1oCmk3PbfXNFwaHbWuwJBAOQ/ry/hLD7AqB8x +DmCM82A9E59ICNNlHOhxpJoh6nrNTPCsBAEu/SmqrL8mS6gmbRKUaya5Lx1pkxj2 +s/kWOokCQQDhXCcYXjjWiIfxhl6Rlgkk1vmI0l6785XSJNv4P7pXjGmShXfIzroh +S8uWK3tL0GELY7+UAKDTUEVjjQdGxYSXAkEA3bo1JzKCwJ3lJZ1ebGuqmADRO6UP +40xH977aadfN1mEI6cusHmgpISl0nG5YH7BMsvaT+bs1FUH8m+hXDzoqOwJBAK3Z +X/za+KV/REya2z0b+GzgWhkXUGUa/owrEBdHGriQ47osclkUgPUdNqcLmaDilAF4 +1Z4PHPrI5RJIONAx+JECQQC/fChqjBgFpk6iJ+BOdSexQpgfxH/u/457W10Y43HR +soS+8btbHqjQkowQ/2NTlUfWvqIlfxs6ZbFsIp/HrhZL +-----END RSA PRIVATE KEY----- diff --git a/tests/utils.py b/tests/utils.py @@ -0,0 +1,38 @@ +import mimetypes +from uuid import uuid4 + + +def encode_multipart_formdata(fields, files): + """ + fields is a sequence of (name, value) elements for regular form fields. + files is a sequence of (name, filename, value) elements for data to be + uploaded as files. + Return (content_type, body) ready for httplib.HTTP instance + """ + boundary = uuid4().hex + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + boundary) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + boundary) + L.append( + 'Content-Disposition: form-data; name="%s"; filename="%s"' % ( + key, filename + ) + ) + L.append('Content-Type: %s' % get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + boundary + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % boundary + return content_type, body + + +def get_content_type(filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/webssh/main.py b/webssh/main.py @@ -5,7 +5,7 @@ import tornado.ioloop from tornado.options import parse_command_line, options from webssh.handler import IndexHandler, WsockHandler from webssh.settings import (get_app_settings, get_host_keys_settings, - get_policy_setting) + get_policy_setting, max_body_size) def make_handlers(loop, options): @@ -29,7 +29,7 @@ def main(): parse_command_line() loop = tornado.ioloop.IOLoop.current() app = make_app(make_handlers(loop, options), get_app_settings(options)) - app.listen(options.port, options.address) + app.listen(options.port, options.address, max_body_size=max_body_size) logging.info('Listening on {}:{}'.format(options.address, options.port)) loop.start() diff --git a/webssh/settings.py b/webssh/settings.py @@ -29,6 +29,7 @@ define('version', type=bool, help='Show version information', base_dir = os.path.dirname(__file__) +max_body_size = 1 * 1024 * 1024 def get_app_settings(options):