Skip to content
Browse files

SSH Plugin: The XHR-based known_hosts handler has been removed and re…

…placed with logic that works over the WebSocket.

SSH Plugin: The fingerprint display now works properly with ECDSA fingerprints.
ssh.js:  The known_hosts text editor is now stored as `GateOne.SSH.khEditor` for easy reference.
  • Loading branch information...
1 parent a446cc1 commit 1d0e8037fbfb7c270f3710ce24154e24b7031bea @liftoff committed Nov 22, 2015
View
2 gateone/__init__.py
@@ -3,7 +3,7 @@
__version_info__ = (1, 2, 0)
__license__ = "AGPLv3" # ...or proprietary (see LICENSE.txt)
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
-__commit__ = "20151112095820" # Gets replaced by git (holds the date/time)
+__commit__ = "20151116212858" # Gets replaced by git (holds the date/time)
import os
GATEONE_DIR = os.path.dirname(os.path.abspath(__file__))
View
173 gateone/applications/terminal/plugins/ssh/ssh.py
@@ -12,18 +12,19 @@
This Python plugin file implements the following hooks::
hooks = {
- 'Web': [(r"/ssh", KnownHostsHandler)],
'WebSocket': {
- 'ssh_get_connect_string': get_connect_string,
- 'ssh_execute_command': ws_exec_command,
- 'ssh_get_identities': get_identities,
- 'ssh_get_public_key': get_public_key,
- 'ssh_get_private_key': get_private_key,
- 'ssh_get_host_fingerprint': get_host_fingerprint,
- 'ssh_gen_new_keypair': generate_new_keypair,
- 'ssh_store_id_file': store_id_file,
- 'ssh_delete_identity': delete_identity,
- 'ssh_set_default_identities': set_default_identities
+ 'terminal:ssh_get_known_hosts': get_known_hosts,
+ 'terminal:ssh_save_known_hosts': save_known_hosts,
+ 'terminal:ssh_get_connect_string': get_connect_string,
+ 'terminal:ssh_execute_command': ws_exec_command,
+ 'terminal:ssh_get_identities': get_identities,
+ 'terminal:ssh_get_public_key': get_public_key,
+ 'terminal:ssh_get_private_key': get_private_key,
+ 'terminal:ssh_get_host_fingerprint': get_host_fingerprint,
+ 'terminal:ssh_gen_new_keypair': generate_new_keypair,
+ 'terminal:ssh_store_id_file': store_id_file,
+ 'terminal:ssh_delete_identity': delete_identity,
+ 'terminal:ssh_set_default_identities': set_default_identities
},
'Escape': opt_esc_handler,
'Events': {
@@ -76,7 +77,6 @@
re.MULTILINE|re.DOTALL)
TIMER = None # Used to store temporary, cancellable timeouts
# TODO: make execute_command() a user-configurable option... So it will automatically run whatever command(s) the user likes whenever they connect to a given server. Differentiate between when they connect and when they start up a master or slave channel.
-# TODO: Make it so that equivalent KnownHostsHandler functionality works over the WebSocket.
# Exceptions
class SSHMultiplexingException(Exception):
@@ -381,60 +381,109 @@ def ws_exec_command(self, settings):
self.write_message(message)
# Handlers
-class KnownHostsHandler(BaseHandler):
- """
- This handler allows the client to view, edit, and upload the known_hosts
- file associated with their user account.
- """
- @tornado.web.authenticated
- def get(self):
- """
- Determine what the user is asking for and call the appropriate method.
- """ # NOTE: Just dealing with known_hosts for now but keys are next
- get_kh = self.get_argument('known_hosts', None)
- if get_kh:
- self._return_known_hosts()
-
- @tornado.web.authenticated
- def post(self):
- """
- Determine what the user is updating by checking the given arguments and
- proceed with the update.
- """
- known_hosts = self.get_argument('known_hosts', None)
- if known_hosts:
- kh = self.request.body
- self._save_known_hosts(kh)
-
- def _return_known_hosts(self):
- """Returns the user's known_hosts file in text/plain format."""
- user = self.current_user['upn']
- ssh_log.debug("known_hosts requested by %s" % user)
- users_dir = os.path.join(self.settings['user_dir'], user) # "User's dir"
- users_ssh_dir = os.path.join(users_dir, '.ssh')
- kh_path = os.path.join(users_ssh_dir, 'known_hosts')
- known_hosts = ""
- if os.path.exists(kh_path):
- known_hosts = open(kh_path).read()
- self.set_header ('Content-Type', 'text/plain')
- self.write(known_hosts)
-
- def _save_known_hosts(self, known_hosts):
- """Save the given *known_hosts* file."""
- user = self.current_user['upn']
- users_dir = os.path.join(self.settings['user_dir'], user) # "User's dir"
- users_ssh_dir = os.path.join(users_dir, '.ssh')
- kh_path = os.path.join(users_ssh_dir, 'known_hosts')
- # Letting Tornado's exception handler deal with errors here
- with io.open(kh_path, 'wb') as f:
- f.write(known_hosts)
- self.write("success")
+#class KnownHostsHandler(BaseHandler):
+ #"""
+ #This handler allows the client to view, edit, and upload the known_hosts
+ #file associated with their user account.
+ #"""
+ #@tornado.web.authenticated
+ #def get(self):
+ #"""
+ #Determine what the user is asking for and call the appropriate method.
+ #""" # NOTE: Just dealing with known_hosts for now but keys are next
+ #get_kh = self.get_argument('known_hosts', None)
+ #if get_kh:
+ #self._return_known_hosts()
+
+ #@tornado.web.authenticated
+ #def post(self):
+ #"""
+ #Determine what the user is updating by checking the given arguments and
+ #proceed with the update.
+ #"""
+ #known_hosts = self.get_argument('known_hosts', None)
+ #if known_hosts:
+ #kh = self.request.body
+ #self._save_known_hosts(kh)
+
+ #def _return_known_hosts(self):
+ #"""Returns the user's known_hosts file in text/plain format."""
+ #user = self.current_user['upn']
+ #ssh_log.debug("known_hosts requested by %s" % user)
+ #users_dir = os.path.join(self.settings['user_dir'], user) # "User's dir"
+ #users_ssh_dir = os.path.join(users_dir, '.ssh')
+ #kh_path = os.path.join(users_ssh_dir, 'known_hosts')
+ #known_hosts = ""
+ #if os.path.exists(kh_path):
+ #known_hosts = open(kh_path).read()
+ #self.set_header ('Content-Type', 'text/plain')
+ #self.write(known_hosts)
+
+ #def _save_known_hosts(self, known_hosts):
+ #"""Save the given *known_hosts* file."""
+ #print("known_hosts: %s" % known_hosts)
+ #user = self.current_user['upn']
+ #users_dir = os.path.join(self.settings['user_dir'], user) # "User's dir"
+ #users_ssh_dir = os.path.join(users_dir, '.ssh')
+ #if not os.path.isdir(users_ssh_dir): # Make .ssh dir if not present
+ #mkdir_p(users_ssh_dir)
+ #os.chmod(users_ssh_dir, 0o700)
+ #kh_path = os.path.join(users_ssh_dir, 'known_hosts')
+ ## Letting Tornado's exception handler deal with errors here
+ #with io.open(kh_path, 'wb') as f:
+ #print("Writing known hosts: %s" % kh_path)
+ #f.write(known_hosts)
+ #self.write("success")
"""
WebSocket Commands
------------------
"""
# WebSocket commands (not the same as handlers)
+def get_known_hosts(self):
+ """
+ Attached to the (server-side) `terminal:ssh_get_known_hosts` WebSocket
+ action; returns the current user's known_hosts file.
+ """
+ user = self.current_user['upn']
+ ssh_log.debug("known_hosts requested by %s" % user)
+ users_dir = os.path.join(self.settings['user_dir'], user) # "User's dir"
+ users_ssh_dir = os.path.join(users_dir, '.ssh')
+ kh_path = os.path.join(users_ssh_dir, 'known_hosts')
+ known_hosts = ""
+ if os.path.exists(kh_path):
+ known_hosts = open(kh_path).read()
+ message = {
+ 'terminal:sshjs_known_hosts': {
+ 'known_hosts': known_hosts
+ }
+ }
+ self.write_message(message)
+
+def save_known_hosts(self, known_hosts):
+ """
+ Attached to the (server-side) `terminal:ssh_save_known_hosts` WebSocket
+ action; saves the given *known_hosts* (string) to the user's known_hosts
+ file.
+ """
+ user = self.current_user['upn']
+ ssh_log.debug("known_hosts updated by %s" % user)
+ users_dir = os.path.join(self.settings['user_dir'], user) # "User's dir"
+ users_ssh_dir = os.path.join(users_dir, '.ssh')
+ if not os.path.isdir(users_ssh_dir): # Make .ssh dir if not present
+ mkdir_p(users_ssh_dir)
+ os.chmod(users_ssh_dir, 0o700)
+ kh_path = os.path.join(users_ssh_dir, 'known_hosts')
+ try:
+ with io.open(kh_path, 'wb') as f:
+ f.write(known_hosts)
+ except Exception as e:
+ error_msg = _("Exception trying to save known_hosts file: %s" % e)
+ ssh_log.error(error_msg)
+ self.write_message(_(
+ "An error was encountered trying to save the known_hosts file. "
+ "See server logs for details."))
+
def get_connect_string(self, term):
"""
Attached to the (server-side) `terminal:ssh_get_connect_string` WebSocket
@@ -1161,8 +1210,10 @@ def initialize(self):
self.on('terminal:authenticate', bind(create_user_ssh_dir, self))
hooks = {
- 'Web': [(r"/ssh", KnownHostsHandler)],
+ #'Web': [(r"/ssh", KnownHostsHandler)],
'WebSocket': {
+ 'terminal:ssh_get_known_hosts': get_known_hosts,
+ 'terminal:ssh_save_known_hosts': save_known_hosts,
'terminal:ssh_get_connect_string': get_connect_string,
'terminal:ssh_execute_command': ws_exec_command,
'terminal:ssh_get_identities': get_identities,
View
86 gateone/applications/terminal/plugins/ssh/static/ssh.js
@@ -39,6 +39,7 @@ go.Base.update(go.SSH, {
GateOne.Net.addAction('terminal:sshjs_delete_identity_complete', GateOne.SSH.deleteCompleteAction);
GateOne.Net.addAction('terminal:sshjs_cmd_output', GateOne.SSH.commandCompleted);
GateOne.Net.addAction('terminal:sshjs_ask_passphrase', GateOne.SSH.enterPassphraseAction);
+ GateOne.Net.addAction('terminal:sshjs_known_hosts', GateOne.SSH.handleKnownHosts);
GateOne.Events.on("terminal:new_terminal", GateOne.SSH.getConnectString);
*/
var prefsPanel = u.getNode('#'+prefix+'panel_prefs'),
@@ -81,7 +82,8 @@ go.Base.update(go.SSH, {
};
prefsPanelKnownHosts.innerHTML = gettext("Edit Known Hosts");
prefsPanelKnownHosts.onclick = function() {
- u.xhrGet(go.prefs.url+'ssh?known_hosts=True', go.SSH.updateKH);
+ // Ask the server to send us the known_hosts file:
+ go.ws.send(JSON.stringify({"terminal:ssh_get_known_hosts": null}));
}
infoPanelManageIdentities.innerHTML = gettext("Manage Identities");
infoPanelManageIdentities.onclick = function() {
@@ -134,6 +136,7 @@ go.Base.update(go.SSH, {
go.Net.addAction('terminal:sshjs_delete_identity_complete', go.SSH.deleteCompleteAction);
go.Net.addAction('terminal:sshjs_cmd_output', go.SSH.commandCompleted);
go.Net.addAction('terminal:sshjs_ask_passphrase', go.SSH.enterPassphraseAction);
+ go.Net.addAction('terminal:sshjs_known_hosts', go.SSH.handleKnownHosts);
if (!go.prefs.broadcastTerminal) {
E.on("terminal:new_terminal", go.SSH.autoConnect);
E.on("terminal:new_terminal", go.SSH.getConnectString);
@@ -1102,19 +1105,21 @@ go.Base.update(go.SSH, {
}
go.Terminal.newTerminal();
},
- updateKH: function(known_hosts) {
- /**:GateOne.SSH.updateKH(known_hosts)
+ handleKnownHosts: function(message) {
+ /**:GateOne.SSH.handleKnownHosts(message)
- Updates the sshKHTextArea with the given *known_hosts* file.
-
- .. note:: Meant to be used as a callback function passed to :js:meth:`GateOne.Utils.xhrGet`.
+ Updates the sshKHTextArea with the contents of *message['known_hosts']*.
*/
var sshKHTextArea = u.getNode('#'+prefix+'ssh_kh_textarea'),
+ storeEditor = function(cm) {
+ // Stores the instance of CodeMirror as GateOne.SSH.khEditor
+ go.SSH.khEditor = cm;
+ },
enableEditor = function() {
// Add the Editor so we get line numbers
- go.Editor.fromTextArea(u.getNode("#go_default_ssh_kh_textarea"), { lineNumbers: true, lineWrapping: true, tabindex: 1, autofocus: true });
+ go.Editor.fromTextArea(u.getNode("#go_default_ssh_kh_textarea"), {lineNumbers: true, lineWrapping: true, tabindex: 1, autofocus: true}, storeEditor);
};
- sshKHTextArea.value = known_hosts;
+ sshKHTextArea.value = message.known_hosts;
// Now show the panel
v.togglePanel('#'+prefix+'panel_known_hosts', enableEditor);
},
@@ -1154,28 +1159,11 @@ go.Base.update(go.SSH, {
form.onsubmit = function(e) {
// Submit the modified known_hosts file to the server and notify when complete
e.preventDefault(); // Don't actually submit
- var kh = u.getNode('#'+prefix+'ssh_kh_textarea').value,
- xhr = new XMLHttpRequest(),
- handleStateChange = function(e) {
- var status = null;
- try {
- status = parseInt(e.target.status);
- } catch(e) {
- return;
- }
- if (e.target.readyState == 4 && status == 200 && e.target.responseText) {
- v.displayMessage(gettext("SSH Plugin: known_hosts saved."));
- // Hide the panel
- v.togglePanel('#'+prefix+'panel_known_hosts');
- }
- };
- if (xhr.addEventListener) {
- xhr.addEventListener('readystatechange', handleStateChange, false);
- } else {
- xhr.onreadystatechange = handleStateChange;
- }
- xhr.open('POST', go.prefs.url+'ssh?known_hosts=True', true);
- xhr.send(kh);
+ var kh = GateOne.SSH.khEditor.getValue();
+ go.ws.send(JSON.stringify({'terminal:ssh_save_known_hosts': kh}));
+ v.displayMessage(gettext("SSH Plugin: known_hosts saved."));
+ // Hide the panel
+ v.togglePanel('#'+prefix+'panel_known_hosts');
}
form.appendChild(sshHeader);
form.appendChild(sshKHTextArea);
@@ -1198,25 +1186,35 @@ go.Base.update(go.SSH, {
The fingerprint will be colorized using the hex values of the fingerprint as the color code with the last value highlighted in bold.
*/
// Example message: {"sshjs_display_fingerprint": {"result": "Success", "fingerprint": "cc:2f:b9:4f:f6:c0:e5:1d:1b:7a:86:7b:ff:86:97:5b"}}
+ console.log('message:', message);
if (message['result'] == 'Success') {
var fingerprint = message['fingerprint'],
hexes = fingerprint.split(':'),
text = '',
colorized = '',
- count = 0;
- colorized += '<span style="color: #';
- hexes.forEach(function(hex) {
- if (count == 3 || count == 6 || count == 9 || count == 12) {
- colorized += '">' + text + '</span><span style="color: #' + hex;
- text = hex;
- } else if (count == 15) {
- colorized += '">' + text + '</span><span style="text-decoration: underline">' + hex + '</span>';
- } else {
- colorized += hex;
- text += hex;
- }
- count += 1;
- });
+ count = 0,
+ temp;
+ if (fingerprint.indexOf('ECDSA') == -1) { // Old fashioned fingerprint
+ colorized += '<span style="color: #';
+ hexes.forEach(function(hex) {
+ if (count == 3 || count == 6 || count == 9 || count == 12) {
+ colorized += '">' + text + '</span><span style="color: #' + hex;
+ text = hex;
+ } else if (count == 15) {
+ colorized += '">' + text + '</span><span style="text-decoration: underline">' + hex + '</span>';
+ } else {
+ colorized += hex;
+ text += hex;
+ }
+ count += 1;
+ });
+ } else {
+ temp = fingerprint.split(':')[1]; // Just the fingerprint part
+ colorized = fingerprint.split(':')[0] + ':<br>' + temp.substring(0, temp.length - 8); // All but the last 8 chars which we'll highlight
+ colorized += '<span style="color: #0ACD24">'; // A nice bright green for the last few bits
+ colorized += temp.substr(temp.length - 8);
+ colorized += '</span>';
+ }
v.displayMessage('<i>' + message['host'] + '</i>: ' + colorized);
}
},
View
2 gateone/core/server.py
@@ -19,7 +19,7 @@
}
}
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
-__commit__ = "20151112095820" # Gets replaced by git (holds the date/time)
+__commit__ = "20151116212858" # Gets replaced by git (holds the date/time)
# NOTE: Docstring includes reStructuredText markup for use with Sphinx.
__doc__ = '''\
View
2 gateone/static/gateone.js
@@ -82,7 +82,7 @@ The base object for all Gate One modules/plugins.
*/
GateOne.__name__ = "GateOne";
GateOne.__version__ = "1.2";
-GateOne.__commit__ = "20151112095820";
+GateOne.__commit__ = "20151116212858";
GateOne.__repr__ = function () {
return "[" + this.__name__ + " " + this.__version__ + "]";
};

0 comments on commit 1d0e803

Please sign in to comment.
Something went wrong with that request. Please try again.