mirror of https://github.com/huashengdun/webssh
Merge 40ade4df69
into bb2fba30f3
commit
74e66dc2f2
|
@ -3,7 +3,6 @@ language: python
|
|||
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
|
|
30
README.md
30
README.md
|
@ -203,6 +203,36 @@ Running as a standalone server
|
|||
```bash
|
||||
wssh --port=8080 --sslport=4433 --certfile='cert.crt' --keyfile='cert.key' --xheaders=False --policy=reject
|
||||
```
|
||||
|
||||
### Profiling
|
||||
|
||||
Due to security, we should not disclose our private keys to anybody. Especially transfer
|
||||
the private key and the passphrase in the same transaction, although the HTTPS protocol
|
||||
can protect the transaction data.
|
||||
|
||||
This feature can provide the selectable profiles (just like ~/.ssh/config), it provides
|
||||
the features just like the SSH Client config file (normally located at ~/.ssh/config) like this:
|
||||
|
||||
```yaml
|
||||
required: False #If true, the profile is required to be selected before connect
|
||||
profiles:
|
||||
- name: The label will be shown on the profiles dropdown box
|
||||
description: "It will be shown on the tooltip"
|
||||
host: my-server.com
|
||||
port: 22
|
||||
username: user
|
||||
private-key: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
ABCD........
|
||||
......
|
||||
......
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
- name: Profile 2
|
||||
description: "It will shown on the tooltip"
|
||||
host: my-server.com
|
||||
port: 22
|
||||
username: user2
|
||||
```
|
||||
|
||||
|
||||
### Tips
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
paramiko==2.10.4
|
||||
tornado==5.1.1; python_version < '3.5'
|
||||
tornado==6.1.0; python_version >= '3.5'
|
||||
PyYAML>=5.3.1
|
||||
|
||||
#The following package used for testing
|
||||
#pytest
|
||||
#pytest-cov
|
||||
#codecov
|
||||
#flake8
|
||||
#mock
|
||||
|
|
1
setup.py
1
setup.py
|
@ -27,7 +27,6 @@ setup(
|
|||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
required: true #If true, user have to select one of the profiles
|
||||
profiles:
|
||||
- name: sample1
|
||||
description: "Long description"
|
||||
host: localhost
|
||||
port: 22
|
||||
#optional, if specified, the username field should not be shown on the template
|
||||
username: robey
|
||||
|
||||
- name: sample2
|
||||
description: "Long description"
|
||||
host: localhost
|
||||
port: 22
|
||||
#optional, if specified, the username field should not be shown on the template
|
||||
username: robey
|
||||
#optional, if specified.
|
||||
#The below private key is clone from ./tests/data/user_rsa_key
|
||||
private-key: |
|
||||
-----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-----
|
|
@ -0,0 +1,99 @@
|
|||
import pytest, os, re, yaml, random
|
||||
from tornado.options import options
|
||||
from tornado.testing import AsyncTestCase, AsyncHTTPTestCase
|
||||
from webssh.main import make_app, make_handlers
|
||||
from webssh.settings import get_app_settings
|
||||
from tests.utils import make_tests_data_path
|
||||
from yaml.loader import SafeLoader
|
||||
|
||||
class TestYAMLLoading(object):
|
||||
def test_profile_samples(self):
|
||||
if 'PROFILES' in os.environ: del os.environ['PROFILES']
|
||||
assert 'profiles' not in get_app_settings(options)
|
||||
|
||||
os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml')
|
||||
assert 'profiles' in get_app_settings(options)
|
||||
profiles=get_app_settings(options)['profiles']['profiles']
|
||||
assert profiles[0]['name']=='sample1'
|
||||
assert profiles[0]['description']=='Long description'
|
||||
assert profiles[0]['host']=='localhost'
|
||||
assert profiles[0]['port']==22
|
||||
assert profiles[0]['username']=='robey'
|
||||
|
||||
assert profiles[1]['name']=='sample2'
|
||||
assert profiles[1]['description']=='Long description'
|
||||
assert profiles[1]['host']=='localhost'
|
||||
assert profiles[1]['port']==22
|
||||
assert profiles[1]['username']=='robey'
|
||||
assert profiles[1]['private-key']==open(make_tests_data_path('user_rsa_key'), 'r').read()
|
||||
del os.environ['PROFILES']
|
||||
|
||||
class _TestBasic_(object):
|
||||
running = [True]
|
||||
sshserver_port = 2200
|
||||
body = 'hostname={host}&port={port}&profile={profile}&username={username}&password={password}'
|
||||
headers = {'Cookie': '_xsrf=yummy'}
|
||||
|
||||
def _getApp_(self, **kwargs):
|
||||
loop = self.io_loop
|
||||
options.debug = False
|
||||
options.policy = random.choice(['warning', 'autoadd'])
|
||||
options.hostfile = ''
|
||||
options.syshostfile = ''
|
||||
options.tdstream = ''
|
||||
options.delay = 0.1
|
||||
#options.profiles=make_tests_data_path('tests/data/profiles-sample.yaml')
|
||||
app = make_app(make_handlers(loop, options), get_app_settings(options))
|
||||
return app
|
||||
|
||||
class TestWebGUIWithProfiles(AsyncHTTPTestCase, _TestBasic_):
|
||||
def get_app(self):
|
||||
try:
|
||||
os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml')
|
||||
return self._getApp_()
|
||||
finally:
|
||||
del os.environ['PROFILES']
|
||||
|
||||
|
||||
def test_get_app_settings(self):
|
||||
try:
|
||||
os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml')
|
||||
settings=get_app_settings(options)
|
||||
assert 'profiles' in settings
|
||||
profiles=settings['profiles']['profiles']
|
||||
assert profiles[0]['name']=='sample1'
|
||||
assert profiles[0]['description']=='Long description'
|
||||
assert profiles[0]['host']=='localhost'
|
||||
assert profiles[0]['port']==22
|
||||
assert profiles[0]['username']=='robey'
|
||||
|
||||
assert profiles[1]['name']=='sample2'
|
||||
assert profiles[1]['description']=='Long description'
|
||||
assert profiles[1]['host']=='localhost'
|
||||
assert profiles[1]['port']==22
|
||||
assert profiles[1]['username']=='robey'
|
||||
assert profiles[1]['private-key']==open(make_tests_data_path('user_rsa_key'), 'r').read()
|
||||
finally:
|
||||
del os.environ['PROFILES']
|
||||
|
||||
def test_without_profiles(self):
|
||||
rep = self.fetch('/')
|
||||
assert rep.code==200, 'Testing server response status code: {0}'.format(rep.code)
|
||||
assert str(rep.body).index('<!-- PROFILES -->')>=0, 'Expected the "profiles.html" but "index.html"'
|
||||
|
||||
class TestWebGUIWithoutProfiles(AsyncHTTPTestCase, _TestBasic_):
|
||||
def get_app(self):
|
||||
if 'PROFILES' in os.environ: del os.environ['PROFILES']
|
||||
return self._getApp_()
|
||||
|
||||
def test_get_app_settings(self):
|
||||
if 'PROFILES' in os.environ: del os.environ['PROFILES']
|
||||
settings=get_app_settings(options)
|
||||
assert 'profiles' not in settings
|
||||
|
||||
def test_with_profiles(self):
|
||||
rep = self.fetch('/')
|
||||
assert rep.code==200, 'Testing server response status code: {0}'.format(rep.code)
|
||||
with pytest.raises(ValueError):
|
||||
str(rep.body).index('<!-- PROFILES -->')
|
||||
assert False, 'Expected the origin "index.html" but "profiles.html"'
|
|
@ -387,12 +387,37 @@ class IndexHandler(MixinHandler, tornado.web.RequestHandler):
|
|||
hostname, port)
|
||||
)
|
||||
|
||||
def get_profile(self):
|
||||
profiles = self.settings.get('profiles', None)
|
||||
if profiles: # If the profiles is configurated
|
||||
value = self.get_argument('profile', None)
|
||||
if profiles.get('required', False) \
|
||||
and len(profiles['profiles']) > 0 \
|
||||
and not value:
|
||||
raise InvalidValueError(
|
||||
'Argument "profile" is required according to your settings.'
|
||||
)
|
||||
if not (value is None or profiles['profiles'] is None):
|
||||
return profiles['profiles'][int(value)]
|
||||
return None
|
||||
|
||||
def get_args(self):
|
||||
hostname = self.get_hostname()
|
||||
port = self.get_port()
|
||||
username = self.get_value('username')
|
||||
profile = self.get_profile()
|
||||
if profile is not None and len(profile) > 0:
|
||||
hostname = profile.get('host', self.get_hostname())
|
||||
port = profile.get('port', self.get_port())
|
||||
username = profile.get('username', self.get_value('username'))
|
||||
if 'private-key' in profile:
|
||||
filename = ''
|
||||
privatekey = profile['private-key']
|
||||
else:
|
||||
privatekey, filename = self.get_privatekey()
|
||||
else:
|
||||
hostname = self.get_hostname()
|
||||
port = self.get_port()
|
||||
username = self.get_value('username')
|
||||
privatekey, filename = self.get_privatekey()
|
||||
password = self.get_argument('password', u'')
|
||||
privatekey, filename = self.get_privatekey()
|
||||
passphrase = self.get_argument('passphrase', u'')
|
||||
totp = self.get_argument('totp', u'')
|
||||
|
||||
|
@ -488,7 +513,16 @@ class IndexHandler(MixinHandler, tornado.web.RequestHandler):
|
|||
pass
|
||||
|
||||
def get(self):
|
||||
self.render('index.html', debug=self.debug, font=self.font)
|
||||
profiles = self.settings.get('profiles')
|
||||
if profiles and len(profiles) > 0:
|
||||
self.render(
|
||||
'profiles.html',
|
||||
profiles=profiles,
|
||||
debug=self.debug,
|
||||
font=self.font
|
||||
)
|
||||
else:
|
||||
self.render('index.html', debug=self.debug, font=self.font)
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def post(self):
|
||||
|
|
|
@ -3,6 +3,10 @@ import os.path
|
|||
import ssl
|
||||
import sys
|
||||
|
||||
import os
|
||||
import yaml
|
||||
from yaml.loader import SafeLoader
|
||||
|
||||
from tornado.options import define
|
||||
from webssh.policy import (
|
||||
load_host_keys, get_policy_class, check_policy_setting
|
||||
|
@ -12,6 +16,11 @@ from webssh.utils import (
|
|||
)
|
||||
from webssh._version import __version__
|
||||
|
||||
try:
|
||||
FileNotFoundError
|
||||
except NameError:
|
||||
FileNotFoundError = IOError
|
||||
|
||||
|
||||
def print_version(flag):
|
||||
if flag:
|
||||
|
@ -73,6 +82,30 @@ class Font(object):
|
|||
return '/'.join(dirs + [filename])
|
||||
|
||||
|
||||
def get_profiles():
|
||||
filename = os.getenv('PROFILES', None)
|
||||
if filename:
|
||||
if not filename.startswith(os.sep):
|
||||
filename = os.path.join(os.path.abspath(os.sep), filename)
|
||||
try:
|
||||
if not os.path.exists(filename):
|
||||
raise FileNotFoundError()
|
||||
with open(filename, 'r') as fp:
|
||||
result = yaml.load(fp, Loader=SafeLoader)
|
||||
if result:
|
||||
idx = 0
|
||||
for p in result['profiles']:
|
||||
p['index'] = idx
|
||||
idx += 1
|
||||
result['required'] = bool(result.get('required', 'False'))
|
||||
return result
|
||||
except FileNotFoundError:
|
||||
logging.warning('Cannot found file profiles: {0}'.format(filename))
|
||||
except Exception:
|
||||
logging.warning('Unexpected error', exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def get_app_settings(options):
|
||||
settings = dict(
|
||||
template_path=os.path.join(base_dir, 'webssh', 'templates'),
|
||||
|
@ -87,6 +120,9 @@ def get_app_settings(options):
|
|||
),
|
||||
origin_policy=get_origin_setting(options)
|
||||
)
|
||||
settings['profiles'] = get_profiles()
|
||||
if not settings['profiles']:
|
||||
del settings['profiles']
|
||||
return settings
|
||||
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Bootstrap Cookie Alert by Wruczek
|
||||
* https://github.com/Wruczek/Bootstrap-Cookie-Alert
|
||||
* Released under MIT license
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var cookieAlert = document.querySelector(".cookiealert");
|
||||
var acceptCookies = document.querySelector(".acceptcookies");
|
||||
|
||||
if (!cookieAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
cookieAlert.offsetHeight; // Force browser to trigger reflow (https://stackoverflow.com/a/39451131)
|
||||
|
||||
// Show the alert if we cant find the "acceptCookies" cookie
|
||||
if (!getCookie("acceptCookies")) {
|
||||
cookieAlert.classList.add("show");
|
||||
}
|
||||
|
||||
// When clicking on the agree button, create a 1 year
|
||||
// cookie to remember user's choice and close the banner
|
||||
acceptCookies.addEventListener("click", function () {
|
||||
setCookie("acceptCookies", true, 365);
|
||||
cookieAlert.classList.remove("show");
|
||||
|
||||
// dispatch the accept event
|
||||
window.dispatchEvent(new Event("cookieAlertAccept"))
|
||||
});
|
||||
|
||||
// Cookie functions from w3schools
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) === 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
})();
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,2 @@
|
|||
/*! js-cookie v3.0.1 | MIT */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})}));
|
|
@ -0,0 +1,70 @@
|
|||
(function($){
|
||||
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('form').validate({'ignore':'.ignore-validation'});
|
||||
|
||||
$('.profile-item').click(function(evt){
|
||||
console.log('Selected a profile: '+$(this).text());
|
||||
let dd=$(this).parents('div.dropdown:first');
|
||||
$(dd).find('button:first').text($(this).text());
|
||||
$('input:first').val($(this).attr('value'));
|
||||
|
||||
let profile=$(this).attr('value');
|
||||
if(profile=='')profile='-1';
|
||||
let found=false;
|
||||
for(var i=0; i<profiles.length; i++)
|
||||
if(profiles[i]['index']==profile){
|
||||
profile=profiles[i];
|
||||
found=true;
|
||||
break;
|
||||
}
|
||||
|
||||
$('form#connect').trigger('reset');
|
||||
$('.fld-private-key').show();
|
||||
$('.fld-password').show()
|
||||
$('input[type=text],input[type=password],input[type=number]').val('').removeAttr('readonly');
|
||||
if(found){
|
||||
$('input[name=profile]').val(profile['index']);
|
||||
if(profile['host'])$('input[name=hostname]').val('somewhere.com').attr('readonly', 'readonly');
|
||||
if(profile['port'])$('input[name=port]').val('65535').attr('readonly', 'readonly');
|
||||
if(profile['username'])$('input[name=username]').val('somebody').attr('readonly', 'readonly');
|
||||
if(profile['private-key']){
|
||||
$('.fld-private-key').hide();
|
||||
$('input[name=passphrase]').focus().select();
|
||||
$('.fld-password').hide()
|
||||
}else{
|
||||
$('input[name=password]').focus().select();
|
||||
}
|
||||
}
|
||||
|
||||
if(Boolean(Cookies.get('acceptCookies'))){
|
||||
console.debug('Store the profile: '+profile['index']+' - '+profile['name']);
|
||||
let expired=new Date();
|
||||
expired.setTime(expired.getTime()+30*86400000); //expired=now+30days; 86400000=1000*60*60*24
|
||||
Cookies.set('profileIndex', profile['index'], {'expires':expired, 'path':'/'});
|
||||
Cookies.set('profileName', profile['name'], {'expires':expired, 'path':'/'});
|
||||
}
|
||||
|
||||
return this;
|
||||
});
|
||||
|
||||
|
||||
let lastIndex=Cookies.get('profileIndex');
|
||||
let lastName =Cookies.get('profileName');
|
||||
if(Boolean(Cookies.get('acceptCookies')) && lastIndex!=undefined && lastName!=undefined){
|
||||
console.debug('Restore the last selected profile: '+lastIndex+' - '+lastName);
|
||||
let found=false;
|
||||
$('.profile-item').each(function(idx, val){
|
||||
if($(this).attr('value')==lastIndex && $(this).text()==lastName){
|
||||
found=true;
|
||||
$(this).trigger('click');
|
||||
console.info('Restored the last profile['+lastIndex+'] - '+lastName);
|
||||
}
|
||||
});
|
||||
if(!found)console.info('Profile index and name mismatched!');
|
||||
}
|
||||
|
||||
console.log('/static/js/profiles.js loaded');
|
||||
});
|
||||
})(jQuery);
|
|
@ -0,0 +1,140 @@
|
|||
<!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"/>
|
||||
<link href="static/css/cookiealert.css" rel="stylesheet" type="text/css"/>
|
||||
<style>
|
||||
.row {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
margin-left: 5px;
|
||||
}
|
||||
{% if font.family %}
|
||||
@font-face {
|
||||
font-family: '{{ font.family }}';
|
||||
src: url('{{ font.url }}');
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: '{{ font.family }}';
|
||||
}
|
||||
{% end %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="waiter" style="display: none"> Connecting ... </div>
|
||||
<!-- PROFILES -->
|
||||
|
||||
<div class="container form-container" style="display: none">
|
||||
<form id="connect" action="" method="post" enctype="multipart/form-data"{% if debug %} novalidate{% end %}>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="Profile">Profile</label>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="profilesBtn" data-toggle="dropdown" aria-expanded="false">
|
||||
Select a profile
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="profilesBtn">
|
||||
<a class="dropdown-item profile-item" href="#" value="">[[ Clear ]]</a>
|
||||
{%for p in profiles['profiles']%}<a class="dropdown-item profile-item" href="#" value="{{p['index']}}" {%if 'description' in p%}data-toggle="tooltip" title="{{p['description']}}" tooltip-delay="3"{%end%}>{{p['name']}}</a>{%end%}
|
||||
</div>
|
||||
<input type="hidden" name="profile" value="" {%if profiles['required']%}required{%end%}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="Hostname">Hostname</label>
|
||||
<input class="form-control" type="text" id="hostname" name="hostname" value="" required>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="Port">Port</label>
|
||||
<input class="form-control" type="number" id="port" name="port" placeholder="22" value="" min=1 max=65535>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="Username">Username</label>
|
||||
<input class="form-control" type="text" id="username" name="username" value="" required>
|
||||
</div>
|
||||
<div class="col fld-password">
|
||||
<label for="Password">Password</label>
|
||||
<input class="form-control" type="password" id="password" name="password" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col fld-private-key">
|
||||
<label for="Username">Private Key</label>
|
||||
<input class="form-control" type="file" id="privatekey" name="privatekey" value="">
|
||||
</div>
|
||||
<div class="col fld-passphrase">
|
||||
<label for="Passphrase">Passphrase</label>
|
||||
<input class="form-control" type="password" id="passphrase" name="passphrase" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="totp">Totp (time-based one-time password)</label>
|
||||
<input class="form-control" type="password" id="totp" name="totp" value="">
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="term" name="term" value="xterm-256color">
|
||||
{% module xsrf_form_html() %}
|
||||
<button type="submit" class="btn btn-primary">Connect</button>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="status" style="color: red;"></div>
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
|
||||
<!-- START Bootstrap-Cookie-Alert -->
|
||||
<div class="alert text-center cookiealert" role="alert">
|
||||
<b>Do you like cookies?</b> 🍪 We use cookies to ensure you get the best experience on our website. It will be used to store your preferred profile only! <a href="https://cookiesandyou.com/" target="_blank">Learn more</a>
|
||||
<button type="button" class="btn btn-primary btn-sm acceptcookies">I agree</button>
|
||||
</div>
|
||||
<!-- END Bootstrap-Cookie-Alert -->
|
||||
|
||||
<script type="text/javascript"><!--
|
||||
const profiles=[{%for p in profiles['profiles']%}{
|
||||
"index": {{p['index']}},
|
||||
"name": "{{p['name']}}",
|
||||
"host": {{'true' if 'host' in p else 'false'}},
|
||||
"port": {{'true' if 'port' in p else 'false'}},
|
||||
"username": {{'true' if 'username' in p else 'false'}},
|
||||
"private-key": {{'true' if ('private-key' in p and len(p['private-key'])>0) else 'false'}},
|
||||
},{%end%}
|
||||
];
|
||||
//></script>
|
||||
<script src="static/js/jquery.min.js"></script>
|
||||
<script src="static/js/jquery.validation-1.19.3.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/xterm-addon-fit.min.js"></script>
|
||||
<script src="static/js/cookiealert.js"></script>
|
||||
<script src="static/js/js.cookie.min.js"></script>
|
||||
<script src="static/js/main.js"></script>
|
||||
<script src="static/js/profiles.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue