From 81ae0e71f78d9d864a7d5c260b6c430d895bb32d Mon Sep 17 00:00:00 2001 From: Dan McDougall Date: Wed, 8 Feb 2012 22:36:11 -0500 Subject: [PATCH] DOH: Added the logging and ssh plugin templates to the git repo. NO WONDER people were saying some things just don't look right. LOL! auth.py: Fixed all the authentication redirects so they works with the new url_prefix option. gateone.py: Fixed the login_url (Tornado setting) so it now uses the url_prefix option. gateone.py: Fixed the HTTPSRedirectHandler so it also takes the url_prefix into account. I also changed its URL pattern regex to be ".*" (meaning, 'match anything') from just "/" (meaning, 'match *just* http://whatever/'). That way a user can hit your Gate One server's hostname/IP via HTTP in a browser with *any* URL and get magically redirected to HTTPS on the proper port. gateone.py: Removed that leftover print statement from the new ErrorHandler. SSH Plugin: Added support for telnet:// URLs to ssh_connect.py. This should resolve https://github.com/liftoff/GateOne/issues/87 Bookmarks Plugin: Added support for telnet:// URLs as well. Bookmarks Plugin: Added a new type of tag that gets automatically added to bookmarks: URL type. Essentially this will let you filter the bookmarks list based on the type of URL in the bookmark. This was necessary in order to be able to easily differentiate between ssh:// and telnet:// URLs. Bookmarks Plugin: Added a new "Autotags" section to the tag cloud area where you can filter based on URL type (protocol) and the age of bookmarks (e.g. "<7 Days"). setup.py: Made a minor change to ensure that when it is run that the combined_plugins.js gets automatically overwritten. Playback Plugin: Changed playback.js a bit in an attempt to save some memory but I believe what I'm experiencing is a bug in Chrome... Everything stays nice and low (in terms of memory utilization) and then suddenly jumps a huge amount after switching tabs and then back again. It needs more investigation. Logging Plugin: Added the ability to download logs in the self-contained recording format. NOTE: Something strange is going on with log titles and the new telnet protocol support in ssh_connect.py. It'll be fixed soon. --- gateone/auth.py | 13 +- gateone/gateone.py | 40 ++- gateone/plugins/bookmarks/static/bookmarks.js | 335 ++++++++++++++--- .../plugins/bookmarks/templates/bookmarks.css | 34 +- gateone/plugins/logging/logging_plugin.py | 158 +++++++- gateone/plugins/logging/static/logging.js | 18 +- gateone/plugins/logging/templates/logging.css | 132 +++++++ .../logging/templates/playback_log.html | 336 ++++++++++++++++++ gateone/plugins/playback/playback.py | 80 ++++- gateone/plugins/playback/static/playback.js | 74 ++-- .../templates/self_contained_recording.html | 1 - gateone/plugins/ssh/scripts/ssh_connect.py | 145 +++++++- gateone/plugins/ssh/ssh.py | 3 +- gateone/plugins/ssh/templates/ssh.css | 193 ++++++++++ gateone/static/combined_plugins.js | 2 +- gateone/static/gateone.js | 15 +- gateone/templates/themes/dark-black.css | 1 - gateone/termio.py | 2 +- gateone/utils.py | 1 + setup.py | 3 + 20 files changed, 1445 insertions(+), 141 deletions(-) create mode 100644 gateone/plugins/logging/templates/logging.css create mode 100644 gateone/plugins/logging/templates/playback_log.html create mode 100644 gateone/plugins/ssh/templates/ssh.css diff --git a/gateone/auth.py b/gateone/auth.py index b654165c..4bd49c93 100644 --- a/gateone/auth.py +++ b/gateone/auth.py @@ -96,7 +96,7 @@ def user_login(self, user): logging.info(_("Creating user directory: %s" % user_dir)) mkdir_p(user_dir) os.chmod(user_dir, 0700) - session_file = user_dir + '/session' + session_file = os.path.join(user_dir, 'session') if os.path.exists(session_file): session_data = open(session_file).read() session_info = tornado.escape.json_decode(session_data) @@ -117,11 +117,12 @@ def user_logout(self, user, redirect=None): information and optionally, redirects them to *redirect* (URL). """ logging.debug("user_logout(%s)" % user) + url_prefix = self.settings['url_prefix'] if redirect: self.write(redirect) self.finish() else: - self.write("/") + self.write(url_prefix) self.finish() class NullAuthHandler(BaseAuthHandler): @@ -162,7 +163,7 @@ def get(self): if next_url: self.redirect(next_url) else: - self.redirect("/") + self.redirect(self.settings['url_prefix']) class GoogleAuthHandler(BaseAuthHandler, tornado.auth.GoogleMixin): """ @@ -220,7 +221,7 @@ def _on_auth(self, user): if next_url: self.redirect(next_url) else: - self.redirect("/") + self.redirect(self.settings['url_prefix']) # Add our KerberosAuthHandler if sso is available KerberosAuthHandler = None @@ -270,7 +271,7 @@ def _on_auth(self, user): if next_url: self.redirect(next_url) else: - self.redirect("/") + self.redirect(self.settings['url_prefix']) except ImportError: pass # No SSO available. @@ -321,6 +322,6 @@ def _on_auth(self, user): if next_url: self.redirect(next_url) else: - self.redirect("/") + self.redirect(self.settings['url_prefix']) except ImportError: pass # No PAM auth available. diff --git a/gateone/gateone.py b/gateone/gateone.py index 54be27e7..236cdf2b 100755 --- a/gateone/gateone.py +++ b/gateone/gateone.py @@ -611,7 +611,10 @@ class HTTPSRedirectHandler(tornado.web.RequestHandler): def get(self): """Just redirects the client from HTTP to HTTPS""" port = self.settings['port'] - self.redirect('https://%s:%s/' % self.request.headers['Host'], port) + url_prefix = self.settings['url_prefix'] + self.redirect( + 'https://%s:%s%s' % ( + self.request.headers['Host'], port, url_prefix)) class BaseHandler(tornado.web.RequestHandler): """ @@ -1115,13 +1118,14 @@ def authenticate(self, settings): for css_template in plugins['css']: self.write_message(json_encode({'load_css': css_template})) - def new_multiplex(self, cmd, term_id): + def new_multiplex(self, cmd, term_id, logging=True): """ Returns a new instance of :py:class:`termio.Multiplex` with the proper global and client-specific settings. * *cmd* - The command to execute inside of Multiplex. * *term_id* - The terminal to associate with this Multiplex or a descriptive identifier (it's only used for logging purposes). + * *logging* - If False, logging will be disabled for this instance of Multiplex (even if it would otherwise be enabled). """ user_dir = self.settings['user_dir'] try: @@ -1132,21 +1136,24 @@ def new_multiplex(self, cmd, term_id): session_dir = self.settings['session_dir'] session_dir = os.path.join(session_dir, self.session) log_path = None - if self.settings['session_logging']: - log_dir = os.path.join(user_dir, user) - log_dir = os.path.join(log_dir, 'logs') - # Create the log dir if not already present - if not os.path.exists(log_dir): - mkdir_p(log_dir) - log_name = datetime.now().strftime('%Y%m%d%H%M%S%f.golog') - log_path = os.path.join(log_dir, log_name) + syslog_logging = False + if logging: + syslog_logging = self.settings['syslog_session_logging'] + if self.settings['session_logging']: + log_dir = os.path.join(user_dir, user) + log_dir = os.path.join(log_dir, 'logs') + # Create the log dir if not already present + if not os.path.exists(log_dir): + mkdir_p(log_dir) + log_name = datetime.now().strftime('%Y%m%d%H%M%S%f.golog') + log_path = os.path.join(log_dir, log_name) facility = string_to_syslog_facility(self.settings['syslog_facility']) return termio.Multiplex( cmd, log_path=log_path, user=user, term_id=term_id, - syslog=self.settings['syslog_session_logging'], + syslog=syslog_logging, syslog_facility=facility, syslog_host=self.settings['syslog_host'] ) @@ -1683,7 +1690,6 @@ def get_error_html(self, status_code, **kwargs): self.require_setting("static_path") if status_code in [404, 500, 503, 403]: filename = os.path.join(self.settings['static_path'], '%d.html' % status_code) - print("filename: %s" % filename) if os.path.exists(filename): f = open(filename, 'r') data = f.read() @@ -1715,7 +1721,7 @@ def __init__(self, settings): static_path=os.path.join(GATEONE_DIR, "static"), static_url_prefix="%sstatic/" % settings['url_prefix'], gzip=True, - login_url="/auth" + login_url="%sauth" % settings['url_prefix'] ) # Make sure all the provided settings wind up in self.settings for k, v in settings.items(): @@ -1740,7 +1746,7 @@ def __init__(self, settings): docs_path = os.path.join(GATEONE_DIR, 'docs') docs_path = os.path.join(docs_path, 'build') docs_path = os.path.join(docs_path, 'html') - url_prefix = tornado_settings['url_prefix'] + url_prefix = settings['url_prefix'] # Setup our URL handlers handlers = [ (r"%s" % url_prefix, MainHandler), @@ -2225,7 +2231,11 @@ def main(): ssl_options = None https_server = tornado.httpserver.HTTPServer( Application(settings=app_settings), ssl_options=ssl_options) - https_redirect = tornado.web.Application([(r"/", HTTPSRedirectHandler),]) + https_redirect = tornado.web.Application( + [(r".*", HTTPSRedirectHandler),], + port=options.port, + url_prefix=options.url_prefix + ) tornado.web.ErrorHandler = ErrorHandler try: # Start your engines! if options.address: diff --git a/gateone/plugins/bookmarks/static/bookmarks.js b/gateone/plugins/bookmarks/static/bookmarks.js index ab8fb3a7..48b4d3f9 100644 --- a/gateone/plugins/bookmarks/static/bookmarks.js +++ b/gateone/plugins/bookmarks/static/bookmarks.js @@ -34,6 +34,7 @@ GateOne.Bookmarks.sortToggle = false; GateOne.Bookmarks.searchFilter = null; GateOne.Bookmarks.page = 0; // Used to tracking pagination GateOne.Bookmarks.dateTags = []; +GateOne.Bookmarks.URLTypeTags = []; GateOne.Bookmarks.toUpload = []; // Used for tracking what needs to be uploaded to the server GateOne.Bookmarks.loginSync = true; // Makes sure we don't display "Synchronization Complete" if the user just logged in (unless it is the first time). GateOne.Bookmarks.temp = ""; // Just a temporary holding space for things like drag & drop @@ -506,7 +507,17 @@ GateOne.Base.update(GateOne.Bookmarks, { bmTaglist.appendChild(tag); } } - if (b.tags) { + if (b.URLTypeTags) { + for (var i in b.URLTypeTags) { + var tag = u.createElement('li', {'id': 'bm_autotag bm_urltype_tag'}); + tag.onclick = function(e) { + b.removeFilterURLTypeTag(bookmarks, this.innerHTML); + }; + tag.innerHTML = b.URLTypeTags[i]; + bmTaglist.appendChild(tag); + } + } + if (b.tags.length) { for (var i in b.tags) { // Recreate the tag filter list var tag = u.createElement('li', {'id': 'bm_tag'}); tag.innerHTML = b.tags[i]; @@ -516,7 +527,7 @@ GateOne.Base.update(GateOne.Bookmarks, { bmTaglist.appendChild(tag); } } - if (b.tags) { + if (b.tags.length) { // Remove all bookmarks that don't have matching *Bookmarks.tags* bookmarks.forEach(function(bookmark) { var bookmarkTags = bookmark.tags, @@ -535,7 +546,19 @@ GateOne.Base.update(GateOne.Bookmarks, { bookmarks = filteredBookmarks; filteredBookmarks = []; // Have to reset this for use further down } - if (b.dateTags) { + if (b.URLTypeTags.length) { + // Remove all bookmarks that don't have matching URL type + bookmarks.forEach(function(bookmark) { + var urlType = bookmark.url.split(':')[0]; + if (b.URLTypeTags.indexOf(urlType) == 0) { + // Add the bookmark to the list + filteredBookmarks.push(bookmark); + } + }); + bookmarks = filteredBookmarks; + filteredBookmarks = []; // Have to reset this for use further down + } + if (b.dateTags.length) { // Remove from the bookmarks array all bookmarks that don't measure up to *Bookmarks.dateTags* bookmarks.forEach(function(bookmark) { var dateObj = new Date(parseInt(bookmark.created)), @@ -738,7 +761,7 @@ GateOne.Base.update(GateOne.Bookmarks, { xhr.send(params); } else { // Check if this is an SSH URL and use the SSH icon for it - if (bookmark.url.slice(0,3) == "ssh") { + if (u.startsWith('telnet', bookmark.url) || u.startsWith('ssh', bookmark.url)) { b.storeFavicon(bookmark, go.Icons['ssh']); } // Ignore everything else (until we add suitable favicons) @@ -867,8 +890,11 @@ GateOne.Base.update(GateOne.Bookmarks, { bmElement.appendChild(bmContent); bmDesc.innerHTML = bookmark.notes; bmContent.appendChild(bmDesc); + // TODO: Get this adding the autotags to the taglist if (!ad && b.bookmarks.length) { var bmDateTag = u.createElement('li', {'class': 'bm_autotag'}), + goTag = u.createElement('li', {'class': 'bm_autotag bm_urltype_tag'}), + urlType = bookmark.url.split(':')[0], dateTag = b.getDateTag(dateObj); bmVisited.innerHTML = bookmark.visits; bmElement.appendChild(bmVisited); @@ -881,6 +907,11 @@ GateOne.Base.update(GateOne.Bookmarks, { }; bmTaglist.appendChild(bmTag); }); + goTag.innerHTML = urlType; // The ★ gets added via CSS + goTag.onclick = function(e) { + b.addFilterURLTypeTag(b.filteredBookmarks, urlType); + } + bmTaglist.appendChild(goTag); bmDateTag.innerHTML = dateTag; bmDateTag.onclick = function(e) { b.addFilterDateTag(b.filteredBookmarks, dateTag); @@ -981,7 +1012,7 @@ GateOne.Base.update(GateOne.Bookmarks, { bmHeader = u.createElement('div', {'id': 'bm_header', 'class': 'sectrans'}), bmContainer = u.createElement('div', {'id': 'bm_container', 'class': 'sectrans'}), bmPagination = u.createElement('div', {'id': 'bm_pagination', 'class': 'sectrans'}), - bmTagCloud = u.createElement('div', {'id': 'bm_tagcloud', 'class': 'sectrans'}), +// bmTagCloud = u.createElement('div', {'id': 'bm_tagcloud', 'class': 'sectrans'}), bmTags = u.createElement('div', {'id': 'bm_tags', 'class': 'sectrans'}), bmNew = u.createElement('a', {'id': 'bm_new', 'class': 'quartersectrans'}), bmHRFix = u.createElement('hr', {'style': {'opacity': 0, 'margin-bottom': 0}}), @@ -993,11 +1024,14 @@ GateOne.Base.update(GateOne.Bookmarks, { bmSync = u.createElement('a', {'id': 'bm_sync', 'title': 'Synchronize your bookmarks with the server.'}), bmH2 = u.createElement('h2'), bmHeaderImage = u.createElement('span', {'id': 'bm_header_star'}), - bmTagCloudUL = u.createElement('ul', {'id': 'bm_tagcloud_ul'}), - bmTagCloudTip = u.createElement('span', {'id': 'bm_tagcloud_tip', 'class': 'sectrans'}), - bmTagsHeader = u.createElement('h3', {'class': 'sectrans'}), +// bmTagCloudUL = u.createElement('ul', {'id': 'bm_tagcloud_ul'}), +// bmTagCloudTip = u.createElement('span', {'id': 'bm_tagcloud_tip', 'class': 'sectrans'}), +// bmTagsHeader = u.createElement('h3', {'class': 'sectrans'}), +// pipeSeparator = u.createElement('span'), +// bmTagsHeaderTagsLink = u.createElement('a'), +// bmTagsHeaderAutotagsLink = u.createElement('a', {'class': 'inactive'}), bmSearch = u.createElement('input', {'id': 'bm_search', 'name': prefix+'search', 'type': 'search', 'tabindex': 1, 'placeholder': 'Search Bookmarks'}), - allTags = b.getTags(b.bookmarks), +// allTags = b.getTags(b.bookmarks), toggleSort = u.partial(b.toggleSortOrder, b.bookmarks); bmH2.innerHTML = 'Bookmarks'; if (!embedded) { @@ -1046,14 +1080,20 @@ GateOne.Base.update(GateOne.Bookmarks, { bmDisplayOpts.appendChild(bmSortOpts); bmHeader.appendChild(bmTags); bmHeader.appendChild(bmHRFix); // The HR here fixes an odd rendering bug with Chrome on Mac OS X - bmTagsHeader.innerHTML = 'Tags'; - go.Visual.applyTransform(bmTagsHeader, 'translate(300%, 0)'); +// bmTagsHeaderAutotagsLink.innerHTML = "Autotags"; +// pipeSeparator.innerHTML = " | "; +// bmTagsHeaderTagsLink.innerHTML = "Tags"; +// bmTagsHeader.appendChild(bmTagsHeaderTagsLink); +// bmTagsHeader.appendChild(pipeSeparator); +// bmTagsHeader.appendChild(bmTagsHeaderAutotagsLink); +// bmTagsHeader.innerHTML = 'Tags | Autotags'; +// go.Visual.applyTransform(bmTagsHeader, 'translate(300%, 0)'); go.Visual.applyTransform(bmPagination, 'translate(300%, 0)'); - bmTagCloud.appendChild(bmTagsHeader); - bmTagCloud.appendChild(bmTagCloudUL); - bmTagCloudTip.style.opacity = 0; - bmTagCloudTip.innerHTML = "
Tip: " + b.generateTip(); - bmTagCloud.appendChild(bmTagCloudTip); +// bmTagCloud.appendChild(bmTagsHeader); +// bmTagCloud.appendChild(bmTagCloudUL); +// bmTagCloudTip.style.opacity = 0; +// bmTagCloudTip.innerHTML = "
Tip: " + b.generateTip(); +// bmTagCloud.appendChild(bmTagCloudTip); if (existingPanel) { // Remove everything first while (existingPanel.childNodes.length >= 1 ) { @@ -1086,20 +1126,141 @@ GateOne.Base.update(GateOne.Bookmarks, { } } if (!embedded) { + b.loadTagCloud('tags'); setTimeout(function() { // Fade them in and load the bookmarks - go.Visual.applyTransform(bmTagsHeader, ''); +// go.Visual.applyTransform(bmTagsHeader, ''); go.Visual.applyTransform(bmPagination, ''); b.loadBookmarks(1); }, 800); // Needs to be just a bit longer than the previous setTimeout - setTimeout(function() { // This one looks nicer if it comes last - bmTagCloudTip.style.opacity = 1; - }, 3000); - setTimeout(function() { // Make it go away after a while - bmTagCloudTip.style.opacity = 0; +// setTimeout(function() { // This one looks nicer if it comes last +// bmTagCloudTip.style.opacity = 1; +// }, 3000); +// setTimeout(function() { // Make it go away after a while +// bmTagCloudTip.style.opacity = 0; +// setTimeout(function() { +// u.removeElement(bmTagCloudTip); +// }, 1000); +// }, 30000); +// allTags.forEach(function(tag) { +// var li = u.createElement('li', {'class': 'bm_tag sectrans', 'title': 'Click to filter or drop on a bookmark to tag it.', 'draggable': true}); +// li.innerHTML = tag; +// li.addEventListener('dragstart', b.handleDragStart, false); +// go.Visual.applyTransform(li, 'translateX(700px)'); +// li.onclick = function(e) { +// b.addFilterTag(b.bookmarks, tag); +// }; +// li.oncontextmenu = function(e) { +// // Bring up the context menu +// e.preventDefault(); // Prevent regular context menu +// b.tagContextMenu(li); +// } +// bmTagCloudUL.appendChild(li); +// if (tag == "Untagged") { +// li.className = 'bm_tag sectrans untagged'; +// } +// setTimeout(function unTrans() { +// go.Visual.applyTransform(li, ''); +// }, delay); +// delay += 50; +// }); +// if (existingPanel) { +// existingPanel.appendChild(bmTagCloud); +// } else { +// bmPanel.appendChild(bmTagCloud); +// } + } + }, + loadTagCloud: function(active) { + // Loads the tag cloud. If *active* is given it must be one of 'tags' or 'autotags'. It will mark the appropriate header as inactive and load the respective tags. + var go = GateOne, + u = go.Utils, + b = go.Bookmarks, + prefix = go.prefs.prefix, + delay = 1000, + existingPanel = u.getNode('#'+prefix+'panel_bookmarks'), + existingTagCloud = u.getNode('#'+prefix+'bm_tagcloud'), + existingTagCloudUL = u.getNode('#'+prefix+'bm_tagcloud_ul'), + existingTip = u.getNode('#'+prefix+'bm_tagcloud_tip'), + existingTagsLink = u.getNode('#'+prefix+'bm_tags_header_link'), + existingAutotagsLink = u.getNode('#'+prefix+'bm_autotags_header_link'), + bmTagCloud = u.createElement('div', {'id': 'bm_tagcloud', 'class': 'sectrans'}), + bmTagCloudUL = u.createElement('ul', {'id': 'bm_tagcloud_ul'}), + bmTagCloudTip = u.createElement('span', {'id': 'bm_tagcloud_tip', 'class': 'sectrans'}), + bmTagsHeader = u.createElement('h3', {'class': 'sectrans'}), + pipeSeparator = u.createElement('span'), + bmTagsHeaderTagsLink = u.createElement('a', {'id': 'bm_tags_header_link'}), + bmTagsHeaderAutotagsLink = u.createElement('a', {'id': 'bm_autotags_header_link'}), + allTags = b.getTags(b.bookmarks), + allAutotags = b.getAutotags(b.bookmarks); + bmTagsHeaderTagsLink.onclick = function(e) { + b.loadTagCloud('tags'); + } + bmTagsHeaderAutotagsLink.onclick = function(e) { + b.loadTagCloud('autotags'); + } + if (active) { + if (active == 'tags') { + if (existingAutotagsLink) { + existingTagsLink.className = ''; + existingAutotagsLink.className = 'inactive'; + } else { + bmTagsHeaderAutotagsLink.className = 'inactive'; + } + } else if (active == 'autotags') { + if (existingTagsLink) { + existingTagsLink.className = 'inactive'; + existingAutotagsLink.className = ''; + } else { + bmTagsHeaderTagsLink.className = 'inactive'; + } + } + } + if (existingTagCloudUL) { + // Send all the tags away + u.toArray(existingTagCloudUL.childNodes).forEach(function(elem) { + elem.style.opacity = 0; setTimeout(function() { - u.removeElement(bmTagCloudTip); + u.removeElement(elem); }, 1000); - }, 30000); + }); + setTimeout(function() { + u.removeElement(existingTagCloudUL); + }, 1000); + } + if (existingTip) { + existingTip.style.opacity = 0; + setTimeout(function() { + u.removeElement(existingTip); + }, 800); + } + setTimeout(function() { // This looks nicer if it comes last + bmTagCloudTip.style.opacity = 1; + }, 3000); + setTimeout(function() { // Make it go away after a while + bmTagCloudTip.style.opacity = 0; + setTimeout(function() { + u.removeElement(bmTagCloudTip); + }, 1000); + }, 30000); + go.Visual.applyTransform(bmTagsHeader, 'translate(300%, 0)'); + bmTagsHeaderAutotagsLink.innerHTML = "Autotags"; + pipeSeparator.innerHTML = " | "; + bmTagsHeaderTagsLink.innerHTML = "Tags"; + bmTagsHeader.appendChild(bmTagsHeaderTagsLink); + bmTagsHeader.appendChild(pipeSeparator); + bmTagsHeader.appendChild(bmTagsHeaderAutotagsLink); + bmTagCloudTip.style.opacity = 0; + bmTagCloudTip.innerHTML = "
Tip: " + b.generateTip(); + if (existingTagCloud) { + existingTagCloud.appendChild(bmTagCloudUL); + existingTagCloud.appendChild(bmTagCloudTip); + } else { + bmTagCloud.appendChild(bmTagsHeader); + bmTagCloud.appendChild(bmTagCloudUL); + bmTagCloud.appendChild(bmTagCloudTip); + existingPanel.appendChild(bmTagCloud); + } + if (active == 'tags') { allTags.forEach(function(tag) { var li = u.createElement('li', {'class': 'bm_tag sectrans', 'title': 'Click to filter or drop on a bookmark to tag it.', 'draggable': true}); li.innerHTML = tag; @@ -1122,12 +1283,42 @@ GateOne.Base.update(GateOne.Bookmarks, { }, delay); delay += 50; }); - if (existingPanel) { - existingPanel.appendChild(bmTagCloud); - } else { - bmPanel.appendChild(bmTagCloud); - } + } else if (active == 'autotags') { + allAutotags.forEach(function(tag) { + var li = u.createElement('li', {'title': 'Click to filter.'}); + li.innerHTML = tag; + go.Visual.applyTransform(li, 'translateX(700px)'); + if (u.startsWith('<', tag) || u.startsWith('>', tag)) { // Date tag + li.className = 'bm_autotag sectrans'; + li.onclick = function(e) { + b.addFilterDateTag(b.bookmarks, tag); + }; + setTimeout(function unTrans() { + go.Visual.applyTransform(li, ''); + setTimeout(function() { + li.className = 'bm_autotag'; + }, 1000); + }, delay); + } else { // URL type tag + li.className = 'bm_autotag bm_urltype_tag sectrans'; + li.onclick = function(e) { + b.addFilterURLTypeTag(b.bookmarks, tag); + } + setTimeout(function unTrans() { + go.Visual.applyTransform(li, ''); + setTimeout(function() { + li.className = 'bm_autotag bm_urltype_tag'; + }, 1000); + }, delay); + } + bmTagCloudUL.appendChild(li); + + delay += 50; + }); } + setTimeout(function() { + go.Visual.applyTransform(bmTagsHeader, ''); + }, 800); }, openBookmark: function(URL) { // If the current terminal is in a disconnected state, connects to *URL* in the current terminal. @@ -1143,27 +1334,28 @@ GateOne.Base.update(GateOne.Bookmarks, { b.openSearchDialog(URL, bookmark.name); return; } - if (URL.slice(0,4) == "http") { // NOTE: Includes https URLs - // This is a regular URL, open in a new window + if (u.startsWith('ssh', URL) || u.startsWith('telnet', URL)) { + // This is a URL that will be handled by Gate One. Send it to the terminal: + if (termTitle == 'Gate One') { + // Foreground terminal has yet to be connected, use it + b.incrementVisits(URL); + go.Input.queue(URL+'\n'); + go.Net.sendChars(); + } else { + b.incrementVisits(URL); + go.Terminal.newTerminal(); + setTimeout(function() { + go.Input.queue(URL+'\n'); + go.Net.sendChars(); + }, 250); + } + } else { + // This is a regular URL, open in a new window and let the browser handle it b.incrementVisits(URL); go.Visual.togglePanel('#'+prefix+'panel_bookmarks'); window.open(URL); return; // All done } - // Proceed as if this is an SSH URL... - if (termTitle == 'Gate One') { - // Foreground terminal has yet to be connected, use it - b.incrementVisits(URL); - go.Input.queue(URL+'\n'); - go.Net.sendChars(); - } else { - b.incrementVisits(URL); - go.Terminal.newTerminal(); - setTimeout(function() { - go.Input.queue(URL+'\n'); - go.Net.sendChars(); - }, 250); - } go.Visual.togglePanel('#'+prefix+'panel_bookmarks'); }, toggleSortOrder: function() { @@ -1264,6 +1456,34 @@ GateOne.Base.update(GateOne.Bookmarks, { } b.loadBookmarks(); }, + addFilterURLTypeTag: function(bookmarks, tag) { + // Adds the given dateTag to the filter list + logDebug('addFilterURLTypeTag: ' + tag); + var go = GateOne, + b = go.Bookmarks; + for (var i in b.URLTypeTags) { + if (b.URLTypeTags[i] == tag) { + // Tag already exists, ignore. + return; + } + } + b.URLTypeTags.push(tag); + // Reset the pagination since our bookmark list will change + b.page = 0; + b.loadBookmarks(); + }, + removeFilterURLTypeTag: function(bookmarks, tag) { + // Removes the given dateTag from the filter list + logDebug("removeFilterURLTypeTag: " + tag); + var go = GateOne, + b = go.Bookmarks; + for (var i in b.URLTypeTags) { + if (b.URLTypeTags[i] == tag) { + b.URLTypeTags.splice(i, 1); + } + } + b.loadBookmarks(); + }, getTags: function(/*opt*/bookmarks) { // Returns an array of all the tags in Bookmarks.bookmarks or *bookmarks* if given. // NOTE: Ordered alphabetically @@ -1287,6 +1507,31 @@ GateOne.Base.update(GateOne.Bookmarks, { tagList.sort(); return tagList; }, + getAutotags: function(/*opt*/bookmarks) { + // Returns an array of all the autotags in Bookmarks.bookmarks or *bookmarks* if given. + // NOTE: Ordered alphabetically with the URL types coming before date tags + var go = GateOne, + b = go.Bookmarks, + autoTagList = [], + dateTagList = []; + if (!bookmarks) { + bookmarks = b.bookmarks; + } + bookmarks.forEach(function(bookmark) { + var dateObj = new Date(parseInt(bookmark.created)), + dateTag = b.getDateTag(dateObj), + urlType = bookmark.url.split(':')[0]; + if (dateTagList.indexOf(dateTag) == -1) { + dateTagList.push(dateTag); + } + if (autoTagList.indexOf(urlType) == -1) { + autoTagList.push(urlType); + } + }); + autoTagList.sort(); + dateTagList.sort(); + return autoTagList.concat(dateTagList); + }, openImportDialog: function() { // Displays the form where a user can create or edit a bookmark. // If *URL* is given, pre-fill the form with the associated bookmark for editing. diff --git a/gateone/plugins/bookmarks/templates/bookmarks.css b/gateone/plugins/bookmarks/templates/bookmarks.css index 28cebf69..0c88f39f 100644 --- a/gateone/plugins/bookmarks/templates/bookmarks.css +++ b/gateone/plugins/bookmarks/templates/bookmarks.css @@ -206,7 +206,7 @@ padding-right: 0.2em; padding-top: 0.1em; list-style: none; -/* display: inline-block; */ + display: inline-block; color: #fff; background-color: #1F5470; font-size: 0.95em; @@ -265,10 +265,10 @@ #{{prefix}}bm_tagcloud h3 { font-family: 'Istok Web', Dejavu Sans, Verdana, Arial, sans-serif; } -#{{prefix}}bm_tagcloud a.inactive { +#{{container}} #{{prefix}}bm_tagcloud a.inactive { color: #777; } -#{{prefix}}bm_tagcloud a.inactive:hover { +#{{container}} #{{prefix}}bm_tagcloud a.inactive:hover { color: #1F5470; } #{{prefix}}bm_tagcloud_ul { @@ -286,6 +286,28 @@ font-size: 0.95em; cursor: move; } +#{{container}} .bm_autotag { + color: #fff; + background-color: #1F5470; + margin-left: 0.2em; + padding-left: 0.2em; + padding-right: 0.2em; + list-style: none; + display: inline-block; + font-size: 0.95em; + cursor: pointer; +} +#{{container}} .bm_autotag:hover { + color: #fff; + background-color: maroon; + cursor: pointer; +} +#{{container}} .bm_urltype_tag { + font-style: italic; +} +#{{container}} .bm_urltype_tag:before { + content: "★"; +} #{{container}} .bm_tag:hover { color: #fff; background-color: #1F5470; @@ -321,10 +343,10 @@ padding-left: 0.3em; padding-right: 0.3em; } -#{{container}} .bm_controls a:hover { +/*#{{container}} .bm_controls a:hover { color: #fff; background-color: #CC0003; -} +}*/ #{{container}} .bm_content { top: 0; left: 0; @@ -505,7 +527,7 @@ top: 0; left: 0; width: 100%; - height: 100%; + height: 2em; /* Just big enough to cover most of the bookmark element but not enough to cover the tags */ z-index: 100; } #{{container}} .linkfloat:hover { diff --git a/gateone/plugins/logging/logging_plugin.py b/gateone/plugins/logging/logging_plugin.py index e43792fe..5c45318e 100644 --- a/gateone/plugins/logging/logging_plugin.py +++ b/gateone/plugins/logging/logging_plugin.py @@ -285,7 +285,7 @@ def send_message(fd, event): PROC.start() return -def _retrieve_log_playback(queue, settings, tws=None): +def _retrieve_log_playback(queue, settings): """ Writes a JSON-encoded message to the client containing the log in a self-contained HTML format similar to:: @@ -298,7 +298,6 @@ def _retrieve_log_playback(queue, settings, tws=None): *settings['colors']* - The CSS color scheme to use when generating output. *settings['theme']* - The CSS theme to use when generating output. *settings['where']* - Whether or not the result should go into a new window or an iframe. - *tws* - TerminalWebSocket instance. The output will look like this:: @@ -413,6 +412,159 @@ def _retrieve_log_playback(queue, settings, tws=None): message = {'logging_log_playback': out_dict} queue.put(message) +def save_log_playback(settings, tws=None): + """ + Calls _save_log_playback() via a multiprocessing Process() so it doesn't + cause the IOLoop to block. + """ + settings['container'] = tws.container + settings['prefix'] = tws.prefix + settings['user'] = user = tws.get_current_user()['upn'] + settings['users_dir'] = os.path.join(tws.settings['user_dir'], user) + settings['gateone_dir'] = tws.settings['gateone_dir'] + settings['url_prefix'] = tws.settings['url_prefix'] + q = Queue() + global PROC + PROC = Process(target=_save_log_playback, args=(q, settings)) + io_loop = tornado.ioloop.IOLoop.instance() + def send_message(fd, event): + """ + Sends the log enumeration result to the client. Necessary because + IOLoop doesn't pass anything other than *fd* and *event* when it handles + file descriptor events. + """ + io_loop.remove_handler(fd) + message = q.get() + tws.write_message(message) + # This is kind of neat: multiprocessing.Queue() instances have an + # underlying fd that you can access via the _reader: + io_loop.add_handler(q._reader.fileno(), send_message, io_loop.READ) + PROC.start() + return + +def _save_log_playback(queue, settings): + """ + Writes a JSON-encoded message to the client containing the log in a + self-contained HTML format similar to:: + + ./logviewer.py log_filename + + The difference between this function and :py:meth:`_retrieve_log_playback` + is that this one instructs the client to save the file to disk instead of + opening it in a new window. + + *settings* - A dict containing the *log_filename*, *colors*, and *theme* to + use when generating the HTML output. + *settings['log_filename']* - The name of the log to display. + *settings['colors']* - The CSS color scheme to use when generating output. + *settings['theme']* - The CSS theme to use when generating output. + + The output will look like this:: + + { + 'result': "Success", + 'data': , + 'mimetype': 'text/html' + 'filename': + } + It is expected that the client will create a new window with the result of + this method. + """ + #print("Running retrieve_log_playback(%s)" % settings); + out_dict = { + 'result': "Success", + 'mimetype': 'text/html', + 'data': "", # Will be replace with the rendered template + } + # Local variables + gateone_dir = settings['gateone_dir'] + user = settings['user'] + users_dir = settings['users_dir'] + container = settings['container'] + prefix = settings['prefix'] + url_prefix = settings['url_prefix'] + log_filename = settings['log_filename'] + short_logname = log_filename.split('.golog')[0] + out_dict['filename'] = "%s.html" % short_logname + theme = "%s.css" % settings['theme'] + colors = "%s.css" % settings['colors'] + # Important paths + # NOTE: Using os.path.join() in case Gate One can actually run on Windows + # some day. + logs_dir = os.path.join(users_dir, "logs") + log_path = os.path.join(logs_dir, log_filename) + templates_path = os.path.join(gateone_dir, 'templates') + colors_path = os.path.join(templates_path, 'term_colors') + themes_path = os.path.join(templates_path, 'themes') + plugins_path = os.path.join(gateone_dir, 'plugins') + logging_plugin_path = os.path.join(plugins_path, 'logging') + template_path = os.path.join(logging_plugin_path, 'templates') + playback_template_path = os.path.join(template_path, 'playback_log.html') + # recording format: + # {"screen": [log lines], "time":"2011-12-20T18:00:01.033Z"} + # Actual method logic + if os.path.exists(log_path): + # Next we render the theme and color templates so we can pass them to + # our final template + out_dict['metadata'] = get_or_update_metadata(log_path, user) + try: + rows = out_dict['metadata']['rows'] + cols = out_dict['metadata']['cols'] + except KeyError: + # Log was created before rows/cols metadata was included via termio.py + # Use some large values to ensure nothing wraps and hope for the best: + rows = 40 + cols = 500 + with open(os.path.join(colors_path, colors)) as f: + colors_file = f.read() + colors_template = tornado.template.Template(colors_file) + rendered_colors = colors_template.generate( + container=container, + prefix=prefix, + url_prefix=url_prefix + ) + with open(os.path.join(themes_path, theme)) as f: + theme_file = f.read() + theme_template = tornado.template.Template(theme_file) + # Setup our 256-color support CSS: + colors_256 = "" + for i in xrange(256): + fg = "#%s span.fx%s {color: #%s;}" % ( + container, i, COLORS_256[i]) + bg = "#%s span.bx%s {background-color: #%s;} " % ( + container, i, COLORS_256[i]) + colors_256 += "%s %s" % (fg, bg) + colors_256 += "\n" + rendered_theme = theme_template.generate( + container=container, + prefix=prefix, + colors_256=colors_256, + url_prefix=url_prefix + ) + # NOTE: 'colors' are customizable but colors_256 is universal. That's + # why they're separate. + # Lastly we render the actual HTML template file + # NOTE: Using Loader() directly here because I was getting strange EOF + # errors trying to do it the other way :) + loader = tornado.template.Loader(template_path) + playback_template = loader.load('playback_log.html') + recording = retrieve_log_frames(log_path, rows, cols) + preview = 'false' + playback_html = playback_template.generate( + prefix=prefix, + container=container, + theme=rendered_theme, + colors=rendered_colors, + preview=preview, + recording=json_encode(recording), + url_prefix=url_prefix + ) + out_dict['data'] = playback_html + else: + out_dict['result'] = "ERROR: Log not found" + message = {'save_file': out_dict} + queue.put(message) + # Temporarily disabled while I work around the problem of gzip files not being # downloadable over the websocket. #def get_log_file(log_filename, tws): @@ -440,6 +592,6 @@ def _retrieve_log_playback(queue, settings, tws=None): 'logging_get_logs': enumerate_logs, 'logging_get_log_flat': retrieve_log_flat, 'logging_get_log_playback': retrieve_log_playback, - #'logging_get_log_file': get_log_file, + 'logging_get_log_file': save_log_playback, } } \ No newline at end of file diff --git a/gateone/plugins/logging/static/logging.js b/gateone/plugins/logging/static/logging.js index 1fda4c2d..be3dc33a 100644 --- a/gateone/plugins/logging/static/logging.js +++ b/gateone/plugins/logging/static/logging.js @@ -543,9 +543,9 @@ GateOne.Base.update(GateOne.Logging, { logMetadataDiv = u.getNode('#'+prefix+'log_metadata'), downloadButton = u.createElement('button', {'id': 'log_download', 'type': 'submit', 'value': 'Submit', 'class': 'button black'}), logObj = null; - downloadButton.innerHTML = "Download"; + downloadButton.innerHTML = "Download (HTML)"; downloadButton.onclick = function(e) { - go.ws.send(JSON.stringify({'logging_get_log_file': logFile})); + l.saveRenderedLog(logFile); } // Retreive the metadata on the log in question for (var i in l.serverLogs) { @@ -572,8 +572,7 @@ GateOne.Base.update(GateOne.Logging, { while (logMetadataDiv.childNodes.length >= 1 ) { logMetadataDiv.removeChild(logMetadataDiv.firstChild); } - // Downloads of log files temporarily disabled while I work out some kinks in the code for downloading binary files -// logMetadataDiv.appendChild(downloadButton); + logMetadataDiv.appendChild(downloadButton); for (var i in metadataNames) { var row = u.createElement('div', {'class': 'metadata_row'}), title = u.createElement('div', {'class':'metadata_title'}), @@ -799,6 +798,17 @@ GateOne.Base.update(GateOne.Logging, { } go.ws.send(JSON.stringify({'logging_get_log_playback': message})); }, + saveRenderedLog: function(logFile) { + // Tells the server to open *logFile*, rendere it as a self-contained recording, and send it back to the browser for saving (using the save_file action). + var go = GateOne, + message = { + 'log_filename': logFile, + 'theme': go.prefs.theme, + 'colors': go.prefs.colors + }; + go.ws.send(JSON.stringify({'logging_get_log_file': message})); + go.Visual.displayMessage(logFile + ' will be downloaded when rendering is complete. Large logs can take some time so please be patient.'); + }, sortFunctions: { date: function(a,b) { // Sorts by date (start_date) followed by alphabetical order of the title (connect_string) diff --git a/gateone/plugins/logging/templates/logging.css b/gateone/plugins/logging/templates/logging.css new file mode 100644 index 00000000..377e3782 --- /dev/null +++ b/gateone/plugins/logging/templates/logging.css @@ -0,0 +1,132 @@ +#{{container}} .logitem_title { + width: 15em; +} +#{{prefix}}panel_logs { + width: 90%; + height: 85%; /* A bit off the bottom so you can see if something scrolls */ +} +#{{prefix}}logview_container { + position: absolute; + top: 2.5em; + bottom: 0; + padding-left: .2em; + margin-left: .2em; + left: 0; + right: 0; +} +#{{container}} .logview_options { + display: table-cell; + text-transform: uppercase; + background-image: none; + padding-left: 0.5em; +} +#{{container}} .logitem_header { + display: table-header-group; + font-weight: bold; + font-size: 1.1em; + color: #1F5470; +} +#{{container}} .logitem_header_cell { + color: #1F5470; + background-image: none; + background: none; +} +#{{container}} .logitem_header_cell.active { + color: maroon; +} +#{{container}} .logitem_header_cell:hover { + color: maroon; + cursor: pointer; + background-color: #fff; +} +#{{prefix}}log_metadata { + width: 100%; + display: table; +} +#{{prefix}}log_info { + float: right; + width: 47%; + margin-right: 0.5em; + margin-top: 1.5em; +} +#{{prefix}}log_preview { + width: 100%; + height: 50%; + border: 1px #000 solid; +/* background-color: white; */ +/* opacity: 0.3; */ +} +/* Effects */ +#{{container}} div[name="{{prefix}}logitem"]:hover { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.2); + -webkit-transform-origin: center center; + -webkit-transition: all 0.05s ease-in-out; + -moz-transition: all 0.05s ease-in-out; + -moz-transform-origin: center center; + -o-transition-property: all; + -o-transition-duration: 0.05s; + -o-transition-timing-function: ease-in-out; + -o-transform-origin: center center; + -khtml-transform-origin: center center; + -khtml-transition: all 0.05s ease-in-out; + -ms-transform-origin: center center; + -ms-transition: all 0.05s ease-in-out; + transform-origin: top left; + transition: all 0.05s ease-in-out; +} +#{{prefix}}log_pagination { + width: 100%; + float: right; + display: block; + font-size: 0.8em; + clear: right; + margin-top: -1.2em; + padding-bottom: .5em; +} +#{{prefix}}log_pagination ul { + border: 0; + margin: 0; + padding: 0; +} +#{{prefix}}log_pagination li { + border: 0; + margin:0; + padding:0; + font-size: 0.8em; + list-style: none; + margin-right: 0.1em; +} +#{{prefix}}log_pagination a { + border: solid 1px #9aafe5; + margin-right: 0.1em; +} +#{{prefix}}log_pagination .previous-off, +#{{prefix}}log_pagination .next-off { + border: solid 1px #DEDEDE; + color: #888888; + display: block; + float: left; + font-weight: bold; + margin-right: 0.1em; + padding: 0 0.1em; +} +#{{prefix}}log_pagination .next a, +#{{prefix}}log_pagination .previous a { + font-weight:bold; +} +#{{prefix}}log_pagination a:link, +#{{prefix}}log_pagination a:visited { + float: left; +} +#{{prefix}}log_pagination a:hover{ + border: solid 1px #1F5470; +} +#{{container}} #{{prefix}}log_pagination .active { + background: #1F5470; + color: #FFF; +} +#{{container}} #{{prefix}}log_pagination .inactive a { + border: solid 1px #DEDEDE; + color: #888; +} \ No newline at end of file diff --git a/gateone/plugins/logging/templates/playback_log.html b/gateone/plugins/logging/templates/playback_log.html new file mode 100644 index 00000000..22be8e11 --- /dev/null +++ b/gateone/plugins/logging/templates/playback_log.html @@ -0,0 +1,336 @@ + + + + + + + + + Gate One - Session Playback + + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/gateone/plugins/playback/playback.py b/gateone/plugins/playback/playback.py index 3e8a1970..2eeb45fa 100644 --- a/gateone/plugins/playback/playback.py +++ b/gateone/plugins/playback/playback.py @@ -17,6 +17,7 @@ # Python stdlib import os import logging +from datetime import datetime # Our stuff from gateone import BaseHandler, COLORS_256 @@ -29,6 +30,10 @@ import tornado.template from tornado.escape import json_decode +# Globals +plugin_path = os.path.split(__file__)[0] + +# TODO: Change this to use the WebSocket and the new saveAs() function class RecordingHandler(BaseHandler): """ Handles uploads of session recordings and returns them to the client in a @@ -87,6 +92,79 @@ def post(self): url_prefix=self.settings['url_prefix'] ) +def save_recording(settings, tws): + """ + Handles uploads of session recordings and returns them to the client in a + self-contained HTML file that will auto-start playback. + + NOTE: The real crux of the code that handles this is in the template. + """ + now = datetime.now().strftime('%Y%m%d%H%m%S') # e.g. '20120208200222' + out_dict = { + 'result': 'Success', + 'filename': 'GateOne_recording-%s.html' % now, + 'data': None, + 'mimetype': 'text/html' + } + recording = settings["recording"] + container = settings["container"] + prefix = settings["prefix"] + theme = settings["theme"] + colors = settings["colors"] + gateone_dir = tws.settings['gateone_dir'] + plugins_path = os.path.join(gateone_dir, 'plugins') + #playback_plugin_path = os.path.join(plugins_path, 'playback') + template_path = os.path.join(gateone_dir, 'templates') + colors_templates_path = os.path.join(template_path, 'term_colors') + colors_css_path = os.path.join(colors_templates_path, '%s.css' % colors) + with open(colors_css_path) as f: + colors_file = f.read() + themes_templates_path = os.path.join(template_path, 'themes') + theme_css_path = os.path.join(themes_templates_path, '%s.css' % theme) + with open(theme_css_path) as f: + theme_file = f.read() + colors = tornado.template.Template(colors_file) + rendered_colors = colors.generate(container=container, prefix=prefix) + theme = tornado.template.Template(theme_file) + # Setup our 256-color support CSS: + colors_256 = "" + for i in xrange(256): + fg = "#%s span.fx%s {color: #%s;}" % ( + container, i, COLORS_256[i]) + bg = "#%s span.bx%s {background-color: #%s;} " % ( + container, i, COLORS_256[i]) + colors_256 += "%s %s" % (fg, bg) + colors_256 += "\n" + rendered_theme = theme.generate( + container=container, + prefix=prefix, + colors_256=colors_256, + url_prefix=tws.settings['url_prefix'] + ) + templates_path = os.path.join(plugin_path, "templates") + recording_template_path = os.path.join( + templates_path, "self_contained_recording.html") + with open(recording_template_path) as f: + recording_template_data = f.read() + with open('/tmp/recording.json', 'w') as f: + f.write(recording) + logging.info("WTF...") + recording_template = tornado.template.Template(recording_template_data) + rendered_recording = recording_template.generate( + recording=recording, + container=container, + prefix=prefix, + theme=rendered_theme, + colors=rendered_colors + ) + print(rendered_recording) + out_dict['data'] = rendered_recording + message = {'save_file': out_dict} + tws.write_message(message) + hooks = { - 'Web': [(r"/recording", RecordingHandler)] + 'Web': [(r"/recording", RecordingHandler)], + 'WebSocket': { + 'playback_save_recording': save_recording, + } } \ No newline at end of file diff --git a/gateone/plugins/playback/static/playback.js b/gateone/plugins/playback/static/playback.js index d8b1c565..f037544c 100644 --- a/gateone/plugins/playback/static/playback.js +++ b/gateone/plugins/playback/static/playback.js @@ -70,20 +70,20 @@ GateOne.Base.update(GateOne.Playback, { progressBarElement = null, term = termNum, playbackFrames = null, - frame = {'screen': GateOne.terminals[term]['screen'].slice(0), 'time': new Date()}; + frame = {'screen': GateOne.terminals[term]['screen'], 'time': new Date()}; if (!progressBarElement) { progressBarElement = GateOne.Utils.getNode('#'+prefix+'progressBar'); } if (!GateOne.terminals[term]['playbackFrames']) { GateOne.terminals[term]['playbackFrames'] = []; } - playbackFrames = GateOne.terminals[term]['playbackFrames']; + playbackFrames = GateOne.terminals[term]['playbackFrames'].slice(0); // Add the new playback frame to the terminal object playbackFrames.push(frame); frame = null; // Clean up if (playbackFrames.length > GateOne.prefs.playbackFrames) { // Reduce it to fit within the user's configured max -// GateOne.terminals[term]['playbackFrames'].shift(); +// playbackFrames.shift(); // NOTE: This won't work if the user reduced their playbackFrames preference by more than 1 playbackFrames.reverse(); // Have to reverse it before we truncate playbackFrames.length = GateOne.prefs.playbackFrames; // Love that length is assignable! playbackFrames.reverse(); // Put it back in the right order @@ -101,6 +101,7 @@ GateOne.Base.update(GateOne.Playback, { GateOne.Utils.showElement('#'+prefix+'pastearea'); } } + playbackFrames = null; // Immediate cleanup }, savePrefsCallback: function() { // Called when the user clicks the "Save" button in the prefs panel @@ -343,39 +344,48 @@ GateOne.Base.update(GateOne.Playback, { } goDiv.addEventListener(mousewheelevt, wheelFunc, true); }, +// saveRecording: function(term) { +// // Saves the session playback recording +// var go = GateOne, +// u = go.Utils, +// recording = JSON.stringify(go.terminals[term]['playbackFrames']), +// // This creates a form to POST our saved session to /recording on the server +// // NOTE: The server just returns the same data wrapped in a easy-to-use template +// form = u.createElement('form', { +// 'method': 'post', +// 'action': go.prefs.url + 'recording?r=' + new Date().getTime(), +// 'target': '_blank' +// }), +// recordingField = u.createElement('textarea', {'name': 'recording'}), +// themeField = u.createElement('input', {'name': 'theme'}), +// colorsField = u.createElement('input', {'name': 'colors'}), +// containerField = u.createElement('input', {'name': 'container'}), +// prefixField = u.createElement('input', {'name': 'prefix'}); +// recordingField.value = recording; +// form.appendChild(recordingField); +// themeField.value = go.prefs.theme; +// form.appendChild(themeField); +// colorsField.value = go.prefs.colors; +// form.appendChild(colorsField); +// containerField.value = go.prefs.goDiv.split('#')[1]; +// form.appendChild(containerField); +// prefixField.value = go.prefs.prefix; +// form.appendChild(prefixField); +// document.body.appendChild(form); +// form.submit(); +// setTimeout(function() { +// // No reason to keep this around +// document.body.removeChild(form); +// }, 1000); +// }, saveRecording: function(term) { - // Saves the session playback recording + // Saves the session playback recording by sending the playbackFrames to the server to have them rendered. + // When the server is done rendering the recording it will be sent back to the client via the save_file action. var go = GateOne, u = go.Utils, recording = JSON.stringify(go.terminals[term]['playbackFrames']), - // This creates a form to POST our saved session to /recording on the server - // NOTE: The server just returns the same data wrapped in a easy-to-use template - form = u.createElement('form', { - 'method': 'post', - 'action': go.prefs.url + 'recording?r=' + new Date().getTime(), - 'target': '_blank' - }), - recordingField = u.createElement('textarea', {'name': 'recording'}), - themeField = u.createElement('input', {'name': 'theme'}), - colorsField = u.createElement('input', {'name': 'colors'}), - containerField = u.createElement('input', {'name': 'container'}), - prefixField = u.createElement('input', {'name': 'prefix'}); - recordingField.value = recording; - form.appendChild(recordingField); - themeField.value = go.prefs.theme; - form.appendChild(themeField); - colorsField.value = go.prefs.colors; - form.appendChild(colorsField); - containerField.value = go.prefs.goDiv.split('#')[1]; - form.appendChild(containerField); - prefixField.value = go.prefs.prefix; - form.appendChild(prefixField); - document.body.appendChild(form); - form.submit(); - setTimeout(function() { - // No reason to keep this around - document.body.removeChild(form); - }, 1000); + settings = {'recording': recording, 'prefix': go.prefs.prefix, 'container': go.prefs.goDiv.split('#')[1], 'theme': go.prefs.theme, 'colors': go.prefs.colors}; + go.ws.send(JSON.stringify({'playback_save_recording': settings})); } }); diff --git a/gateone/plugins/playback/templates/self_contained_recording.html b/gateone/plugins/playback/templates/self_contained_recording.html index 5cb8d9f5..4c95ce5a 100644 --- a/gateone/plugins/playback/templates/self_contained_recording.html +++ b/gateone/plugins/playback/templates/self_contained_recording.html @@ -31,7 +31,6 @@ } {{theme}} {{colors}} -{% include "playback.css" %} /* Need a few custom overrides of the defaults for session playback since it isn't being controlled by gateone.js */ .terminal pre { height: 100%; diff --git a/gateone/plugins/ssh/scripts/ssh_connect.py b/gateone/plugins/ssh/scripts/ssh_connect.py index f4cbfc65..1248586e 100755 --- a/gateone/plugins/ssh/scripts/ssh_connect.py +++ b/gateone/plugins/ssh/scripts/ssh_connect.py @@ -291,7 +291,6 @@ def openssh_connect( ssh_session = 'ssh:%s:%s@%s:%s' % (term, user, host, port) script_path = os.path.join( os.environ['GO_SESSION_DIR'], ssh_session) - #script_path = os.environ['GO_SESSION_DIR'] + '/%s' % ssh_session if not script_path: # Just use a generic temp file temp = tempfile.NamedTemporaryFile(prefix="ssh_connect", delete=False) @@ -324,9 +323,96 @@ def openssh_connect( os.execvpe(script_path, [], env) os._exit(0) +def telnet_connect(user, host, port=22, env=None): + """ + Starts an interactive Telnet session to the given host as the given user on + the given port. *user* may be None, False, or an empty string. + + If *env* (dict) is given it will be set before excuting the telnet command. + + .. note:: Some telnet servers don't support sending the username in the connection. In these cases it will simply ask for it after the connection is established. + """ + signal.signal(signal.SIGCHLD, signal.SIG_IGN) # No zombies + if not env: + env = { + 'TERM': 'xterm', + 'LANG': 'en_US.UTF-8', + } + # Get the default rows/cols right from the start + try: + env['LINES'] = os.environ['LINES'] + env['COLUMNS'] = os.environ['COLUMNS'] + except KeyError: + pass # These variables aren't set + if 'PATH' in env: + command = which("telnet", path=env['PATH']) + else: + env['PATH'] = os.environ['PATH'] + command = which("telnet") + args = [host, port] + if user: + args.insert(0, user) + args.insert(0, "-l") + args.insert(0, command) # Command has to go first + script_path = None + if 'GO_TERM' in os.environ.keys(): + if 'GO_SESSION_DIR' in os.environ.keys(): + # Save a file indicating our session is attached to GO_TERM + term = os.environ['GO_TERM'] + telnet_session = 'telnet:%s:%s@%s:%s' % (term, user, host, port) + script_path = os.path.join( + os.environ['GO_SESSION_DIR'], telnet_session) + if not script_path: + # Just use a generic temp file + temp = tempfile.NamedTemporaryFile(prefix="ssh_connect", delete=False) + script_path = "%s" % temp.name + temp.close() # Will be written to below + # Create our little shell script to wrap the SSH command + script = wrapper_script.format( + socket="NO SOCKET", + cmd=" ".join(args), + temp=script_path) + with open(script_path, 'w') as f: + f.write(script) # Save it to disk + # NOTE: We wrap in a shell script so we can execute it and immediately quit. + # By doing this instead of keeping ssh_connect.py running we can save a lot + # of memory (depending on how many terminals are open). + os.chmod(script_path, 0700) # 0700 for good security practices + os.execvpe(script_path, [], env) + os._exit(0) + +def parse_telent_url(url): + """ + Parses a telnet URL like, 'telnet://user@host:23' and returns a tuple of:: + + (user, host, port) + """ + user = None # Default + if '@' in url: # user@host[:port] + host = url.split('@')[1].split(':')[0] + user = url.split('@')[0][9:] + if ':' in user: # Password was included (not secure but it could be useful) + password = user.split(':')[1] + user = user.split(':')[0] + if len(url.split('@')[1].split(':')) == 1: # No port given, assume 22 + port = '23' + else: + port = url.split('@')[1].split(':')[1] + port = port.split('/')[0] # In case there's a query string + else: # Just host[:port] (assume $GO_USER) + url = url[9:] # Remove the protocol + host = url.split(':')[0] + if len(url.split(':')) == 2: # There's a port # + port = url.split(':')[1] + port = port.split('/')[0] # In case there's a query string + else: + port = '23' + return (user, host, port) + def parse_ssh_url(url): """ - Parses an ssh URL like, 'ssh://user@host:22' and returns a tuple of: + Parses an ssh URL like, 'ssh://user@host:22' and returns a tuple of:: + (user, host, port, password, identities) If an ssh URL is given without a username, os.environ['USER'] will be used. @@ -350,8 +436,11 @@ def parse_ssh_url(url): else: port = url.split('@')[1].split(':')[1] port = port.split('/')[0] # In case there's a query string - else: # Just host[:port] (assume $USER) - user = os.environ['USER'] + else: # Just host[:port] (assume $GO_USER) + try: + user = os.environ['GO_USER'] + except KeyError: # Fall back to $USER + user = os.environ['USER'] url = url[6:] # Remove the protocol host = url.split(':')[0] if len(url.split(':')) == 2: # There's a port # @@ -457,7 +546,14 @@ def parse_ssh_url(url): "[Press Shift-F1 for help]\n\nHost/IP or SSH URL [localhost]: ")) if url.startswith('ssh://'): # This is an SSH URL (user, host, port, password, identities) = parse_ssh_url(url) + protocol = 'ssh' + elif url.startswith('telnet://'): # This is a telnet URL + (user, host, port) = parse_telent_url(url) + protocol = 'telnet' else: + protocol = raw_input("Protocol [ssh]: ") + if not protocol: + protocol = 'ssh' port = raw_input("Port [22]: ") if not port: port = '22' @@ -465,20 +561,33 @@ def parse_ssh_url(url): host = url if not url: host = 'localhost' - print(_('Connecting to: ssh://%s@%s:%s' % (user, host, port))) - # Set title (might be redundant but doesn't hurt) - print("\x1b]0;%s@%s\007" % (user, host)) - # Special escape handler: - print("\x1b]_;ssh|%s@%s:%s\007" % (user, host, port)) - openssh_connect(user, host, port, - command=options.command, - password=password, - sshfp=options.sshfp, - randomart=options.randomart, - identities=identities, - additional_args=options.additional_args, - socket=options.socket - ) + if protocol == 'ssh': + print(_('Connecting to ssh://%s@%s:%s' % (user, host, port))) + # Set title + print("\x1b]0;ssh://%s@%s\007" % (user, host)) + # Special escape handler: + print("\x1b]_;ssh|%s@%s:%s\007" % (user, host, port)) + openssh_connect(user, host, port, + command=options.command, + password=password, + sshfp=options.sshfp, + randomart=options.randomart, + identities=identities, + additional_args=options.additional_args, + socket=options.socket + ) + elif protocol == 'telnet': + if user: + print(_('Connecting to telnet://%s@%s:%s' % (user, host, port))) + # Set title + print("\x1b]0;telnet://%s@%s\007" % (user, host)) + else: + print(_('Connecting to telnet://%s:%s' % (host, port))) + # Set title + print("\x1b]0;telnet://%s\007" % host) + telnet_connect(user, host, port) + + except Exception as e: # Catch all print e noop = raw_input(_("[Press Enter to close this terminal]")) diff --git a/gateone/plugins/ssh/ssh.py b/gateone/plugins/ssh/ssh.py index d0e23e3f..450bcf92 100644 --- a/gateone/plugins/ssh/ssh.py +++ b/gateone/plugins/ssh/ssh.py @@ -435,7 +435,8 @@ def get_host_fingerprint(settings, tws): ssh = which('ssh') m = tws.new_multiplex( '%s -p %s -oUserKnownHostsFile=none -F. %s' % (ssh, port, host), - 'get_host_key') + 'get_host_key', + logging=False) # Logging is false so we don't make tons of silly logs def grab_fingerprint(m_instance, match): out_dict['fingerprint'] = match.split()[-1][:-1] m_instance.terminate() diff --git a/gateone/plugins/ssh/templates/ssh.css b/gateone/plugins/ssh/templates/ssh.css new file mode 100644 index 00000000..4d3580df --- /dev/null +++ b/gateone/plugins/ssh/templates/ssh.css @@ -0,0 +1,193 @@ +#{{prefix}}ssh_ids_listcontainer { + margin-left: 0.5em; +} +#{{container}} .ssh_id { + display: table-row; + height: 1.2em; + overflow: hidden; + font-size: 0.9em; + white-space: nowrap; +} +#{{container}} .active.ssh_id { + -webkit-transform: skew(-10deg) translate(0.5%); +} +#{{container}} .ssh_id_title { + width: 15em; +} +#{{container}} .ssh_id_metadata_value { + display: table-cell; + padding-right: 0.5em; + font-family: monospace; + white-space: pre-wrap; +} +#{{container}} .ssh_id_pubkey_value { + width: 100%; + height: 4em; +} +#{{container}} .ssh_id_metadata_title { + display: table-cell; + font-weight: bold; + padding-right: 0.5em; + text-align: right; + width: 24%; + font-size: 0.9em; +} +#{{container}} .ssh_id_default { + text-align: center; + padding-left: 1.2em; +} +#{{prefix}}ssh_id_delete { + margin-bottom: 0.2em; +} +#{{prefix}}panel_ssh_ids { + width: 90%; + height: 85%; /* A bit off the bottom so you can see if something scrolls */ +} +#{{prefix}}ssh_ids_container { + position: absolute; + top: 3.9em; + bottom: 0; + padding-left: .2em; + margin-left: .5em; + left: 0; + right: 0; +} +#{{container}} .ssh_ids_options { + display: table-cell; + text-transform: uppercase; + background-image: none; + padding-left: 0.5em; +} +#{{prefix}}ssh_id_metadata { + width: 100%; + display: table; +} +#{{prefix}}ssh_id_info { + float: right; + width: 52%; + margin-right: 0.5em; + margin-top: -1.5em; +} +#{{container}} .ssh_id_form input { + width: 99%; +} +#{{container}} .ssh_id_form select { + font-size: 0.9em; + display: inline-block; + padding: 0.2em 0.2em; + border-radius: 2px; + position: relative; + margin-right: 1em; + margin-bottom: 0.4em; + margin-left: 0.4em; +} +/* Effects */ +#{{container}} div[name="{{prefix}}ssh_id"]:hover { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.2); + -webkit-transform-origin: center center; + -webkit-transition: background-color 0.05s ease-in-out; + -moz-transition: background-color 0.05s ease-in-out; + -moz-transform-origin: center center; + -o-transition-property: background-color; + -o-transition-duration: 0.05s; + -o-transition-timing-function: ease-in-out; + -o-transform-origin: center center; + -khtml-transform-origin: center center; + -khtml-transition: background-color 0.05s ease-in-out; + -ms-transform-origin: center center; + -ms-transition: background-color 0.05s ease-in-out; + transform-origin: top left; + transition: background-color 0.05s ease-in-out; +} +#{{prefix}}ssh_ids_sort_direction { + float: right; + margin-left: 0.5em; + -webkit-transition: -webkit-transform 0.5s ease-in-out; + -moz-transition: -webkit-transform 0.5s ease-in-out; + -o-transition-property: all; + -o-transition-duration: 0.5s; + -o-transition-timing-function: ease-in-out; + -khtml-transition: -webkit-transform 0.5s ease-in-out; + -ms-transition: -webkit-transform 0.5s ease-in-out; + transition: -webkit-transform 0.5s ease-in-out; + cursor: pointer; +} +#{{prefix}}ssh_ids_sort_direction:hover { +/* color: #fff; */ +} +#{{prefix}}ssh_ids_pagination { + width: 100%; + float: right; + display: block; + font-size: 0.8em; + clear: right; + margin-top: -1.2em; + padding-bottom: .5em; +} +#{{prefix}}ssh_ids_pagination ul { + border: 0; + margin: 0; + padding: 0; +} +#{{prefix}}ssh_ids_pagination li { + border: 0; + margin:0; + padding:0; + font-size: 0.8em; + list-style: none; + margin-right: 0.1em; +} +#{{prefix}}ssh_ids_pagination a { + border: solid 1px #9aafe5; + margin-right: 0.1em; +} +#{{prefix}}ssh_ids_pagination .previous-off, +#{{prefix}}ssh_ids_pagination .next-off { + border: solid 1px #DEDEDE; +/* color: #888888; */ + display: block; + float: left; + font-weight: bold; + margin-right: 0.1em; + padding: 0 0.1em; +} +#{{prefix}}ssh_ids_pagination .next a, +#{{prefix}}ssh_ids_pagination .previous a { + font-weight:bold; +} +#{{prefix}}ssh_ids_pagination a:link, +#{{prefix}}ssh_ids_pagination a:visited { + float: left; +} +#{{prefix}}ssh_ids_pagination a:hover{ + border: solid 1px #1F5470; +} +#{{container}} #{{prefix}}ssh_ids_pagination .active { + background: #1F5470; + color: #FFF; +} +#{{container}} #{{prefix}}ssh_ids_pagination .inactive a { +/* border: solid 1px #DEDEDE; */ +/* color: #888; */ +} +#{{container}} .ssh_panel_link { + float: left; +/* color: #1F5470; */ + font-size: 0.9em; + font-weight: bold; + margin-right: 0.5em; +} +#{{container}} .ssh_panel_link:hover { +/* color: #fff; */ + cursor: pointer; +/* background-color: #1F5470; */ + -webkit-transition: -webkit-transform 0.1s ease-in-out; + -moz-transition: -webkit-transform 0.1s ease-in-out; + -o-transition-property: all; + -o-transition-duration: 0.1s; + -o-transition-timing-function: ease-in-out; + -khtml-transition: -webkit-transform 0.1s ease-in-out; + -ms-transition: -webkit-transform 0.1s ease-in-out; + transition: -webkit-transform 0.1s ease-in-out; +} \ No newline at end of file diff --git a/gateone/static/combined_plugins.js b/gateone/static/combined_plugins.js index dc2c5bb5..467edab2 100644 --- a/gateone/static/combined_plugins.js +++ b/gateone/static/combined_plugins.js @@ -1 +1 @@ -// This file will be automatically generated by Gate One. \ No newline at end of file +// This forces the file to be recreated \ No newline at end of file diff --git a/gateone/static/gateone.js b/gateone/static/gateone.js index 3961adbd..07949237 100644 --- a/gateone/static/gateone.js +++ b/gateone/static/gateone.js @@ -23,7 +23,7 @@ this file. // General TODOs // TODO: Separate creation of the various panels into their own little functions so we can efficiently neglect to execute them if in embedded mode. -// TODO: Add a nice tooltip function to GateOne.Visual that all plugins can use that is integrated into the base themes. +// TODO: Add a nice tooltip function to GateOne.Visual that all plugins can use that is integrated with the base themes. // Everything goes in GateOne (function(window, undefined) { @@ -1216,21 +1216,24 @@ GateOne.Base.update(GateOne.Net, { }, onMessage: function (evt) { logDebug('message: ' + evt.data); - var messageObj = null; + var v = GateOne.Visual, + n = GateOne.Net, + u = GateOne.Utils, + messageObj = null; try { messageObj = JSON.parse(evt.data); } catch (e) { // Non-JSON messages coming over the WebSocket are assumed to be errors, display them as-is (could be handy shortcut to display a message instead of using the 'notice' action). - GateOne.Visual.displayMessage('Message From Server: ' + evt.data); + v.displayMessage('Message From Server: ' + evt.data); } // Execute each respective action try { - GateOne.Utils.items(messageObj).forEach(function(item) { + u.items(messageObj).forEach(function(item) { var key = item[0], val = item[1]; try { - if (GateOne.Net.actions[key]) { - GateOne.Net.actions[key](val); + if (n.actions[key]) { + n.actions[key](val); } } finally { key = null; diff --git a/gateone/templates/themes/dark-black.css b/gateone/templates/themes/dark-black.css index f1aa3670..82790b51 100644 --- a/gateone/templates/themes/dark-black.css +++ b/gateone/templates/themes/dark-black.css @@ -459,7 +459,6 @@ hr { padding-bottom: 0.3em; } #{{container}} .paneltablerow select, input { -/* margin-right: 1em; */ background-color: #000; color: #fff; } diff --git a/gateone/termio.py b/gateone/termio.py index 0974acab..3b395f06 100644 --- a/gateone/termio.py +++ b/gateone/termio.py @@ -1160,7 +1160,7 @@ def terminate(self): pid = os.fork() if pid == 0: # We're inside the sub-child process # So we don't have to wait to restart Gate One: - self.io_loop.stop() # Closes the listening TCP/IP port + #self.io_loop.stop() # Closes the listening TCP/IP port # Have to wait just a moment for the main thread to finish writing to the log: time.sleep(5) # 5 seconds should be plenty of time try: diff --git a/gateone/utils.py b/gateone/utils.py index a2898f9d..1abcfcdb 100644 --- a/gateone/utils.py +++ b/gateone/utils.py @@ -839,6 +839,7 @@ def get_or_update_metadata(golog_path, user): .. note:: All logs will need "fixing" the first time they're enumerated like this since they won't have an end_date. Fortunately we only need to do this once per golog. """ + logging.debug('get_or_update_metadata()') first_frame = retrieve_first_frame(golog_path) metadata = {} if first_frame[14:].startswith('{'): diff --git a/setup.py b/setup.py index 60e3afca..67ced665 100755 --- a/setup.py +++ b/setup.py @@ -15,6 +15,9 @@ docs_dir = os.path.join('gateone', 'docs') tests_dir = os.path.join('gateone', 'tests') i18n_dir = os.path.join('gateone', 'i18n') +combined_js = os.path.join(static_dir, 'combined_plugins.js') +with open(combined_js, 'w') as f: + f.write('// This forces the file to be recreated') if POSIX: prefix = '/opt'