Permalink
Browse files

GREAT NEW CAPABILITY IN THIS COMMIT: I've added asynchronous, non-blo…

…cking pexpect-like functionality to termio! As far as I know this is the first non-blocking expect-like module in Python (I've never seen asynchronous expect, have you?). The new functions are Multiplex.expect(), Multiplex.unexpect(), and Multiplex.await() (which lets you do blocking if you want) with supporting functions, Multiplex.preprocess(), Multiplex.postprocess(), and Multiplex.timeout_check(). The documentation for these functions inside of termio.py is extensive. It doesn't work anything like pexpect does (it supports branching, errorbacks, optional patterns, and "sticky" patterns) so be sure to check out the examples. Also, the SSH plugin has *loads* of code using this new capability if you need real-world usage scenarios.

GENERAL:  The bundled docs have been re-generated.
gateone.py:  When logging is set to "debug" a warning will now be displayed/logged that this will record the keystrokes of all users.
gateone.py:  Changed the default location of known_hosts in the default command to be <user's directory>/ssh/known_hosts
gateone.py:  Added a new kind of hook: 'Auth'.  Plugins can use to register functions to be called immediately after a user authenticates.  Whatever function is registered will be called like so from inside of TerminalWebSocket:  auth_hook(self.get_current_user(), self.settings).
gateone.py:  'Web' plugin hooks can now be a list of tuples *or* just a single tuple (it's a convenience so plugins don't have to wrap a single handler in a list).
gateone.py:  Changed the use of Multiplex.dumplines() to be Multiplex.dump_html() (see note below about the new names of things).
gateone.py:  Switched from using platform.uname() to using os.uname().  Why?  platform.uname() opens a subprocess which gets mucked up by Gate One's signal handling and can result in defunct processes.  It also causes the autoreload feature to fail (only used in debug mode).
gateone.py:  Added a new function to TerminalWebSocket:  new_multiplex().  It just creates a new instance of Multiplex using the current globally-applied and user-specific settings (a convenience).
gateone.py and utils.py:  dtach sockets are now using an underscore instead of a colon.  Why?  Because for some reason they weren't showing up in the output of lsof (bug in lsof?) which made it impossible to determine which socket belonged to which process.
gateone.js:  Added two new generic file saving functions to GateOne.Utils: saveAs(blob, filename) and saveAsAction(<message from server>).  The former lets you save a blob as if the user clicked on a URL to download a file.  The latter is registered as a protocol action, 'save_file'.  Look at the source to see how it works but it essentially allows server-side functions to send data over the WebSocket and have the browser save that data as if it were a regular AJAX file download (think, 'octet-stream' :D).
logviewer.py and utils.py:  Moved get_or_update_metadata() and retrieve_first_frame() from logviewer.py to utils.py.  I had to do this to work around a circular import problem that would happen when trying to build the documentation (not the greatest reason, I know but it works).  termio.py and the logging plugin had their imports adjusted to reflect this.
termio.py:  Loads of code cleanup/name maintenance...  The naming of functions inside of termio was inconsistent and not very pythonic.  Here's a list of what became what:
    * Multiplex.create() -> Multiplex.spawn()
    * Multiplex.proc_write() --> Multiplex.write()
    * Multiplex._buffer_write() --> Multiplex._write()
    * Multiplex._buffer_to_term() --> Multiplex._read()
    * Multiplex.proc_read() --> Multiplex._ioloop_read_handler()
    * Multiplex.dumplines() --> Multiplex.dump_html()
    * Multiplex.proc_kill() --> Multiplex.terminate()
termio.py:  Added a new function, Multiplex._call_callback():  If the IOLoop is started, will add the callback to the IOLoop.add_callback() so that it gets called in the safest way possible (e.g. so it won't get mucked up by threads).  Otherwise the callback will be called immediately and directly.
termio.py:  Added a shortcut function, termio.spawn() that creates a new Multiplex instance, runs Multiplex.spawn(), and returns the Multiplex instance.  See the code for exactly how it works (it's only three lines).
termio.py:  Removed Multiplex.redraw() since it was just about identical to resize().  Also, now resize sends a ctrl-l to the underlying program since this is (apparently) required for $LINES and $COLUMNS to be set correctly.  This should fix the long-standing bug where rows/cols would sometimes get set incorrectly when opening a new terminal.
termio.py:  You can now specify a syslog_host as an argument to BaseMultiplex (or Multiplex).  This is independent of the syslog module...  It uses remote_syslog.py (now bundled with Gate One) to connect to a syslog server over UDP to log messages directly.
termio.py:  term_num has been renamed to term_id inside of BaseMultiplex in the event that we want to use something other than a number as the identifier.
termio.py:  Added Multiplex.writeline() and Multiplex.writelines()...  Figured if we now have write() I might as well add these as well.
termio.py:  Added Multiplex.dump(): It's just a shortcut to running Multiplex.term.dump().
termio.py:  Got rid of Multiplex.die() and added Multiplex.isalive() that both determines whether or not the process is running and also returns True/False based on that.  Use of the die() method has been removed and the use of the Multiplex.alive variable has been changed everywhere to use isalive().
termio.py:  cmd is no longer an optional keyword argument in Multiplex.__init__() (it's required).
NOTE ABOUT THE ABOVE termio.py changes:  I realized that if I make a few little changes here and there to termio (and maybe terminal.py) I could make it work like pexpect so I wouldn't have to bundle/include that module with the SSH plugin (because we don't want passphrases showing up in the process list we must interactively enter them in).  This makes termio.py kind of special because you can now use it to perform effective line-by-line screen-scraping of terminal apps.  For some reason I'm just now realizing how cool that is...  I might spin it off into its own little project.
termio.py:  Switched from using subprocess.Popen to using os.execvpe in Multiplex.create().  The reason for this is that execvpe doesn't keep the forked Python interpreter open which saves memory.  In order to make this work I had to set the process to ignore the SIGCHLD signal so there won't be any defunct (zombie) processes hanging around after a child exits.  This caused me a _lot_ of headaches but I was eventually able to work them all out (crosses fingers).
termio.py:  Separated the Multiplex class into two parts:  _Multiplex() and MultiplexPOSIXIOLoop(_Multiplex) with Multiplex() being an alias to MultiplexPOSIXIOLoop() (if the platform matches).  This is to pave the way towards supporting different kinds of event loops and (potentially) non-POSIX platforms in the future.
termio.py:  Just a note regarding the documentation:  I wrote the documentation *before* I wrote the code and I've only updated it a little bit since.  There might be a few things that are 'off' here and there.  I'll be doing an entire rundown/re-check of *all* the documentation soon in preparation for the 1.0 release so if anything is wrong I'll be fixing it then.
utils.py:  Added two new functions:  which() and timeout_func() that do precisely what you'd expect.
utils.py:  Added shell_command():  It will reset the current SIGCHLD to SIG_DFL, execute the given command via commands.getstatusoutput(), then re-enable the SIGCHLD handler and return the result.  Also, you can pass it a timeout_duration (seconds) to return an error if the command takes too long.  I've gone ahead and changed all usage of getstatusoutput in Gate One to use this new function.
utils.py:  Changed the logic inside kill_dtached_proc() and killall() so that they no longer rely on external programs to perform their functions.
ssh_connect.py:  Added the aforementioned which() function and it's being used to determine the path to the ssh executable.  Also made a few little tweaks here and there to clean things up a bit.
Logging Plugin:  The process of listing the logs on the server (when you first open the log viewer) has been wrapped in a self-cancelling timeout (de-bounce) to make the whole operation a bit faster.  It is still too slow though and needs to be moved to a WebWorker I think.
Logging Plugin:  Fixed a bug where logs were showing up twice in the log viewer.
SSH Plugin:  When connecting to a host its SSH fingerprint will now be displayed in color-coded format that is derived from the fingerprint itself (the colors will always be the same for a given fingerprint).  The idea is like a bubble-babble or randomart hash in that it is supposed to get your brain used to seeing a particular thing whenever you connect to a host and if it ever changes it will be more easily noticed.
SSH Plugin:  ssh.py now includes a function that creates an 'ssh' directory inside of the user's directory for storing SSH-specific files and settings.  It gets registered/called via the new PLUGIN_HOOKS['Auth'] capability.  You'll need to migrate your old known_hosts file to the new location if you want to preserve it (such is the way of the beta).
SSH Plugin:  renamed connect_ssh() in ssh_connect.py to openssh_connect() to more properly reflect what it does (paving the way for supporting dropbear).
SSH Plugin:  ssh_connect.py now provides an option to enable OpenSSH's VisualHostKey option when connecting (user request).
SSH Plugin:  Too many changes/new functionality to list but they're not that important for this changelog since their respective GUI elements haven't been created yet.  They should be in the next commit.  In the mean time you can play around with the new functions from the JS console in your browser...  Execute "GateOne.Logging.level = 10;" and try any or all of the following:
    Run a command using an existing multiplexed SSH tunnel on what I'm calling a "sub-channel" (no terminal will open--you'll just get the command output back):
        GateOne.ws.send(JSON.stringify({'ssh_execute_command': {'cmd': 'dmesg | tail', 'term': 5}}));
    Get the colorized host fingerprint for any given host:
        GateOne.ws.send(JSON.stringify({'ssh_get_host_fingerprint': 'portarisk'}));
    Generate a new SSH keypair:
        GateOne.ws.send(JSON.stringify({'ssh_gen_new_keypair': {'name': 'testkey'}})); // Can also take 'passphrase', 'bits', 'comment', and 'keytype'
    Download said public key (save it to disk):
        GateOne.ws.send(JSON.stringify({'ssh_get_public_key': 'testkey'}));
    Download the private key:
        GateOne.ws.send(JSON.stringify({'ssh_get_private_key': 'testkey'}));
I'll be adding lots of great new functionality to the GUI using this new capability shortly.
  • Loading branch information...
1 parent 546d5a1 commit a68444610efa2892842893dae828a28527e64a6e @liftoff committed Jan 24, 2012
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -140,9 +140,9 @@ <h2>Docstrings<a class="headerlink" href="#docstrings" title="Permalink to this
<dl class="method">
<dt id="auth.BaseAuthHandler.user_logout">
-<tt class="descname">user_logout</tt><big>(</big><em>user</em><big>)</big><a class="reference internal" href="../_modules/auth.html#BaseAuthHandler.user_logout"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.BaseAuthHandler.user_logout" title="Permalink to this definition">¶</a></dt>
-<dd><p>Called immediately after a user logs out. Doesn't actually do
-anything. Just potential future use at this point.</p>
+<tt class="descname">user_logout</tt><big>(</big><em>user</em>, <em>redirect=None</em><big>)</big><a class="reference internal" href="../_modules/auth.html#BaseAuthHandler.user_logout"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.BaseAuthHandler.user_logout" title="Permalink to this definition">¶</a></dt>
+<dd><p>Called immediately after a user logs out, cleans up the user's session
+information and optionally, redirects them to <em>redirect</em> (URL).</p>
</dd></dl>
</dd></dl>
@@ -153,7 +153,7 @@ <h2>Docstrings<a class="headerlink" href="#docstrings" title="Permalink to this
<dd><p>A handler for when no authentication method is chosen (i.e. --auth=none).</p>
<dl class="method">
<dt id="auth.NullAuthHandler.get">
-<tt class="descname">get</tt><big>(</big><big>)</big><a class="reference internal" href="../_modules/auth.html#NullAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.NullAuthHandler.get" title="Permalink to this definition">¶</a></dt>
+<tt class="descname">get</tt><big>(</big><em>*args</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/auth.html#NullAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.NullAuthHandler.get" title="Permalink to this definition">¶</a></dt>
<dd><p>Sets the 'user' cookie with a new random session ID (<em>go_session</em>) and
sets <em>go_upn</em> to '%anonymous'.</p>
</dd></dl>
@@ -167,7 +167,7 @@ <h2>Docstrings<a class="headerlink" href="#docstrings" title="Permalink to this
<dl class="method">
<dt id="auth.GoogleAuthHandler.get">
<tt class="descname">get</tt><big>(</big><em>*args</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/auth.html#GoogleAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.GoogleAuthHandler.get" title="Permalink to this definition">¶</a></dt>
-<dd><p>Sets the 'user' cookie with an appropriate <em>go_upn</em> and <em>go_session</em>.</p>
+<dd><p>Sets the 'user' cookie with an appropriate <em>upn</em> and <em>session</em>.</p>
</dd></dl>
</dd></dl>
@@ -178,7 +178,7 @@ <h2>Docstrings<a class="headerlink" href="#docstrings" title="Permalink to this
<dd><p>Handles authenticating users via Kerberos/GSSAPI/SSO.</p>
<dl class="method">
<dt id="auth.KerberosAuthHandler.get">
-<tt class="descname">get</tt><big>(</big><big>)</big><a class="reference internal" href="../_modules/auth.html#KerberosAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.KerberosAuthHandler.get" title="Permalink to this definition">¶</a></dt>
+<tt class="descname">get</tt><big>(</big><em>*args</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/auth.html#KerberosAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.KerberosAuthHandler.get" title="Permalink to this definition">¶</a></dt>
<dd><p>Checks the user's request header for the proper Authorization data.
If it checks out the user will be logged in via _on_auth(). If not,
the browser will be redirected to login.</p>
@@ -192,7 +192,7 @@ <h2>Docstrings<a class="headerlink" href="#docstrings" title="Permalink to this
<dd><p>Handles authenticating users via PAM.</p>
<dl class="method">
<dt id="auth.PAMAuthHandler.get">
-<tt class="descname">get</tt><big>(</big><big>)</big><a class="reference internal" href="../_modules/auth.html#PAMAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.PAMAuthHandler.get" title="Permalink to this definition">¶</a></dt>
+<tt class="descname">get</tt><big>(</big><em>*args</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/auth.html#PAMAuthHandler.get"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#auth.PAMAuthHandler.get" title="Permalink to this definition">¶</a></dt>
<dd><p>Checks the user's request header for the proper Authorization data.
If it checks out the user will be logged in via _on_auth(). If not,
the browser will be redirected to login.</p>
@@ -292,6 +292,25 @@ <h3>Plugins<a class="headerlink" href="#plugins" title="Permalink to this headli
</div>
<div class="section" id="class-docstrings">
<h2>Class Docstrings<a class="headerlink" href="#class-docstrings" title="Permalink to this headline">¶</a></h2>
+<dl class="function">
+<dt id="gateone.require_auth">
+<tt class="descclassname">gateone.</tt><tt class="descname">require_auth</tt><big>(</big><em>method</em><big>)</big><a class="headerlink" href="#gateone.require_auth" title="Permalink to this definition">¶</a></dt>
+<dd><p>An equivalent to tornado.web.authenticated for WebSockets
+(TerminalWebSocket, specifically).</p>
+</dd></dl>
+
+<dl class="class">
+<dt id="gateone.HTTPSRedirectHandler">
+<em class="property">class </em><tt class="descclassname">gateone.</tt><tt class="descname">HTTPSRedirectHandler</tt><big>(</big><em>application</em>, <em>request</em>, <em>**kwargs</em><big>)</big><a class="headerlink" href="#gateone.HTTPSRedirectHandler" title="Permalink to this definition">¶</a></dt>
+<dd><p>A handler to redirect clients from HTTP to HTTPS.</p>
+<dl class="method">
+<dt id="gateone.HTTPSRedirectHandler.get">
+<tt class="descname">get</tt><big>(</big><big>)</big><a class="headerlink" href="#gateone.HTTPSRedirectHandler.get" title="Permalink to this definition">¶</a></dt>
+<dd><p>Just redirects the client from HTTP to HTTPS</p>
+</dd></dl>
+
+</dd></dl>
+
<dl class="class">
<dt id="gateone.BaseHandler">
<em class="property">class </em><tt class="descclassname">gateone.</tt><tt class="descname">BaseHandler</tt><big>(</big><em>application</em>, <em>request</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/gateone.html#BaseHandler"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#gateone.BaseHandler" title="Permalink to this definition">¶</a></dt>
@@ -331,11 +350,21 @@ <h2>Class Docstrings<a class="headerlink" href="#class-docstrings" title="Permal
</dd></dl>
<dl class="class">
-<dt id="gateone.OpenLogHandler">
-<em class="property">class </em><tt class="descclassname">gateone.</tt><tt class="descname">OpenLogHandler</tt><big>(</big><em>application</em>, <em>request</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/gateone.html#OpenLogHandler"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#gateone.OpenLogHandler" title="Permalink to this definition">¶</a></dt>
-<dd><p>Handles uploads of user logs and returns them to the client as a basic HTML
-page. Essentially, this works around the limitation of an HTML page being
-unable to save itself =).</p>
+<dt id="gateone.PluginCSSTemplateHandler">
+<em class="property">class </em><tt class="descclassname">gateone.</tt><tt class="descname">PluginCSSTemplateHandler</tt><big>(</big><em>application</em>, <em>request</em>, <em>**kwargs</em><big>)</big><a class="headerlink" href="#gateone.PluginCSSTemplateHandler" title="Permalink to this definition">¶</a></dt>
+<dd><p>Renders plugin CSS template files, passing them the same <em>prefix</em> and
+<em>container</em> variables used by the StyleHandler. This is so we don't need a
+CSS template rendering function in every plugin that needs to use {{prefix}}
+or {{container}}.</p>
+<p>gateone.js will automatically load all <a href="#id1"><span class="problematic" id="id2">*</span></a>.css files in plugin template
+directories using this method.</p>
+</dd></dl>
+
+<dl class="class">
+<dt id="gateone.JSPluginsHandler">
+<em class="property">class </em><tt class="descclassname">gateone.</tt><tt class="descname">JSPluginsHandler</tt><big>(</big><em>application</em>, <em>request</em>, <em>**kwargs</em><big>)</big><a class="headerlink" href="#gateone.JSPluginsHandler" title="Permalink to this definition">¶</a></dt>
+<dd><p>Combines all JavaScript plugins into a single file to keep things simple and
+speedy.</p>
</dd></dl>
<dl class="class">
@@ -383,7 +412,7 @@ <h3><a href="../index.html">Table Of Contents</a></h3>
<h4>Previous topic</h4>
<p class="topless"><a href="authpam.html"
- title="previous chapter"><tt class="docutils literal docutils literal docutils literal"><span class="pre">authpam.py</span></tt> - A PAM Authentication Module</a></p>
+ title="previous chapter"><tt class="docutils literal"><span class="pre">authpam.py</span></tt> - A PAM Authentication Module</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="logviewer.html"
title="next chapter"><tt class="docutils literal"><span class="pre">logviewer.py</span></tt> - Session Log Viewer</a></p>
@@ -218,7 +218,7 @@ <h4>Previous topic</h4>
title="previous chapter">User Guide</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="auth.html"
- title="next chapter"><tt class="docutils literal docutils literal docutils literal"><span class="pre">auth.py</span></tt> - Authentication Classes</a></p>
+ title="next chapter"><tt class="docutils literal"><span class="pre">auth.py</span></tt> - Authentication Classes</a></p>
<h3>This Page</h3>
<ul class="this-page-menu">
<li><a href="../_sources/Developer/index.txt"
@@ -153,7 +153,7 @@ <h2>Class Docstrings<a class="headerlink" href="#class-docstrings" title="Permal
<dl class="function">
<dt id="logviewer.flatten_log">
<tt class="descclassname">logviewer.</tt><tt class="descname">flatten_log</tt><big>(</big><em>log_path</em>, <em>preserve_renditions=True</em>, <em>show_esc=False</em><big>)</big><a class="reference internal" href="../_modules/logviewer.html#flatten_log"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#logviewer.flatten_log" title="Permalink to this definition">¶</a></dt>
-<dd><p>Given a log file at <em>log_path</em>, return a list of log lines contained within.</p>
+<dd><p>Given a log file at <em>log_path</em>, return a str of log lines contained within.</p>
<p>If <em>preserve_renditions</em> is True, CSI escape sequences for renditions will
be preserved as-is (e.g. font color, background, etc). This is to make the
output appear as close to how it was originally displayed as possible.
@@ -186,7 +186,7 @@ <h3><a href="../index.html">Table Of Contents</a></h3>
<h4>Previous topic</h4>
<p class="topless"><a href="gateone.html"
- title="previous chapter"><tt class="docutils literal docutils literal docutils literal"><span class="pre">gateone.py</span></tt> - Gate One's Main Script</a></p>
+ title="previous chapter"><tt class="docutils literal"><span class="pre">gateone.py</span></tt> - Gate One's Main Script</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="sso.html"
title="next chapter"><tt class="docutils literal"><span class="pre">sso.py</span></tt> - A Tornado Kerberos Single Sign-On Module</a></p>
@@ -277,6 +277,12 @@ <h2>Class Docstrings<a class="headerlink" href="#class-docstrings" title="Permal
</div>
</dd></dl>
+<dl class="method">
+<dt id="terminal.Terminal.remove_all_callbacks">
+<tt class="descname">remove_all_callbacks</tt><big>(</big><em>identifier</em><big>)</big><a class="headerlink" href="#terminal.Terminal.remove_all_callbacks" title="Permalink to this definition">¶</a></dt>
+<dd><p>Removes all callbacks associated with <em>identifier</em>.</p>
+</dd></dl>
+
<dl class="method">
<dt id="terminal.Terminal.terminal_reset">
<tt class="descname">terminal_reset</tt><big>(</big><em>*args</em>, <em>**kwargs</em><big>)</big><a class="reference internal" href="../_modules/terminal.html#Terminal.terminal_reset"><span class="viewcode-link">[source]</span></a><a class="headerlink" href="#terminal.Terminal.terminal_reset" title="Permalink to this definition">¶</a></dt>
@@ -349,7 +355,6 @@ <h2>Class Docstrings<a class="headerlink" href="#class-docstrings" title="Permal
7 Swedish
= Swiss</dd>
</dl>
-<p>NOTE: Doesn't actually do anything other than set the variable.</p>
</dd></dl>
<dl class="method">
@@ -375,7 +380,20 @@ <h2>Class Docstrings<a class="headerlink" href="#class-docstrings" title="Permal
7 Swedish
= Swiss</dd>
</dl>
-<p>NOTE: Doesn't actually do anything other than set the variable.</p>
+</dd></dl>
+
+<dl class="method">
+<dt id="terminal.Terminal.use_g0_charset">
+<tt class="descname">use_g0_charset</tt><big>(</big><big>)</big><a class="headerlink" href="#terminal.Terminal.use_g0_charset" title="Permalink to this definition">¶</a></dt>
+<dd><p>Sets the current charset to G0. This should get called when ASCII_SO
+is encountered.</p>
+</dd></dl>
+
+<dl class="method">
+<dt id="terminal.Terminal.use_g1_charset">
+<tt class="descname">use_g1_charset</tt><big>(</big><big>)</big><a class="headerlink" href="#terminal.Terminal.use_g1_charset" title="Permalink to this definition">¶</a></dt>
+<dd><p>Sets the current charset to G1. This should get called when ASCII_SI
+is encountered.</p>
</dd></dl>
<dl class="method">
@@ -691,7 +709,7 @@ <h3><a href="../index.html">Table Of Contents</a></h3>
<h4>Previous topic</h4>
<p class="topless"><a href="sso.html"
- title="previous chapter"><tt class="docutils literal docutils literal docutils literal"><span class="pre">sso.py</span></tt> - A Tornado Kerberos Single Sign-On Module</a></p>
+ title="previous chapter"><tt class="docutils literal"><span class="pre">sso.py</span></tt> - A Tornado Kerberos Single Sign-On Module</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="termio.html"
title="next chapter"><tt class="docutils literal"><span class="pre">termio.py</span></tt> - Terminal Input/Output Module</a></p>
Oops, something went wrong.

0 comments on commit a684446

Please sign in to comment.