mirror of https://github.com/huashengdun/webssh
Merge 40ade4df69
into bb2fba30f3
commit
74e66dc2f2
|
@ -3,7 +3,6 @@ language: python
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.4"
|
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.6"
|
- "3.6"
|
||||||
- "3.7"
|
- "3.7"
|
||||||
|
|
30
README.md
30
README.md
|
@ -204,6 +204,36 @@ Running as a standalone server
|
||||||
wssh --port=8080 --sslport=4433 --certfile='cert.crt' --keyfile='cert.key' --xheaders=False --policy=reject
|
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
|
### Tips
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
paramiko==2.10.4
|
paramiko==2.10.4
|
||||||
tornado==5.1.1; python_version < '3.5'
|
tornado==5.1.1; python_version < '3.5'
|
||||||
tornado==6.1.0; 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',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Programming Language :: Python :: 3.7',
|
'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)
|
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):
|
def get_args(self):
|
||||||
|
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()
|
hostname = self.get_hostname()
|
||||||
port = self.get_port()
|
port = self.get_port()
|
||||||
username = self.get_value('username')
|
username = self.get_value('username')
|
||||||
password = self.get_argument('password', u'')
|
|
||||||
privatekey, filename = self.get_privatekey()
|
privatekey, filename = self.get_privatekey()
|
||||||
|
password = self.get_argument('password', u'')
|
||||||
passphrase = self.get_argument('passphrase', u'')
|
passphrase = self.get_argument('passphrase', u'')
|
||||||
totp = self.get_argument('totp', u'')
|
totp = self.get_argument('totp', u'')
|
||||||
|
|
||||||
|
@ -488,6 +513,15 @@ class IndexHandler(MixinHandler, tornado.web.RequestHandler):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
|
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)
|
self.render('index.html', debug=self.debug, font=self.font)
|
||||||
|
|
||||||
@tornado.gen.coroutine
|
@tornado.gen.coroutine
|
||||||
|
|
|
@ -3,6 +3,10 @@ import os.path
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from yaml.loader import SafeLoader
|
||||||
|
|
||||||
from tornado.options import define
|
from tornado.options import define
|
||||||
from webssh.policy import (
|
from webssh.policy import (
|
||||||
load_host_keys, get_policy_class, check_policy_setting
|
load_host_keys, get_policy_class, check_policy_setting
|
||||||
|
@ -12,6 +16,11 @@ from webssh.utils import (
|
||||||
)
|
)
|
||||||
from webssh._version import __version__
|
from webssh._version import __version__
|
||||||
|
|
||||||
|
try:
|
||||||
|
FileNotFoundError
|
||||||
|
except NameError:
|
||||||
|
FileNotFoundError = IOError
|
||||||
|
|
||||||
|
|
||||||
def print_version(flag):
|
def print_version(flag):
|
||||||
if flag:
|
if flag:
|
||||||
|
@ -73,6 +82,30 @@ class Font(object):
|
||||||
return '/'.join(dirs + [filename])
|
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):
|
def get_app_settings(options):
|
||||||
settings = dict(
|
settings = dict(
|
||||||
template_path=os.path.join(base_dir, 'webssh', 'templates'),
|
template_path=os.path.join(base_dir, 'webssh', 'templates'),
|
||||||
|
@ -87,6 +120,9 @@ def get_app_settings(options):
|
||||||
),
|
),
|
||||||
origin_policy=get_origin_setting(options)
|
origin_policy=get_origin_setting(options)
|
||||||
)
|
)
|
||||||
|
settings['profiles'] = get_profiles()
|
||||||
|
if not settings['profiles']:
|
||||||
|
del settings['profiles']
|
||||||
return settings
|
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