Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit of graphterm

  • Loading branch information...
commit d8e019bde795f64256f8661f515dbb067adffee9 0 parents
@mitotic authored
Showing with 40,362 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +30 −0 LICENSE.txt
  3. +5 −0 MANIFEST.in
  4. +343 −0 README.rst
  5. +128 −0 SCREENSHOTS.rst
  6. BIN  doc-images/gt-screen-collapsed.png
  7. BIN  doc-images/gt-screen-emacs.png
  8. BIN  doc-images/gt-screen-gvi.png
  9. BIN  doc-images/gt-screen-gweather.png
  10. BIN  doc-images/gt-screen-ls-gls.png
  11. BIN  doc-images/gt-screen-split.png
  12. BIN  doc-images/gt-screen-stars3d.png
  13. +35 −0 graphterm/__init__.py
  14. +67 −0 graphterm/bin/ec2common.py
  15. +153 −0 graphterm/bin/ec2launch
  16. +57 −0 graphterm/bin/ec2list
  17. +4 −0 graphterm/bin/ec2scp
  18. +4 −0 graphterm/bin/ec2ssh
  19. +165 −0 graphterm/bin/gls
  20. +127 −0 graphterm/bin/gls.sh
  21. +19 −0 graphterm/bin/gopen
  22. +59 −0 graphterm/bin/gtermapi.py
  23. +48 −0 graphterm/bin/gvi
  24. +114 −0 graphterm/bin/gweather
  25. +10 −0 graphterm/bin/port_forward
  26. +17 −0 graphterm/certs/cert_to_p12.csh
  27. +38 −0 graphterm/certs/create_client_cert.csh
  28. +34 −0 graphterm/certs/create_server_cert.csh
  29. +21 −0 graphterm/certs/mac_import_cert.sh
  30. +156 −0 graphterm/daemon.py
  31. +59 −0 graphterm/episode4.txt
  32. +165 −0 graphterm/gterm.py
  33. +19 −0 graphterm/gterm_setup.py
  34. +387 −0 graphterm/gtermhost.py
  35. +723 −0 graphterm/gtermserver.py
  36. +1,618 −0 graphterm/lineterm.py
  37. +127 −0 graphterm/ordereddict.py
  38. +5,603 −0 graphterm/otrace.py
  39. +923 −0 graphterm/packetserver.py
  40. +48 −0 graphterm/testsslclient.py
  41. +19,976 −0 graphterm/www/ace.js
  42. +377 −0 graphterm/www/graphterm.css
  43. +2,023 −0 graphterm/www/graphterm.js
  44. BIN  graphterm/www/images/tango-application-x-executable.png
  45. BIN  graphterm/www/images/tango-audio-x-generic.png
  46. BIN  graphterm/www/images/tango-folder.png
  47. BIN  graphterm/www/images/tango-image-x-generic.png
  48. BIN  graphterm/www/images/tango-text-html.png
  49. BIN  graphterm/www/images/tango-text-x-generic-template.png
  50. BIN  graphterm/www/images/tango-text-x-generic.png
  51. BIN  graphterm/www/images/tango-text-x-script.png
  52. BIN  graphterm/www/images/tango-video-x-generic.png
  53. +185 −0 graphterm/www/index.html
  54. +4 −0 graphterm/www/jquery/jquery.min.js
  55. +94 −0 graphterm/www/jquery/js-plugins/rangy-core.js
  56. +641 −0 graphterm/www/mode-css.js
  57. +2,475 −0 graphterm/www/mode-html.js
  58. +1,226 −0 graphterm/www/mode-javascript.js
  59. +507 −0 graphterm/www/mode-python.js
  60. +1,012 −0 graphterm/www/mode-xml.js
  61. +27 −0 graphterm/www/perspective.css
  62. +41 −0 graphterm/www/test-ace.html
  63. +204 −0 graphterm/www/theme-twilight.js
  64. +183 −0 graphterm/www/theme-vibrant_ink.js
  65. BIN  graphterm/www/themes/stars-images/starry-night-PublicDomain-VeraKratochvil.jpg
  66. +13 −0 graphterm/www/themes/stars.css
  67. +64 −0 setup.py
4 .gitignore
@@ -0,0 +1,4 @@
+.DS_Store
+*~
+*.pyc
+local/*
30 LICENSE.txt
@@ -0,0 +1,30 @@
+# GraphTerm: A Graphical Terminal Interface
+#
+# GraphTerm was developed as part of the Mindmeldr project.
+# Documentation can be found at http://info.mindmeldr.com/code/graphterm
+#
+# BSD License
+#
+# Copyright (c) 2012, Ramalingam Saravanan <sarava@sarava.net>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
5 MANIFEST.in
@@ -0,0 +1,5 @@
+include LICENSE.txt *.rst *.html
+recursive-include graphterm *.py *.js *.css *.html *.rst *.txt *.gif *.jpg *.png
+graft graphterm/bin
+graft graphterm/certs
+global-exclude *~ *.pyc test*
343 README.rst
@@ -0,0 +1,343 @@
+GraphTerm: A Graphical Terminal Interface
+*********************************************************************************
+.. sectnum::
+.. contents::
+
+.. figure:: doc-images/gt-screen-stars3d.png
+ :align: center
+ :width: 90%
+ :figwidth: 60%
+
+ Screenshot of GraphTerm illustrating graphical ``gls`` and ``cat`` command
+ output using a 3D perspective theme (captured on OS X Lion, using
+ Google Chrome).
+
+ (More images can be found in SCREENSHOTS.rst or SCREENSHOTS.html
+ and in this `YouTube Video <http://youtu.be/JBMexdwXN8w>`_.)
+
+
+Introduction
+=============================
+
+``GraphTerm`` is a browser-based graphical terminal interface, that
+aims to seamlessly blend the command line and graphical user
+interfaces. The goal is to be fully backwards compatible with
+``xterm``. You should be able to use it just like a regular terminal
+interface, accessing additional features only as needed. GraphTerm builds
+upon two earlier projects,
+`XMLTerm <http://www.xml.com/pub/a/2000/06/07/xmlterm/index.html>`_
+which implemented a terminal using the Mozilla framework and
+`AjaxTerm <https://github.com/antonylesuisse/qweb/tree/master/ajaxterm>`_
+which is an AJAX/Python terminal implementation. (Another recent
+project along these lines is `TermKit <http://acko.net/blog/on-termkit/>`_.)
+
+In addition to terminal features, GraphTerm implements file "finder"
+or "explorer" features. It also incorporates some detached terminal
+features of ``GNU screen``, as well as additional browser-based
+sharing and collaboration capabilities. GraphTerm is designed to
+be touch-friendly, by facilitating command re-use to minimize
+the use of the keyboard.
+
+For a demo of some of the GraphTerm features, see this
+`YouTube Video <http://youtu.be/JBMexdwXN8w>`_.
+
+
+GraphTerm Design Goals:
+---------------------------------------------
+
+ - Full backwards compatibility with xterm
+
+ - Incremental feature set
+
+ - Minimalist no-frills graphical UI
+
+ - Minimize use of keyboard (tab/menu completion)
+
+ - Touch-friendly
+
+ - Cloud friendly
+
+ - Platform-independent browser client
+
+ - Easy sharing/collaboration
+
+
+GraphTerm Features:
+--------------------------------------------
+
+ - Clickable text: text displayed on terminal becomes clickable or "tappable"
+
+ - Seamlessly blend text and (optional) graphics
+
+ - History of all commands, entered by typing, clicking, or tapping
+
+ - Multiple users can collaborate on a single terminal window
+
+ - Multiple computers can be accessed from a single browser window
+
+ - Drag and drop
+
+ - Themable using CSS (including 3D perspectives)
+
+
+
+Installation
+==============================
+
+To install ``GraphTerm``, you need to have Python 2.6+ and the Bash
+shell on your Mac/Linux/Unix computer. For a quick install, if the python
+``setuptools`` module is already installed on your system,
+use the following commands::
+
+ sudo easy_install graphterm
+ sudo gterm_setup
+
+For the normal install procedure, download the release tarball from the
+`Python Package Index <http://pypi.python.org/pypi/otrace>`_, untar,
+and execute the following command in the ``graphterm-<version>`` directory::
+
+ python setup.py install
+
+You can also try out ``GraphTerm`` without installing it, by
+running the server ``gtermserver.py`` in the ``graphterm``
+subdirectory, provided you have the ``tornado`` python module
+installed in your system (or in the ``graphterm`` subdirectory).
+
+You can browse/fork the ``GraphTerm`` source code, and download the latest
+version, at `Github <https://github.com/mitotic/graphterm>`_.
+
+
+Usage
+=================================
+
+To start the ``GraphTerm`` server, use the command::
+
+ gtermserver --auth_code=none
+
+(You can use the ``--daemon=start`` option to run it in the background.)
+Then, open up a browser that supports websockets, such as Google
+Chrome, Firefox, or Safari (Chrome works best), and enter the
+following URL::
+
+ http://localhost:8900
+
+Alternatively, you can use the ``gterm`` command to open up the
+browser window.
+
+Once within the ``graphterm`` browser page, select the host you
+wish to connect to and create a new terminal session on the host.
+Then try out the following commands::
+
+ gls <directory>
+ gvi <text-filename>
+ gweather
+
+The first two are graphterm-aware scripts that imitate
+basic features of the standard ``ls`` and ``vi`` commands.
+
+*Usage Tips:*
+
+ - *Terminal type:* The default terminal type is set to ``linux``,
+ but it has a poor fullscreen mode and command history does
+ not work properly. You can try out the terminal types ``screen``
+ or ``xterm``, which may work better for some purposes.
+ Use the ``--term_type`` option to set the default terminal type.
+ (Fully supporting these terminal types is a work in progress.)
+
+ - *Sessions and sharing:* For each host, sessions are assigned default names like
+ ``tty1`` etc. You can also create unique session names simply by using
+ it in an URL, e.g.::
+
+ http://localhost:8900/local/mysession
+
+ The first user to create a session "owns" it. Others connecting to the
+ same session have read-only access (unless they "steal" the session).
+
+ - *Multiple hosts:* More than one host can connect to the ``graphterm`` server.
+ The local host is connected by default. To connect an additional
+ host, run the following command on the host you wish to connect::
+
+ gtermhost <serveraddr> <hostname>
+
+ where ``serveraddr`` is the address or name of the computer where
+ the server is running. You can use the ``--daemon=start`` option to
+ run the command in the background. (By default, the server listens for host
+ connections on port 8899.)
+
+ - *Security:* The ``--auth_code`` option can be used to specify
+ an authentication code required for users connecting to the server.
+ Although multiple hosts can connect to the terminal server,
+ initially, it would be best to use ``graphterm`` to simply connect
+ to ``localhost``, on a computer with only trusted users.
+ (*Note:* Users can always use SSH port forwarding to securely connect
+ to the ``graphterm`` server listening as ``localhost`` on a remote
+ computer, e.g.. ``ssh -L 8900:localhost:8900 user@example.com``)
+ *Do not run the server as root*. As the code matures,
+ security can be improved through the use of SSL certificates
+ and server/client authentication.
+ These features are implemented in the code, but have not been
+ properly configured/tested.
+
+ - *Visual cues:* In the default theme, *blue* color denotes text that can
+ be *clicked* or *tapped*. The action triggered by clicking depends on
+ several factors, such as whether there is text in the current command
+ line, and whether the Control modifier in the *Bottom menu* is active.
+ Click on the last displayed prompt to toggle display of the *Bottom menu*.
+ Clicking on other prompts toggles display of the command output
+ (unless the Control modifier is used, in which case the command line
+ is copied and pasted.)
+
+ - *Copy/paste:* Click on the cursor to paste text from the clipboard.
+
+ - *Drag and drop:* Sort of works within a window and across two
+ windows. You can drag filenames (text-only) and drop them on
+ folders, executables, or the command line. Visual feedback can
+ be confusing.
+
+ - *Command recall:* Use *up/down arrows* after partially typing a
+ command to search for matching commands, and use *right arrow*
+ for completion.
+
+ - *Touch devices:* Click on the cursor to display virtual keyboard
+ on the ipad etc.
+
+ - *Themes:* Themes are a work in progress, especially the 3-D
+ perspective theme (which only works on Chrome/Safari).
+
+
+
+Support
+=============================
+
+ - Report bugs and other issues using the Github `Issue Tracker <https://github.com/mitotic/graphterm/issues>`_.
+
+ - Additional documentation and updates will be made available on the project home page,
+ `info.mindmeldr.com/code/graphterm <http://info.mindmeldr.com/code/graphterm>`_.
+
+
+Implementation
+==========================================
+
+The GraphTerm server written in pure python, using the
+`Tornado web framework <http://tornadoweb.org>`_,
+with websocket support. The GraphTerm client uses standard
+HTML5+Javascript+CSS.
+
+GraphTerm extends the ``xterm`` terminal API by adding a
+new control sequence for programs to transmit a CGI-like HTTP response
+through standard output (via a websocket) to be displayed in the
+browser window. GraphTerm-aware programs can interact with the
+user using HTML forms etc.
+
+
+API for GraphTerm-aware programs
+==========================================
+
+A `graphterm-aware program <https://github.com/mitotic/graphterm/tree/master/graphterm/bin>`_
+writes to to the standard output in a format similar to a HTTP
+response, preceded and followed by
+``xterm``-like *escape sequences*::
+
+ \x1b[?1155;<cookie>h
+ {"content_type": "text/html", ...}
+
+ <table>
+ ...
+ </table>
+ \x1b[?1155l
+
+where ``<cookie>`` denotes a numeric value stored in the environment
+variable ``GRAPHTERM_COOKIE``. (The random cookie is a security
+measure that prevents malicious files from accessing GraphTerm.)
+The opening escape sequence is followed by a *dictionary* of header
+names and values, using JSON format. This is followed by a blank line,
+and then any data (such as the HTML fragment to be displayed).
+
+A `graphterm-aware program <https://github.com/mitotic/graphterm/tree/master/graphterm/bin>`_
+can be written in any language, much like a CGI script.
+See the programs ``gls``, ``gvi``, ``gweather``, ``ec2launch`` and
+``ec2list`` for examples of GraphTerm API usage.
+
+
+Cloud integration
+===============================
+
+The GraphTerm distribution includes the scripts ``ec2launch, ec2list, ec2scp,``
+and ``ec2ssh`` to launch and monitor Amazon Web Services EC2 instances
+to run GraphTerm in the "cloud". You will need to have an Amazon AWS
+account to use these scripts, and also need to install the ``boto`` python module.
+To create an instance, use the command::
+
+ ec2instance <instance_tagname>
+
+To *temporarily* run a publicly accessible GraphTerm server for
+demonstration or teaching purposes, use the following command on the instance::
+
+ gtermserver --daemon=start --auth_code=none --host=<primary_domain_or_address>
+
+*Note: This is totally insecure and should not be used for handling any sensitive information.*
+Ensure that the security group associated with the cloud instance
+allows access to inbound TCP port 22 (for SSH access), 8900 (for GraphTerm users to connect), and
+port 8899 (for GraphTerm hosts to connect). Also, when using ``ec2scp`` and ``sc2ssh``
+to access the instance, ensure that you specify the appropriate login name (e.g., ``ubuntu``
+for Ubuntu distribution).
+Secondary cloud instances should connect to the GraphTerm server on
+the primary instance using the command::
+
+ gtermhost --daemon=start <primary_domain_or_address> <secondary_host_name>
+
+For increased security in a publicly-accessible server, you will need to use a cryptic authentication code,
+and also use *https* instead of *http*, with SSL cettificates . Since GraphTerm is currently in
+*alpha* status, security cannot be guaranteed even with these options enabled.
+(To avoid these problems, use SSH port forwarding to access GraphTerm
+on ``localhost`` whenever possble.)
+
+*otrace* integration
+===============================
+
+GraphTerm was originally developed as a graphical front-end for
+`otrace <http://info.mindmeldr.com/code/otrace>`_,
+an object-oriented python debugger. Use the ``--oshell``
+option when connecting a host to the server enables ``otrace``
+debugging features, allowing access to the innards of the
+program running on the host.
+
+
+Caveats and limitations
+===============================
+
+ - *Reliability:* This software has not been subject to extensive testing. Use at your own risk.
+
+ - *Platforms:* The ``GraphTerm`` client should work on most recent browsers that support Websockets, such as Google Chrome, Firefox, and Safari. The ``GraphTerm`` server is pure-python, but with some OS-specific calls for file, shell, and terminal-related operations. It has been tested only on Linux and Mac OS X so far.
+
+ - *Current limitations:*
+ * Support for ``xterm`` escape sequences is incomplete.
+ * Most features of GraphTerm only work with the bash shell, not with C-shell, due the need for PROMPT_COMMAND to keep track of the current working directory.
+ * At the moment, you cannot customize the shell prompt. (You
+ should be able to in the future.)
+
+Credits
+===============================
+
+``GraphTerm`` is inspired by two earlier projects that implement the
+terminal interface within the browser,
+`XMLTerm <http://www.xml.com/pub/a/2000/06/07/xmlterm/index.html>`_ and
+`AjaxTerm <https://github.com/antonylesuisse/qweb/tree/master/ajaxterm>`_.
+It borrows many of the ideas from *XMLTerm* and re-uses chunks of code from
+*AjaxTerm*.
+
+The ``gls`` command uses icons from the `Tango Icon Library <http://tango.freedesktop.org>`_
+
+ Graphical editing uses the `Ajax.org Cloud9 Editor <http://ace.ajax.org>`_
+
+The 3D perspective mode was inspired by Sean Slinsky's `Star Wars
+Opening Crawl with CSS3 <http://www.seanslinsky.com/star-wars-crawl-with-css3>`_.
+
+``GraphTerm`` was developed as part of the `Mindmeldr <http://mindmeldr.com>`_ project, which is aimed at improving classroom interaction.
+
+
+License
+=====================
+
+``GraphTerm`` is distributed as open source under the `BSD-license <http://www.opensource.org/licenses/bsd-license.php>`_.
+
128 SCREENSHOTS.rst
@@ -0,0 +1,128 @@
+GraphTerm Screenshots
+*********************************************************************************
+.. sectnum::
+.. contents::
+
+ls vs. gls
+==================================================
+
+.. figure:: doc-images/gt-screen-ls-gls.png
+ :align: center
+
+ Comparing plain vanilla ``ls`` command and the graphterm-aware ``gls``.
+ The icons and the blue filenames are clickable. (The icon display
+ is optional, and may be disabled.)
+
+ ..
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
+
+
+stars3d theme, with icons enabled
+==================================================
+
+.. figure:: doc-images/gt-screen-stars3d.png
+ :align: center
+ :width: 90%
+ :figwidth: 85%
+
+ Showing output of the ``cat episode4.txt`` command below the
+ output of the ``gls`` command, using the 3D perspective theme.
+ This is actually a working theme, although it is meant for
+ primarily for "show". Scrolling through a large text file using the
+ ``vi`` editor in this theme gives a nice *roller coaster* effect!
+ (This screenshot was captured with Google Chrome running on
+ Mac OS X Lion, which supports hidden scrollbars. On other
+ software platforms, the scrollbar will be visible.)
+
+ ..
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
+
+Graphical weather forecast (using Google Weather API)
+=========================================================
+
+.. figure:: doc-images/gt-screen-gweather.png
+ :align: center
+
+ Showing the screen for the command ``gweather College Station`` to
+ illustrate inline HTML display. If the location is omitted, a HTML
+ form will be displayed to enter the location name.
+
+ ..
+
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
+
+Text editing (emacs)
+==================================================
+
+.. figure:: doc-images/gt-screen-emacs.png
+ :align: center
+
+ Showing the screen for the command ``emacs gtermserver.py`` to
+ illustrate backwards compatibility with the traditional terminal interface.
+
+ ..
+
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
+
+Graphical code editing using a "cloud" editor
+==================================================
+
+.. figure:: doc-images/gt-screen-gvi.png
+ :align: center
+
+ Showing the screen for the command ``gvi gtermserver.py`` to
+ illustrate graphical editing using the Ajax.org Cloud9 editor (ACE).
+
+ ..
+
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
+
+Collapsed mode
+==================================================
+
+.. figure:: doc-images/gt-screen-collapsed.png
+ :align: center
+
+ Showing the screen when all command output is collapsed. Clicking
+ on any of the underlined prompts will display the command output.
+ Also note the *Bottom menubar*, which is enabled by clicking on
+ the last prompt. Clicking on *Control* and then any of the prompts
+ will cause the corresponding command to be pasted.
+
+ ..
+
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
+
+Split scrolling
+==================================================
+
+.. figure:: doc-images/gt-screen-split.png
+ :align: center
+
+ Showing the split-screen scrolling mode, where the command
+ line is anchored at the bottom of the screen. Clicking on ``gls``
+ output will paste filenames into the command line.
+
+ ..
+
+.. raw:: html
+
+ <hr style="margin-bottom: 3em;">
BIN  doc-images/gt-screen-collapsed.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  doc-images/gt-screen-emacs.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  doc-images/gt-screen-gvi.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  doc-images/gt-screen-gweather.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  doc-images/gt-screen-ls-gls.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  doc-images/gt-screen-split.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  doc-images/gt-screen-stars3d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 graphterm/__init__.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+#
+# GraphTerm: A Graphical Terminal Interface
+#
+# GraphTerm was developed as part of the Mindmeldr project.
+# Documentation can be found at http://info.mindmeldr.com/code/graphterm
+#
+# BSD License
+#
+# Copyright (c) 2012, Ramalingam Saravanan <sarava@sarava.net>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+"""GraphTerm: A Graphical Terminal Interface
+"""
67 graphterm/bin/ec2common.py
@@ -0,0 +1,67 @@
+"""
+ec2common: Common code to manage Amazon AWS EC2 instances
+"""
+
+import boto
+import os
+import sys
+import time
+
+from boto.route53.connection import Route53Connection
+
+boto_config = """Create the file ~/.boto containing:
+ [Credentials]
+ aws_access_key_id = ACCESS_KEY
+ aws_secret_access_key = SECRET_KEY
+"""
+
+if not os.path.exists(os.path.expanduser("~/.boto")):
+ print >> sys.stderr, boto_config
+ sys.exit(1)
+
+def get_zone(zone_domain):
+ route53conn = Route53Connection()
+
+ results = route53conn.get_all_hosted_zones()
+ zones = results['ListHostedZonesResponse']['HostedZones']
+
+ for zone in zones:
+ if zone['Name'] == zone_domain+".":
+ return route53conn, zone
+
+ return (route53conn, None)
+
+def cname(route53conn, zone, domain_name, alt_name, ttl=60, remove=False):
+ from boto.route53.record import ResourceRecordSets
+ zone_id = zone['Id'].replace('/hostedzone/', '')
+ changes = ResourceRecordSets(route53conn, zone_id)
+ change = changes.add_change("DELETE" if remove else "CREATE",
+ name=domain_name,
+ type="CNAME", ttl=ttl)
+ if alt_name:
+ change.add_value(alt_name)
+ changes.commit()
+
+def get_ec2():
+ return boto.connect_ec2()
+
+def kill(instance_ids=[]):
+ ec2 = get_ec2()
+ ec2.terminate_instances(instance_ids=instance_ids)
+
+def get_instance_props(instance_id=None):
+ ec2 = get_ec2()
+
+ all_instances = ec2.get_all_instances()
+ props_list = []
+ for res in all_instances:
+ iobj = res.instances[0]
+ if not instance_id or instance_id == iobj.id or any(tag.startswith(instance_id) for tag in iobj.tags):
+ props = {"id": iobj.id,
+ "public_dns": iobj.public_dns_name,
+ "key": iobj.key_name,
+ "state": iobj.state,
+ "tags": iobj.tags}
+ props_list.append(props)
+
+ return props_list
153 graphterm/bin/ec2launch
@@ -0,0 +1,153 @@
+#!/usr/bin/env python
+#
+
+"""
+ec2launch: Launch Amazon AWS EC2 instance
+"""
+
+import boto
+import json
+import os
+import random
+import sys
+import time
+from optparse import OptionParser
+
+import gtermapi
+import ec2common
+
+ssh_dir = os.path.expanduser("~/.ssh")
+
+arg_list = [
+("arg1", "", "Instance name"),
+("type", ("", "t1.micro", "m1.small", "m1.medium", "m1.large"), "Instance type"),
+("key_name", "ec2key", "Instance management SSH key name"),
+("ami", "ami-82fa58eb", "Instance OS (default: Ubuntu 12.04LTS)"),
+("security", "default", "Security group"),
+("domain", "", "Instance domain"),
+]
+
+usage = "usage: %prog [-f] <instance-tagname>"
+parser = OptionParser(usage=usage)
+parser.add_option("-f", "--fullscreen", dest="fullscreen", default=False,
+ help="Fullscreen display", action="store_true")
+
+parser.add_option("-t", "--text", dest="text", default=False,
+ help="Text only", action="store_true")
+
+parser.add_option("", "--dry_run", dest="dry_run", default=False,
+ help="Dry run", action="store_true")
+
+form_html = gtermapi.add_options(parser, arg_list, title="Create Amazon EC2 instance")
+
+(options, args) = parser.parse_args()
+
+headers = {"content_type": "text/html"}
+headers["x_gterm_response"] = "pagelet_fullscreen"
+headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()}
+
+if args:
+ instance_tag = args[0]
+elif options.domain:
+ instance_tag = options.domain
+else:
+ if not gtermapi.Lterm_cookie or options.text:
+ print >> sys.stderr, usage
+ sys.exit(1)
+ headers["x_gterm_response"] = "pagelet_form"
+ sys.stdout.write(gtermapi.wrap(json.dumps(headers)+"\n\n"+form_html))
+ sys.exit(1)
+
+if options.domain:
+ subdomain_name, sep, top_level_domain = options.domain.partition(".")
+else:
+ subdomain_name, top_level_domain = "", ""
+
+key_file = os.path.join(ssh_dir, options.key_name+".pem")
+
+if not os.path.exists(ssh_dir):
+ print >> sys.stderr, "ec2launch: %s directory not found!" % ssh_dir
+ sys.exit(1)
+
+headers = {"content_type": "text/html"}
+headers["x_gterm_response"] = "pagelet_fullscreen"
+headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()}
+
+startup_script = """#!/bin/bash
+set -e -x
+apt-get update && apt-get upgrade -y
+apt-get install -y python-setuptools
+easy_install tornado
+easy_install otrace
+easy_install graphterm
+sudo gterm_setup
+"""
+
+# Connect to EC2
+ec2 = boto.connect_ec2()
+
+# Create key pair, if needed
+if not os.path.exists(key_file):
+ key_pair = ec2.create_key_pair(options.key_name)
+ key_pair.save(ssh_dir)
+ os.chmod(key_file, 0600)
+
+if options.dry_run:
+ print >> sys.stderr, "run_instances:", dict(image_id=options.ami,
+ instance_type=options.type,
+ key_name=options.key_name,
+ security_groups=[options.security],
+ user_data=startup_script)
+ sys.exit(1)
+
+# Launch instance
+reservation = ec2.run_instances(image_id=options.ami,
+ instance_type=options.type,
+ key_name=options.key_name,
+ security_groups=[options.security],
+ user_data=startup_script)
+instance = reservation.instances[0]
+
+# Wait for instance to start running
+Status_template = """<em>Creating instance <b>%s</b>:</em> status=<b>%s</b> (waiting %ds)"""
+
+status = instance.update()
+headers["x_gterm_response"] = "pagelet_fullscreen"
+start_time = time.time()
+while status == "pending":
+ timesec = int(time.time() - start_time)
+ sys.stdout.write(gtermapi.wrap(json.dumps(headers)+"\n\n"+(Status_template % (instance_tag, status, timesec))))
+ time.sleep(3)
+ status = instance.update()
+
+if status != "running":
+ print >> sys.stderr, "ec2launch: ERROR Failed to launch instance: %s" % status
+ sys.exit(1)
+
+# Tag instance
+if instance_tag:
+ instance.add_tag(instance_tag)
+
+instance_id = reservation.id
+instance_obj = None
+all_instances = ec2.get_all_instances()
+for r in all_instances:
+ if r.id == instance_id:
+ instance_obj = r.instances[0]
+ break
+
+if not instance_obj:
+ print >> sys.stderr, "ec2launch: ERROR Unable to find launched instance: %s" % status
+ sys.exit(1)
+
+instance_domain = instance_obj.public_dns_name
+
+if options.domain:
+ route53conn, zone = ec2common.get_zone(top_level_domain)
+ if not zone:
+ print >> sys.stderr, "No Route53 zone found for %s" % options.domain
+
+ # Create new CNAME entry pointing to instance public DNS
+ ec2common.cname(route53conn, zone, options.domain, instance_domain)
+
+print "Created EC2 instance: id=%s, domain=%s" % (instance_id, instance_domain)
57 graphterm/bin/ec2list
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+#
+
+"""
+ec2list: Launch Amazon AWS EC2 instances
+"""
+
+import boto
+import json
+import os
+import sys
+import time
+from optparse import OptionParser
+
+import ec2common
+
+usage = "usage: %prog [-f] [<tag_prefix_or_id>]"
+parser = OptionParser(usage=usage)
+parser.add_option("-f", "--fullscreen", dest="fullscreen", default=False,
+ help="Fullscreen display", action="store_true")
+
+parser.add_option("", "--kill", dest="kill", default=False,
+ help="Kill single matching instance", action="store_true")
+
+parser.add_option("", "--killall", dest="killall", default=False,
+ help="Kill all matching instances", action="store_true")
+
+
+(options, args) = parser.parse_args()
+
+prefix = args[0] if args else ""
+
+if not os.path.exists(os.path.expanduser("~/.boto")):
+ print >> sys.stderr, config_info
+ sys.exit(1)
+
+headers = {"content_type": "text/html"}
+headers["x_gterm_response"] = "pagelet_fullscreen"
+headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()}
+
+props_list = ec2common.get_instance_props(instance_id=prefix)
+
+Props_format = "Instance: id=%(id)s, domain=%(public_dns)s, key=%(key)s, tags=%(tags)s, state=%(state)s"
+for props in props_list:
+ print Props_format % props
+
+if options.kill or options.killall:
+ if not props_list:
+ print >> sys.stderr, "No instances to kill"
+ sys.exit(1)
+ id_list = [x["id"] for x in props_list]
+
+ if len(id_list) > 1 and not options.killall:
+ print >> sys.stderr, "Specify --killall to kill multiple instances"
+ sys.exit(1)
+ ec2common.kill(instance_ids=id_list)
+ print >> sys.stderr, "Killed", id_list
4 graphterm/bin/ec2scp
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+scp -i ~/.ssh/ec2key.pem $*
+
4 graphterm/bin/ec2ssh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+ssh -i ~/.ssh/ec2key.pem $*
+
165 graphterm/bin/gls
@@ -0,0 +1,165 @@
+#!/usr/bin/env python
+#
+
+"""
+gls: graphterm-aware ls
+"""
+
+import json
+import mimetypes
+import os
+import sys
+import xml.dom.minidom
+from optparse import OptionParser
+
+Lterm_cookie = os.getenv("GRAPHTERM_COOKIE", "")
+Html_escapes = ["\x1b[?1155;%sh" % Lterm_cookie,
+ "\x1b[?1155l"]
+
+SPECIAL_FILES = set(["..", ".", "~"])
+
+glscmd = os.getenv("GRAPHTERM_LS_CMD", "") or "gls"
+gvicmd = os.getenv("GRAPHTERM_VI_CMD", "") or "gvi"
+gopencmd = os.getenv("GRAPHTERM_OPEN_CMD", "") or "gopen"
+
+FILE_TYPES = {"directory": ("/static/images/tango-folder.png", "cd %(path); "+glscmd+" -f"),
+ "executable": ("/static/images/tango-application-x-executable.png", ""),
+ "audiofile": ("/static/images/tango-audio-x-generic.png", gopencmd),
+ "htmlfile": ("/static/images/tango-text-html.png", gvicmd),
+ "imagefile": ("/static/images/tango-image-x-generic.png", gopencmd),
+ "plainfile": ("/static/images/tango-text-x-generic-template.png", gopencmd),
+ "textfile": ("/static/images/tango-text-x-generic.png", gvicmd),
+ "videofile": ("/static/images/tango-video-x-generic.png", gopencmd),
+}
+
+IMGFORMAT = "<td><a class='gterm-link gterm-imglink %(classes)s' href='file://%(filepath)s' data-gtermmime='x-graphterm/%(filetype)s' data-gtermcmd='%(filecmd)s'><img class='gterm-img' src='%(fileicon)s'></img></a>"
+
+TXTFORMAT = "<td><a class='gterm-link %(classes)s' href='file://%(filepath)s' data-gtermmime='x-graphterm/%(filetype)s' data-gtermcmd='%(filecmd)s'>%(filename)s</a>"
+
+def file2html(filepath, filename, filemode=0):
+ if filename in SPECIAL_FILES:
+ filetype = "directory"
+ elif os.path.lexists(filepath):
+ mimetype, encoding = mimetypes.guess_type(filename)
+ filetype = "plainfile"
+ if os.path.isdir(filepath):
+ filetype = "directory"
+ elif os.access(filepath, os.X_OK):
+ filetype = "executable"
+ elif mimetype:
+ if mimetype.startswith("audio/"):
+ filetype = "audiofile"
+ elif mimetype == "text/html":
+ filetype = "htmlfile"
+ elif mimetype.startswith("image/"):
+ filetype = "imagefile"
+ elif mimetype.startswith("text/") or mimetype == "application/javascript":
+ filetype = "textfile"
+ elif mimetype.startswith("video/"):
+ filetype = "videofile"
+ else:
+ return "", ""
+
+ classes = "droppable" if filetype in ("directory", "executable") else ""
+ fileicon, filecmd = FILE_TYPES[filetype]
+ params = {"classes": classes, "filepath": filepath, "filename": filename,
+ "filetype": filetype, "fileicon": fileicon, "filecmd": filecmd}
+
+ return IMGFORMAT % params, TXTFORMAT % params
+
+def files2html(file_list, ncols=4):
+ rows = []
+ rowimg = []
+ rowtxt = []
+
+ for j, fileinfo in enumerate(file_list):
+ if len(fileinfo) == 2:
+ fpath, fname, fsize, ftime, fmode, fuid, fgid = fileinfo[0], fileinfo[1], 0, 0, 0, 0, 0
+ else:
+ fpath, fname, fsize, ftime, fmode, fuid, fgid = fileinfo
+ cellimg, celltxt = file2html(fpath, fname, fmode)
+ rowimg.append(cellimg)
+ rowtxt.append(celltxt)
+ if rowtxt and (not ((j+1) % ncols) or (j+1 == len(file_list))):
+ rows.append( '<tr class="gterm-rowimg">' + "".join(rowimg) )
+ rows.append( '<tr class="gterm-rowtxt">' + "".join(rowtxt) )
+ rowimg = []
+ rowtxt = []
+
+ return "\n".join(rows)
+
+def get_file_info(filename):
+ filename = os.path.expanduser(filename)
+ filepath = os.path.normcase(os.path.abspath(filename))
+ if filename.startswith(".."):
+ filename = filepath
+ if os.path.exists(filepath):
+ fstat = os.stat(filepath)
+ elif os.path.lexists(filepath):
+ fstat = os.lstat(filepath)
+ else:
+ return (filepath, filename, 0, 0, 0, 0, 0)
+ return (filepath, filename, fstat.st_size, fstat.st_mtime, fstat.st_mode, fstat.st_uid, fstat.st_gid)
+
+def wrap(html):
+ return Html_escapes[0] + html + Html_escapes[1]
+
+
+usage = "usage: %prog [-f] <location>"
+parser = OptionParser(usage=usage)
+parser.add_option("-f", "--fullscreen",
+ action="store_true", dest="fullscreen", default=False,
+ help="Fullscreen display")
+parser.add_option("-a", "--all",
+ action="store_true", dest="all", default=False,
+ help="Display all files, including hidden")
+parser.add_option("-s", "--size",
+ action="store_true", dest="size", default=False,
+ help="Sort by file size")
+parser.add_option("-t", "--timer",
+ action="store_true", dest="time", default=False,
+ help="Sort by time modified")
+
+(options, args) = parser.parse_args()
+
+home_dir = os.path.expanduser("~")
+work_dir = os.getcwd()
+parent_dir, dir_name = os.path.split(work_dir)
+
+special_dirs = [(parent_dir, ".."),
+ (work_dir, "."),
+ (home_dir, "~")]
+
+if not args:
+ args = os.listdir(work_dir)
+ if not options.all:
+ args = [x for x in args if not x.startswith(".")]
+
+File_list = [get_file_info(filename) for filename in args]
+
+if options.size:
+ File_list.sort(key=lambda x:x[2])
+elif options.time:
+ File_list.sort(key=lambda x:x[3])
+else:
+ File_list.sort(key=lambda x:x[1])
+
+ncols = 4
+
+Table_list = ['<table frame=none border=0>',
+ '<colgroup colspan=%d width=1*>' % (ncols,),
+ ]
+
+Table_list.append(files2html(special_dirs, ncols))
+Table_list.append(files2html(File_list, ncols))
+
+Table_list.append('</table>')
+
+html = "\n".join(Table_list) + "\n"
+
+headers = {"content_type": "text/html"}
+headers["x_gterm_response"] = "pagelet_fullscreen" if options.fullscreen else "pagelet"
+headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": work_dir}
+
+sys.stdout.write(wrap(json.dumps(headers)+"\n\n"+html))
+
127 graphterm/bin/gls.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+# gls: a GraphTerm shell wrapper for the UNIX "ls" command
+# Usage: gls
+
+# TEMPORARY: Ignores all arguments except -f and last directory
+
+options=""
+response_type="pagelet"
+dir=""
+for arg in $*; do
+ if [ "$arg" == "-f" ]; then
+ response_type="pagelet_fullscreen"
+ elif [[ "$arg" == -* ]]; then
+ options="$options $arg"
+ else
+ dir="$arg"
+ fi
+done
+
+if [ "$dir" != "" ]; then
+ cd $dir
+fi
+
+if [ "$options" != "" ]; then
+ # Options encountered; default "ls" behaviour
+ /bin/ls $*
+ exit
+fi
+
+ncols=4
+
+echocmd1="echo -n"
+##echocmd1="/bin/echo -e"
+echocmd2="echo"
+
+rowimg=""
+rowtxt=""
+
+if [ -z $GRAPHTERM_PROMPT ]; then
+ glscmd="~/meldr-hg/xmlterm/bin/gls"
+ gvicmd="~/meldr-hg/xmlterm/bin/gvi"
+else
+ glscmd="gls"
+ gvicmd="gvi"
+fi
+
+output=""
+clickcmd="cd %(path); $glscmd -f"
+files='.. . ~'
+for file in $files; do
+ fileicon="/static/images/tango-folder.png"
+ filetype="specialfile"
+
+ if [ "$file" == ".." ]; then
+ fullpath=$(dirname "$PWD")
+ elif [ "$file" == "." ]; then
+ fullpath="$PWD"
+ elif [ "$file" == '~' ]; then
+ fullpath="$HOME"
+ fi
+
+ rowimg="${rowimg}<td><a class='gterm-link gterm-imglink' href='file://${fullpath}' data-gtermmime='x-graphterm/${filetype}' data-gtermcmd='${clickcmd}'><img class='gterm-img' src='$fileicon'></img></a>"
+
+ rowtxt="${rowtxt}<td><a class='gterm-link' href='file://${fullpath}' data-gtermmime='x-graphterm/${filetype}' data-gtermcmd='${clickcmd}'>${file}</a>"
+
+done
+
+if [ "$rowtxt" != "" ]; then
+ output="$output <tr class='gterm-rowimg'>$rowimg"
+ output="$output <tr class='gterm-rowtxt'>$rowtxt"
+ rowimg=""
+ rowtxt=""
+fi
+
+ifile=0
+for file in *; do
+ fullpath="$PWD/$file"
+ if [ -d "$file" ]; then #directory
+ filetype="directory"
+ fileicon="/static/images/tango-folder.png"
+ clickcmd="cd %(path); $glscmd -f"
+ elif [ -x "$file" ]; then #executable
+ filetype="executable"
+ fileicon="/static/images/tango-application-x-executable.png"
+ clickcmd=""
+ else #plain file
+ filetype="plainfile"
+ fileicon="/static/images/tango-text-x-generic.png"
+ clickcmd="$gvicmd"
+ fi
+
+ rowimg="${rowimg}<td><a class='gterm-link gterm-imglink' href='file://${fullpath}' data-gtermmime='x-graphterm/${filetype}' data-gtermcmd='${clickcmd}'><img class='gterm-img' src='$fileicon'></img></a>"
+
+ rowtxt="${rowtxt}<td><a class='gterm-link' href='file://${fullpath}' data-gtermmime='x-graphterm/${filetype}' data-gtermcmd='${clickcmd}'>${file}</a>"
+
+ (( ifile++ ))
+
+ if [ $(( ifile % ncols )) -eq 0 ]; then
+ output="$output <tr class='gterm-rowimg'>$rowimg"
+ output="$output <tr class='gterm-rowtxt'>$rowtxt"
+ rowimg=""
+ rowtxt=""
+ fi
+
+done
+
+output="$output <tr class='gterm-rowimg'>$rowimg"
+output="$output <tr class='gterm-rowtxt'>$rowtxt"
+
+headers='{"content_type": "text/html", "x_gterm_response": "'"${response_type}"'", "x_gterm_parameters": {"scroll": "top", "current_directory": "'"${PWD}"'"}}'
+
+esc=`printf "\033"`
+nl=`printf "\012"`
+graphterm_code="1155"
+$echocmd1 "${esc}[?${graphterm_code};${GRAPHTERM_COOKIE}h"
+
+$echocmd1 "$headers"
+$echocmd2 ""
+$echocmd2 ""
+
+$echocmd2 '<table frame=none border=0>'
+$echocmd2 "<colgroup colspan=$ncols width=1*>"
+
+$echocmd2 $output
+$echocmd2 '</table>'
+$echocmd1 "${esc}[?${graphterm_code}l"
+echo
19 graphterm/bin/gopen
@@ -0,0 +1,19 @@
+#!/bin/bash
+# gopen: open a file
+# Usage: gopen <filename>
+
+if [ $# -eq 0 ]; then
+ echo "Usage: gopen <file>"
+ exit 1
+fi
+
+if which open > /dev/null; then
+ open "$1"
+elif which xdg-open > /dev/null; then
+ xdg-open "$1" &> /dev/null &
+elif which gnome-open > /dev/null; then
+ gnome-open "$1"
+else
+ echo "No open command found!"
+ exit 1
+fi
59 graphterm/bin/gtermapi.py
@@ -0,0 +1,59 @@
+"""
+gtermapi: Common code gterm-aware programs
+"""
+
+import os
+import random
+
+Lterm_cookie = os.getenv("GRAPHTERM_COOKIE", "")
+Html_escapes = ["\x1b[?1155;%sh" % Lterm_cookie,
+ "\x1b[?1155l"]
+
+def wrap(html):
+ return Html_escapes[0] + html + Html_escapes[1]
+
+Form_template = """<div id="gterm-form-%s" class="gterm-form">%s %s
+<input id="gterm-form-command-%s" class="gterm-form-button gterm-form-command" type="submit" data-gtermformcmd="%s" data-gtermformargs="%s"></input> <input class="gterm-form-button gterm-form-cancel" type="button" value="Cancel"></input>
+</div>"""
+
+Input_text_template = """<span class="gterm-form-label" data-gtermhelp="%s">%s</span><input id="gterm_%s_%s" name="%s" class="gterm-form-input" type="text" autocomplete="off" %s></input>"""
+
+Select_template = """<span class="gterm-form-label" data-gtermhelp="%s">%s</span><select id="gterm_%s_%s" name="%s" class="gterm-form-input" size=1>
+%s
+</select>"""
+Select_option_template = """<option value="%s" %s>%s</option>"""
+
+def create_input_html(id_suffix, arg_list):
+ input_list = []
+ first_arg = True
+ for opt_name, opt_default, opt_help in arg_list:
+ if isinstance(opt_default, basestring):
+ opt_label = "" if opt_name.startswith("arg") else (opt_name+": ")
+ extras = ' autofocus="autofocus"' if first_arg else ""
+
+ input_list.append(Input_text_template % (opt_help, opt_label, id_suffix, opt_name, opt_name, extras))
+ elif isinstance(opt_default, (list, tuple)):
+ opt_list = []
+ opt_sel = "selected"
+ for opt_value in opt_default:
+ opt_list.append(Select_option_template % (opt_value, opt_sel, opt_value or "Select..."))
+ opt_sel = ""
+ input_list.append(Select_template % (opt_help, opt_name+": ", id_suffix, opt_name, opt_name,
+ "\n".join(opt_list)))
+ first_arg = False
+
+ return "\n".join(input_list)
+
+def create_form(id_suffix, arg_list, title=""):
+ opt_names = ",".join(x[0] for x in arg_list)
+ input_html = create_input_html(id_suffix, arg_list)
+ return Form_template % (id_suffix, title, input_html, id_suffix, "ec2launch -f", opt_names)
+
+def add_options(parser, arg_list, title=""):
+ """Returns form html, after adding options"""
+ for opt_name, opt_default, opt_help in arg_list:
+ default= opt_default[0] if isinstance(opt_default, (list, tuple)) else opt_default
+ parser.add_option("", "--"+opt_name, dest=opt_name, default=default,
+ help=opt_help)
+ id_suffix = "1%09d" % random.randrange(0, 10**9)
+ return create_form(id_suffix, arg_list, title=title)
48 graphterm/bin/gvi
@@ -0,0 +1,48 @@
+#!/bin/bash
+# gvi: a GraphTerm shell wrapper for editing files
+# Usage: gvi <filename>
+
+options=""
+file=""
+for arg in $*; do
+ if [[ "$arg" == -* ]]; then
+ options="$options $arg"
+ else
+ file="$arg"
+ fi
+done
+
+if [ "$file" == "" ]; then
+ echo "Usage: gvi <filename>"
+ exit 1
+fi
+
+if [[ "$file" == /* ]]; then
+ # Absolute path
+ file="$file"
+else
+ # Relative path
+ file="$PWD/$file"
+fi
+
+tailname=$(basename "$file")
+filename="${tailname%.*}"
+extension="${tailname##*.}"
+
+echocmd1="echo -n"
+##echocmd1="/bin/echo -e"
+echocmd2="echo"
+
+headers='{"content_type": "text/html", "x_gterm_response": "edit_file", "x_gterm_parameters": {"filepath": "'"${file}"'", "editor": "", "modify": "true", "command": "", "current_directory": "'"${PWD}"'"}}'
+
+esc=`printf "\033"`
+nl=`printf "\012"`
+cr=`printf "\015"`
+graphterm_code="1155"
+$echocmd1 "${esc}[?${graphterm_code};${GRAPHTERM_COOKIE}h"
+
+$echocmd1 "$headers"
+$echocmd2 ""
+$echocmd2 ""
+$echocmd1 "${esc}[?${graphterm_code}l"
+echo
114 graphterm/bin/gweather
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+#
+
+"""
+gweather: Display weather using Google Weather API
+"""
+
+import json
+import os
+import random
+import sys
+import xml.dom.minidom
+from optparse import OptionParser
+
+try:
+ from urllib.request import urlopen
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlopen, urlencode
+
+Lterm_cookie = os.getenv("GRAPHTERM_COOKIE", "")
+Html_escapes = ["\x1b[?1155;%sh" % Lterm_cookie,
+ "\x1b[?1155l"]
+
+def wrap(html):
+ return Html_escapes[0] + html + Html_escapes[1]
+
+Google_weather_url = "http://www.google.com/ig/api?"
+Google_img_url = "http://www.google.com/ig/images/weather"
+
+title_template = """
+<b>Current weather in %(city)s</b>
+"""
+cur_template = """
+<img src="http://www.google.com%(icon)s" alt="%(condition)s"> <span class="weather-item">%(temp_f)s &deg;F,</span> <span class="weather-item">%(condition)s</span>
+<b>Forecast</b>
+"""
+
+fcst_template = """
+<img src="http://www.google.com%(icon)s" alt="%(condition)s"> <span class="weather-item">%(day_of_week)s:</span> <span class="weather-item">%(condition)s,</span> <span class="weather-item">Low %(low)s &deg;F,</span> <span class="weather-item">High %(high)s &deg;F</span>
+"""
+
+form_template = """<div class="gterm-form">Please specify location for weather info:
+ <input id="gweather-input%s" name="arg1" type="text" autocomplete="off" autofocus="autofocus"></input>
+<input id="gterm-form-command-%s" class="gterm-form-button gterm-form-command" type="submit" data-gtermformcmd="gweather -f" data-gtermformargs="arg1"></input>
+<input class="gterm-form-button gterm-form-cancel" type="button" value="Cancel"></input>
+ </div>"""
+
+usage = "usage: %prog [-f] <location>"
+parser = OptionParser(usage=usage)
+parser.add_option("-f", "--fullscreen",
+ action="store_true", dest="fullscreen", default=False,
+ help="Fullscreen display")
+parser.add_option("-t", "--text",
+ action="store_true", dest="text", default=False,
+ help="Plain text display")
+
+(options, args) = parser.parse_args()
+location = " ".join(args)
+
+headers = {"content_type": "text/html"}
+headers["x_gterm_response"] = "pagelet_fullscreen"
+headers["x_gterm_parameters"] = {"scroll": "top", "current_directory": os.getcwd()}
+
+if not location:
+ if not Lterm_cookie or options.text:
+ print >> sys.stderr, "Please specify location"
+ sys.exit(1)
+ random_id = "1%09d" % random.randrange(0, 10**9)
+ form_html = form_template % (random_id, random_id)
+ headers["x_gterm_response"] = "pagelet_form"
+ print wrap(json.dumps(headers)+"\n\n"+form_html)
+ sys.exit(1)
+
+def xml2dict(root_elem, schema):
+ retval = {}
+ for key, value in schema.iteritems():
+ lst = []
+ retval[key] = lst
+ for elem in dom.documentElement.getElementsByTagName(key):
+ if isinstance(value, dict):
+ vals = xml2dict(elem, value)
+ else:
+ vals = {}
+ for key2 in value:
+ vals[key2] = elem.getElementsByTagName(key2)[0].getAttribute("data")
+ lst.append(vals)
+ return retval
+
+schema = {"forecast_information": ["city"],
+ "current_conditions": ["condition", "humidity", "icon", "temp_c", "temp_f", "wind_condition"],
+ "forecast_conditions": ["condition", "day_of_week", "low", "icon", "high"],
+ }
+
+url = Google_weather_url + urlencode({"weather": location})
+
+weather_xml = urlopen(url).read()
+
+dom = xml.dom.minidom.parseString(weather_xml)
+
+weather_dict = xml2dict(dom, schema)
+
+html = title_template % weather_dict["forecast_information"][0] + cur_template % weather_dict["current_conditions"][0]
+for fcst in weather_dict["forecast_conditions"]:
+ html += fcst_template % fcst
+
+html += "<em>(Using the Google Weather API)</em>"
+
+if not Lterm_cookie or options.text:
+ import lxml.html
+ sys.stdout.write(lxml.html.fromstring(html).text_content())
+else:
+ sys.stdout.write(wrap(json.dumps(headers)+"\r\n\r\n"+html))
+
10 graphterm/bin/port_forward
@@ -0,0 +1,10 @@
+#!/bin/bash
+# Forward specified port to 8900
+#
+
+if [ $# -ne 1 ]; then
+ echo "Usage: port_forward <80|443>"
+ exit 1
+fi
+
+iptables -t nat -A PREROUTING -p tcp --dport $1 -j REDIRECT --to 8900
17 graphterm/certs/cert_to_p12.csh
@@ -0,0 +1,17 @@
+#!/bin/csh
+
+if ( $#argv < 2 ) then
+ echo "Usage: cert_to_p12.csh <certfile> <keyfile> <password>"
+ exit 1
+endif
+
+set password=password
+if ( $# > 2 ) then
+ set password=$3
+endif
+
+set name=$1:t
+set name=$name:r
+
+echo openssl pkcs12 -export -in $1 -inkey $2 -out $name.p12 -passout pass:$password
+openssl pkcs12 -export -in $1 -inkey $2 -out $name.p12 -passout pass:$password
38 graphterm/certs/create_client_cert.csh
@@ -0,0 +1,38 @@
+#!/bin/csh
+
+if ( $#argv < 2 ) then
+ echo "Usage: create_client_cert.csh <certfile> <clientname> [<clientorg> [<passwd>]]"
+ exit 1
+endif
+
+set clientorg=GraphTerm
+if ( $# > 2 ) then
+ set clientorg="$3"
+endif
+
+set password=""
+if ( $# > 3 ) then
+ set password="$4"
+endif
+
+set certfile=$1
+set certprefix=$certfile:r
+
+set clientname=$2
+set clientprefix="${certprefix:t}-$clientname"
+
+set expdays=1024
+
+echo openssl genrsa -out $clientprefix.key 1024
+openssl genrsa -out $clientprefix.key 1024
+
+echo openssl req -new -key $clientprefix.key -out $clientprefix.csr -batch -subj "/O=$clientorg/CN=$clientname"
+openssl req -new -key $clientprefix.key -out $clientprefix.csr -batch -subj "/O=$clientorg/CN=$clientname"
+
+echo openssl x509 -req -days $expdays -in $clientprefix.csr -CA $certprefix.crt -CAkey $certprefix.key -set_serial 01 -out $clientprefix.crt
+openssl x509 -req -days $expdays -in $clientprefix.csr -CA $certprefix.crt -CAkey $certprefix.key -set_serial 01 -out $clientprefix.crt
+
+echo openssl pkcs12 -export -in $clientprefix.crt -inkey $clientprefix.key -out $clientprefix.p12 -passout pass:$password
+openssl pkcs12 -export -in $clientprefix.crt -inkey $clientprefix.key -out $clientprefix.p12 -passout pass:$password
+
+echo "Created $clientprefix.key, $clientprefix.crt, $clientprefix.p12"
34 graphterm/certs/create_server_cert.csh
@@ -0,0 +1,34 @@
+#!/bin/csh
+
+if ( $#argv < 1 ) then
+ echo "Usage: create_server_cert.csh <hostname> [<serverorg> [<passwd>]]"
+ exit 1
+endif
+
+set hostname=$1
+
+set serverorg=GraphTerm
+if ( $# > 1 ) then
+ set serverorg="$3"
+endif
+
+set password=""
+if ( $# > 2 ) then
+ set password="$3"
+endif
+
+set expdays=1024
+
+echo openssl genrsa -out $hostname.key 1024
+openssl genrsa -out $hostname.key 1024
+
+echo openssl req -new -key $hostname.key -out $hostname.csr -batch -subj "/O=$serverorg/CN=$hostname"
+openssl req -new -key $hostname.key -out $hostname.csr -batch -subj "/O=$serverorg/CN=$hostname"
+
+echo openssl x509 -req -days $expdays -in $hostname.csr -signkey $hostname.key -out $hostname.crt
+openssl x509 -req -days $expdays -in $hostname.csr -signkey $hostname.key -out $hostname.crt
+
+echo openssl x509 -noout -fingerprint -in $hostname.crt
+openssl x509 -noout -fingerprint -in $hostname.crt
+
+echo "Created $hostname.key, $hostname.crt"
21 graphterm/certs/mac_import_cert.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+if [ $# -eq 0 ]; then
+ echo "Usage: mac_import_cert.sh <cert_file> [clientname]"
+ exit 1
+fi
+
+certfile=$1
+clientname=gterm-local
+if [ $# -gt 1 ]; then
+ clientname=$2
+fi
+
+keychain=$HOME/Library/Keychains/login.keychain
+
+if security find-certificate -c $clientname; then
+ security delete-certificate -c $clientname
+fi
+
+echo security import $certfile -k $keychain -P password
+security import $certfile -k $keychain -P password
156 graphterm/daemon.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python
+
+# A simple unix/linux daemon in Python
+# by Sander Marechal
+# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
+# (Public domain)
+
+import sys, os, time, atexit
+from signal import SIGTERM
+
+class Daemon(object):
+ """
+ A generic daemon class.
+
+ Usage: subclass the Daemon class and override the run() method
+ """
+ def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
+ self.stdin = stdin
+ self.stdout = stdout
+ self.stderr = stderr
+ self.pidfile = pidfile
+
+ def daemonize(self):
+ """
+ do the UNIX double-fork magic, see Stevens' "Advanced
+ Programming in the UNIX Environment" for details (ISBN 0201563177)
+ http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+ """
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # exit first parent
+ sys.exit(0)
+ except OSError, e:
+ sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # decouple from parent environment
+ os.chdir("/")
+ os.setsid()
+ os.umask(0)
+
+ # do second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # exit from second parent
+ sys.exit(0)
+ except OSError, e:
+ sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # redirect standard file descriptors
+ sys.stdout.flush()
+ sys.stderr.flush()
+ si = file(self.stdin, 'r')
+ so = file(self.stdout, 'a+')
+ se = file(self.stderr, 'a+', 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+
+ # write pidfile
+ atexit.register(self.delpid)
+ pid = str(os.getpid())
+ file(self.pidfile,'w+').write("%s\n" % pid)
+
+ def delpid(self):
+ os.remove(self.pidfile)
+
+ def start(self):
+ """
+ Start the daemon
+ """
+ # Check for a pidfile to see if the daemon already runs
+ try:
+ pf = file(self.pidfile,'r')
+ pid = int(pf.read().strip())
+ pf.close()
+ except IOError:
+ pid = None
+
+ if pid:
+ message = "pidfile %s already exists. Daemon already running?\n"
+ sys.stderr.write(message % self.pidfile)
+ sys.exit(1)
+
+ # Start the daemon
+ self.daemonize()
+ self.run()
+
+ def stop(self):
+ """
+ Stop the daemon
+ """
+ # Get the pid from the pidfile
+ try:
+ pf = file(self.pidfile,'r')
+ pid = int(pf.read().strip())
+ pf.close()
+ except IOError:
+ pid = None
+
+ if not pid:
+ message = "pidfile %s does not exist. Daemon not running?\n"
+ sys.stderr.write(message % self.pidfile)
+ return # not an error in a restart
+
+ # Try killing the daemon process
+ try:
+ while 1:
+ os.kill(pid, SIGTERM)
+ time.sleep(0.1)
+ except OSError, err:
+ err = str(err)
+ if err.find("No such process") > 0:
+ if os.path.exists(self.pidfile):
+ os.remove(self.pidfile)
+ else:
+ print str(err)
+ sys.exit(1)
+
+ def restart(self):
+ """
+ Restart the daemon
+ """
+ self.stop()
+ self.start()
+
+ def run(self):
+ """
+ You should override this method when you subclass Daemon. It will be called after the process has been
+ daemonized by start() or restart().
+ """
+
+class ServerDaemon(Daemon):
+ def __init__(self, pidfile, run_function):
+ self.run_function = run_function
+ super(ServerDaemon, self).__init__(pidfile)
+
+ def run(self):
+ self.run_function()
+
+ def daemon_run(self, action):
+ if action == "start":
+ self.start()
+ elif action == "stop":
+ self.stop()
+ elif action == "restart":
+ self.restart()
+ elif action == "status":
+ status = "Running" if os.path.exists(self.pidfile) else "Stopped"
+ print >> sys.stderr, status
+ else:
+ print >> sys.stderr, "Daemon option must be one of start/stop/restart/status - ", action
+ sys.exit(1)
59 graphterm/episode4.txt
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Episode IV
+ A NEW HOPE
+
+It is a period of civil war. Rebel
+spaceships, striking from a
+hidden base, have won their
+first victory against the evil
+Galactic Empire.
+
+During the battle, Rebel spies
+managed to steal secret plans to
+the Empire's ultimate weapon,
+the DEATH STAR, an armored
+space station with enough
+power to destroy an entire
+planet.
+
+Pursued by the Empire's sinister
+agents, Princess Leia races
+home aboard her starship,
+custodian of the stolen plans
+that can save her people and
+restore freedom to the galaxy....
+
+
165 graphterm/gterm.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python
+
+"""gterm: GraphTerm client launcher
+"""
+
+import hashlib
+import hmac
+import logging
+import os
+import Queue
+import random
+import subprocess
+import sys
+import threading
+
+import tornado.httpclient
+
+Http_addr = "localhost"
+Http_port = 8900
+
+App_dir = os.path.join(os.getenv("HOME"), ".graphterm")
+Gterm_secret_file = os.path.join(App_dir, "graphterm_secret")
+
+def command_output(command_args, **kwargs):
+ """ Executes a command and returns the string tuple (stdout, stderr)
+ keyword argument timeout can be specified to time out command (defaults to 1 sec)
+ """
+ timeout = kwargs.pop("timeout", 1)
+ def command_output_aux():
+ try:
+ proc = subprocess.Popen(command_args, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ return proc.communicate()
+ except Exception, excp:
+ return "", str(excp)
+ if not timeout:
+ return command_output_aux()
+
+ exec_queue = Queue.Queue()
+ def execute_in_thread():
+ exec_queue.put(command_output_aux())
+ thrd = threading.Thread(target=execute_in_thread)
+ thrd.start()
+ try:
+ return exec_queue.get(block=True, timeout=timeout)
+ except Queue.Empty:
+ return "", "Timed out after %s seconds" % timeout
+
+def getuid(pid):
+ """Return uid of running process"""
+ command_args = ["lsof", "-a", "-p", str(pid), "-d", "cwd", "-Fu"]
+ std_out, std_err = command_output(command_args, timeout=1)
+ if std_err:
+ logging.warning("getuid: ERROR %s", std_err)
+ return None
+ try:
+ return int(std_out.split("\n")[1][1:])
+ except Exception, excp:
+ logging.warning("getuid: ERROR %s", excp)
+ return None
+
+def auth_request(http_addr, http_port, nonce, timeout=None, client_auth=False, protocol="http"):
+ """Simulate user form submission by executing a HTTP request"""
+
+ cert_dir = App_dir
+ server_name = "localhost"
+ client_prefix = server_name + "-gterm-local"
+ ca_certs = cert_dir+"/"+server_name+".crt"
+
+ ssl_options = {}
+ if client_auth:
+ client_cert = cert_dir+"/"+client_prefix+".crt"
+ client_key = cert_dir+"/"+client_prefix+".key"
+ ssl_options.update(client_cert=client_cert, client_key=client_key)
+
+ url = "%s://%s:%s/_auth/?nonce=%s" % (protocol, http_addr, http_port, nonce)
+ request = tornado.httpclient.HTTPRequest(url, validate_cert=True, ca_certs=ca_certs,
+ **ssl_options)
+ http_client = tornado.httpclient.HTTPClient()
+ try:
+ response = http_client.fetch(request)
+ if response.error:
+ print >> sys.stderr, "HTTPClient ERROR response.error ", response.error
+ return None
+ return response.body
+ except tornado.httpclient.HTTPError, excp:
+ print >> sys.stderr, "HTTPClient ERROR ", excp
+ return None
+
+def auth_token(secret, connection_id, client_nonce, server_nonce):
+ """Return (client_token, server_token)"""
+ SIGN_SEP = "|"
+ prefix = SIGN_SEP.join([connection_id, client_nonce, server_nonce]) + SIGN_SEP
+ return [hmac.new(str(secret), prefix+conn_type, digestmod=hashlib.sha256).hexdigest()[:24] for conn_type in ("client", "server")]
+
+def main():
+ global Http_addr, Http_port
+ from optparse import OptionParser
+ usage = "usage: gterm [-h ... options]"
+ parser = OptionParser(usage=usage)
+
+ parser.add_option("", "--https", dest="https", action="store_true",
+ help="Use SSL (TLS) connections for security")
+ parser.add_option("", "--server_auth", dest="server_auth", action="store_true",
+ help="Authenticate server before opening gterm window")
+ parser.add_option("", "--client_cert", dest="client_cert", default="",
+ help="Path to client CA cert (or '.')")
+ parser.add_option("", "--term_type", dest="term_type", default="",
+ help="Terminal type (linux/screen/xterm) NOT YET IMPLEMENTED")
+
+ (options, args) = parser.parse_args()
+ protocol = "https" if options.https else "http"
+
+ if options.server_auth:
+ if not os.path.exists(Gterm_secret_file):
+ print >> sys.stderr, "gterm: Server not running (no secret file); use 'gtermserver' command to start it."
+ sys.exit(1)
+
+ try:
+ with open(Gterm_secret_file) as f:
+ Http_port, Gterm_pid, Gterm_secret = f.read().split()
+ Http_port = int(Http_port)
+ Gterm_pid = int(Gterm_pid)
+ except Exception, excp:
+ print >> sys.stderr, "gterm: Error in reading %s: %s" % (Gterm_secret_file, excp)
+ sys.exit(1)
+
+ if os.getuid() != getuid(Gterm_pid):
+ print >> sys.stderr, "gterm: Server not running (invalid pid); use 'gtermserver' command to start it."
+ sys.exit(1)
+
+ client_nonce = "1%018d" % random.randrange(0, 10**18) # 1 prefix to keep leading zeros when stringified
+
+ resp = auth_request(Http_addr, Http_port, client_nonce, protocol=protocol)
+ if not resp:
+ print >> sys.stderr, "gterm: Auth HTTTP Request failed"
+ sys.exit(1)
+
+ server_nonce, received_token = resp.split(":")
+ client_token, server_token = auth_token(Gterm_secret, "graphterm", client_nonce, server_nonce)
+ if received_token != client_token:
+ print >> sys.stderr, "gterm: Server failed to authenticate itself"
+ sys.exit(1)
+
+ # TODO: Send server token to server in URL to authenticate
+ ##print >> sys.stderr, "**********snonce", server_nonce, client_token, server_token
+
+ url = "%s://%s:%d" % (protocol, Http_addr, Http_port)
+ if sys.platform.startswith("linux"):
+ command_args = ["xdg-open", url]
+ else:
+ command_args = ["open", url]
+
+ std_out, std_err = command_output(command_args, timeout=5)
+ if std_err:
+ print >> sys.stderr, "gterm: ERROR in opening browser window '%s' - %s\n Check if server is running. If not, start it with 'gtermserver' command." % (" ".join(command_args), std_err)
+ sys.exit(1)
+
+ # TODO: Create minimal browser window (without URL box etc.)
+ # by searching directly for browser executables, or using open, xdg-open, or gnome-open
+ # For security, closing websocket should close out (or at least about:blank) the terminal window
+ # (to prevent reconnecting to malicious server)
+
+if __name__ == "__main__":
+ main()
19 graphterm/gterm_setup.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import os, sys
+
+BINDIR = "bin"
+Exec_path = os.path.join(os.path.dirname(__file__), BINDIR)
+
+def setup_bindir():
+ print >> sys.stderr, "Configuring", Exec_path
+ for dirpath, dirnames, filenames in os.walk(Exec_path):
+ for filename in filenames:
+ if not filename.startswith(".") and not filename.endswith(".pyc"):
+ os.chmod(os.path.join(dirpath, filename), 0555)
+
+def main():
+ setup_bindir()
+
+if __name__ == "__main__":
+ main()
387 graphterm/gtermhost.py
@@ -0,0 +1,387 @@
+#!/usr/bin/env python
+
+"""gtermhost: GraphTerm host connector
+"""
+
+import cgi
+import functools
+import logging
+import otrace
+import os
+import random
+import signal
+import sys
+import threading
+import time
+import urllib
+
+import lineterm
+import packetserver
+
+RETRY_SEC = 15
+
+OSHELL_NAME = "osh"
+
+##SHELL_CMD = "bash -l"
+SHELL_CMD = "/bin/bash -l"
+
+# Short prompt (long prompt with directory metadata fills most of row)
+##PROMPT_PREFIX = '<gtprompt/>' # Unique prompt prefix
+PROMPT_PREFIX = '' # No unique prefix necessary for bash (using PROMPT_COMMAND)
+PROMPT_SUFFIX = '$'
+SHELL_PROMPT = [PROMPT_PREFIX, '\W', PROMPT_SUFFIX]
+
+HTML_ESCAPES = ["\x1b[?1155;", "h",
+ "\x1b[?1155l"]
+
+class HtmlWrapper(object):
+ """ Wrapper for HTML output
+ """
+ def __init__(self, lterm_cookie):
+ self.lterm_cookie = lterm_cookie
+
+ def wrap(self, html, msg_type=""):
+ return HTML_ESCAPES[0] + self.lterm_cookie + HTML_ESCAPES[1] + html + HTML_ESCAPES[-1]
+
+class TerminalClient(packetserver.RPCLink, packetserver.PacketClient):
+ _all_connections = {}
+ first_terminal = [True]
+ def __init__(self, host, port, command=SHELL_CMD, lterm_cookie="", io_loop=None, ssl_options={},
+ term_type="", lterm_logfile=""):
+ super(TerminalClient, self).__init__(host, port, io_loop=io_loop,
+ ssl_options=ssl_options, max_packet_buf=3,
+ reconnect_sec=RETRY_SEC, server_type="frame")
+ self.term_type = term_type
+ self.lterm_cookie = lterm_cookie
+ self.lterm_logfile = lterm_logfile
+ self.command = command
+ self.terms = {}
+ self.lineterm = None
+
+ def shutdown(self):
+ print >> sys.stderr, "Shutting down client connection %s -> %s:%s" % (self.connection_id, self.host, self.port)
+ if self.lineterm:
+ self.lineterm.shutdown()
+ self.lineterm = None
+ super(TerminalClient, self).shutdown()
+
+ def add_oshell(self):
+ self.add_term(OSHELL_NAME, 0, 0)
+
+ def add_term(self, term_name, height, width):
+ if term_name not in self.terms:
+ self.send_request_threadsafe("terminal_update", term_name, True)
+ self.terms[term_name] = (height, width)
+
+ def remove_term(self, term_name):
+ try:
+ del self.terms[term_name]
+ except Exception:
+ pass
+ self.send_request_threadsafe("terminal_update", term_name, False)
+
+ if not self.lineterm:
+ return
+ if not term_name:
+ self.lineterm.kill_all()
+ else:
+ self.lineterm.kill_term(term_name)
+
+ def xterm(self, term_name="", height=25, width=80, command=SHELL_CMD):
+ self.first_terminal[0] = False
+ if not self.lineterm:
+ self.lineterm = lineterm.Multiplex(self.screen_callback, command=command,
+ cookie=self.lterm_cookie, prompt=SHELL_PROMPT,
+ term_type=self.term_type, logfile=self.lterm_logfile)
+ term_name = self.lineterm.terminal(term_name, height=height, width=width)
+ self.add_term(term_name, height, width)
+ return term_name
+
+ def screen_callback(self, term_name, command, arg):
+ # Invoked in lineterm thread; schedule callback in ioloop
+ self.send_request_threadsafe("response", term_name, [["terminal", command, arg]])
+
+ def remote_response(self, term_name, message_list):
+ self.send_request_threadsafe("response", term_name, message_list)
+
+ def remote_request(self, term_name, req_list):
+ """
+ Setup commands:
+ reconnect
+
+ Input commands:
+ incomplete_input <line>
+ input <line>
+ open_terminal <name> <command>
+ click_paste <text> <file_uri> {command:, clear_last:, normalize:, enter:}
+ get_finder <kind> <directory>
+ save_file <filepath> <filedata>
+
+ Output commands:
+ completed_input <line>
+ prompt <str>
+ stdin <str>
+ stdout <str>
+ stderr <str>
+ """
+ try:
+ resp_list = []
+ for cmd in req_list:
+ action = cmd.pop(0)
+
+ if action == "reconnect":
+ if self.lineterm:
+ self.lineterm.reconnect(term_name)
+
+ elif action == "open_terminal":
+ if self.lineterm:
+ name = self.lineterm.terminal(cmd[0][0], command=cmd[0][1])
+ self.remote_response(term_name, [["open", name, ""]])
+
+ elif action == "set_size":
+ if term_name != OSHELL_NAME:
+ self.xterm(term_name, cmd[0][0], cmd[0][1])
+
+ elif action == "kill_term":
+ self.remove_term(term_name)
+
+ elif action == "keypress":
+ if self.lineterm:
+ self.lineterm.term_write(term_name, str(cmd[0]))
+
+ elif action == "save_file":
+ if self.lineterm:
+ self.lineterm.save_file(term_name, cmd[0], cmd[1])
+
+ elif action == "click_paste":
+ # click_paste: text, file_uri, {command:, clear_last:, normalize:, enter:}
+ if self.lineterm:
+ self.lineterm.click_paste(term_name, cmd[0], cmd[1], cmd[2])
+
+ elif action == "clear_last_entry":
+ if self.lineterm:
+ self.lineterm.clear_last_entry(term_name, long(cmd[0]))
+
+ elif action == "get_finder":
+ if self.lineterm:
+ self.lineterm.get_finder(term_name, cmd[0], cmd[1])
+
+ elif action == "incomplete_input":
+ cmd_incomplete = str(cmd[0])
+ dummy, sep, text = cmd_incomplete.rpartition(" ")
+ options = otrace.OShell.instance.completer(text, 0, line=cmd_incomplete, all=True)
+ if text:
+ options = [cmd_incomplete[:-len(text)]+option for option in options]
+ else:
+ options = [cmd_incomplete+option for option in options]
+ resp_list.append(["completed_input", options]) # Not escaped; handle as text
+
+ elif action == "input":
+ cmd_input = str(cmd[0]).lstrip() # Unescaped text
+ here_doc = cmd[1]
+ entry_list = []
+
+ if cmd_input == "cat episode4.txt":
+ # Easter egg
+ std_out, std_err = Episode4, ""
+ else:
+ std_out, std_err = otrace.OShell.instance.execute(cmd_input, here_doc=here_doc)
+ resp_list.append(["input", cmd[0]]) # Not escaped; handle as text
+
+ prompt, cur_dir_path = otrace.OShell.instance.get_prompt()
+ resp_list.append(["prompt", cgi.escape(prompt), "file://"+urllib.quote(cur_dir_path)])
+
+ auth_html = False
+ if self.lterm_cookie and std_out.startswith(HTML_ESCAPES[0]):
+ auth_prefix = HTML_ESCAPES[0]+self.lterm_cookie+HTML_ESCAPES[1]
+ auth_html = std_out.startswith(auth_prefix)
+ if auth_html:
+ offset = len(auth_prefix)
+ else:
+ # Unauthenticated html
+ offset = std_out.find(HTML_ESCAPES[1])+len(HTML_ESCAPES[1])
+
+ if std_out.endswith(HTML_ESCAPES[-1]):
+ html_output = std_out[offset:-len(HTML_ESCAPES[-1])]
+ elif std_out.endswith(HTML_ESCAPES[-1]+"\n"):
+ html_output = std_out[offset:-len(HTML_ESCAPES[-1])-1]
+ else:
+ html_output = std_out[offset:]
+
+ headers, content = lineterm.parse_headers(html_output)
+
+ if auth_html:
+ resp_list.append(["html_output", content])
+ else:
+ # Unauthenticated; extract plain text from html
+ try:
+ import lxml.html
+ std_out = lxml.html.fromstring(content).text_content()
+ except Exception:
+ std_out = content
+
+ if not auth_html:
+ entry_list.append('<pre class="output">')
+ if std_out and std_out != "_NoPrompt_":
+ entry_list.append('<span class="stdout">%s</span>' % cgi.escape(std_out))
+ if std_err:
+ entry_list.append('<span class="stderr">%s</span>' % cgi.escape(std_err))
+ entry_list.append('</pre>')
+ resp_list.append(["output", "\n".join(entry_list)])
+
+ elif action == "errmsg":
+ logging.warning("remote_request: ERROR %s", cmd[0])
+ else:
+ raise Exception("Invalid action: "+action)
+ self.remote_response(term_name, resp_list);
+ except Exception, excp:
+ import traceback
+ errmsg = "%s\n%s" % (excp, traceback.format_exc())
+ print >> sys.stderr, "TerminalClient.remote_request: "+errmsg
+ self.remote_response(term_name, [["errmsg", errmsg]])
+ ##self.shutdown()
+
+
+class GTCallbackMixin(object):
+ """ GT callback implementation
+ """
+ oshell_client = None
+ def set_client(self, oshell_client):
+ self.oshell_client = oshell_client
+
+ def logmessage(self, log_level, msg, exc_info=None, logtype="", plaintext=""):
+ # If log_level is None, always display message
+ if self.oshell_client and (log_level is None or log_level >= self.log_level):
+ self.oshell_client.remote_response(OSHELL_NAME, [["log", logtype, log_level, msg]])
+
+ if logtype or log_level is None:
+ sys.stderr.write((plaintext or msg)+"\n")
+
+ def editback(self, content, filepath="", filetype="", editor="", modify=False):
+ if editor and editor != "web":
+ return otrace.TraceCallback.editback(self, content, filepath=filepath, filetype=filetype,
+ editor=editor, modify=modify)
+ params = {"editor": editor, "modify": modify, "command": "edit -f "+filepath if modify else "",
+ "filepath": filepath, "filetype": filetype}
+ self.oshell_client.remote_response(OSHELL_NAME, [["edit", params, content]])
+ return (None, None)
+
+if otrace:
+ class GTCallback(GTCallbackMixin, otrace.TraceCallback):
+ pass
+else:
+ class GTCallback(GTCallbackMixin):
+ pass
+
+Lterm_cookie = None
+def gterm_shutdown(trace_shell=None):
+ TerminalClient.shutdown_all()
+ if trace_shell:
+ trace_shell.close()
+
+def gterm_connect(host_name, server_addr, server_port=8899, shell_cmd=SHELL_CMD, connect_kw={},
+ oshell_globals=None, oshell_unsafe=False, oshell_workdir="", oshell_init=""):
+ """ Returns (host_connection, lterm_cookie, trace_shell)
+ """
+ lterm_cookie = "1%015d" % random.randrange(0, 10**15) # 1 prefix to keep leading zeros when stringified
+
+ host_connection = TerminalClient.get_client(host_name,
+ connect=(server_addr, server_port, shell_cmd, lterm_cookie),
+ connect_kw=connect_kw)
+
+ if oshell_globals:
+ host_connection.add_oshell()
+ gterm_callback = GTCallback()
+ gterm_callback.set_client(host_connection)
+ otrace.OTrace.setup(callback_handler=gterm_callback)
+ otrace.OTrace.html_wrapper = HtmlWrapper(lterm_cookie)
+ trace_shell = otrace.OShell(locals_dict=oshell_globals, globals_dict=oshell_globals,
+ allow_unsafe=oshell_unsafe, work_dir=oshell_workdir,
+ add_env={"GRAPHTERM_COOKIE": lterm_cookie}, init_file=oshell_init)
+ else:
+ trace_shell = None
+
+ return (host_connection, lterm_cookie, trace_shell)
+
+def run_host(options, args):
+ global IO_loop, Gterm_host, Lterm_cookie, Trace_shell, Xterm, Killterm
+ import tornado.ioloop
+ server_addr = args[0]
+ host_name = args[1]
+ protocol = "https" if options.https else "http"
+
+ oshell_globals = globals() if options.oshell else None
+ Gterm_host, Lterm_cookie, Trace_shell = gterm_connect(host_name, server_addr,
+ server_port=options.server_port,
+ oshell_globals=oshell_globals,
+ oshell_unsafe=True)
+ Xterm = Gterm_host.xterm
+ Killterm = Gterm_host.remove_term
+
+ def host_shutdown():
+ gterm_shutdown(Trace_shell)
+ IO_loop.stop()
+
+ def sigterm(signal, frame):
+ logging.warning("SIGTERM signal received")
+ host_shutdown()
+ signal.signal(signal.SIGTERM, sigterm)
+
+ IO_loop = tornado.ioloop.IOLoop.instance()
+ try:
+ if not Trace_shell:
+ IO_loop.start()
+ else:
+ ioloop_thread = threading.Thread(target=IO_loop.start)
+ ioloop_thread.start()
+ time.sleep(1) # Time to start thread
+
+ print >> sys.stderr, "\nType ^D^C to exit"
+ Trace_shell.loop()
+ except KeyboardInterrupt:
+ print >> sys.stderr, "Interrupted"
+
+ finally:
+ try:
+ pass
+ except Exception:
+ pass
+
+ if Trace_shell:
+ IO_loop.add_callback(host_shutdown)
+ else:
+ host_shutdown()
+
+def main():
+ from optparse import OptionParser
+ usage = "usage: gtermhost [-h ... options] <serveraddr> <hostname>"
+ parser = OptionParser(usage=usage)
+
+ parser.add_option("", "--server_port", dest="server_port", default=8899,
+ help="server port (default: 8899)", type="int")
+
+ parser.add_option("", "--oshell", dest="oshell", action="store_true",
+ help="Activate otrace/oshell")
+
+ parser.add_option("", "--https", dest="https", action="store_true",
+ help="Use SSL (TLS) connections for security")
+
+
+ parser.add_option("", "--daemon", dest="daemon", default="",
+ help="daemon=start/stop/restart/status")
+
+ (options, args) = parser.parse_args()
+ if len(args) != 2 and options.daemon != "stop":
+ print >> sys.stderr, usage
+ sys.exit(1)
+
+ if not options.daemon:
+ run_host(options, args)
+ else:
+ from daemon import ServerDaemon
+ pidfile = "/tmp/gtermhost.pid"
+ daemon = ServerDaemon(pidfile, functools.partial(run_host, options, args))
+ daemon.daemon_run(options.daemon)
+
+if __name__ == "__main__":
+ main()
723 graphterm/gtermserver.py
@@ -0,0 +1,723 @@
+#!/usr/bin/env python
+
+"""gtermserver: WebSocket server for GraphTerm
+"""
+
+import cgi
+import collections
+import functools
+import hashlib
+import hmac
+import json
+import logging
+import os
+import Queue
+import random
+import shlex
+import ssl
+import stat
+import subprocess
+import sys
+import threading
+import time
+import traceback
+import urllib
+import urlparse
+import uuid
+
+try:
+ import otrace
+except ImportError:
+ otrace = None
+
+import gtermhost
+import lineterm
+import packetserver
+
+import tornado.httpserver
+import tornado.ioloop
+import tornado.web
+import tornado.websocket
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ from ordereddict import OrderedDict
+
+App_dir = os.path.join(os.getenv("HOME"), ".graphterm")
+File_dir = os.path.dirname(__file__)
+if File_dir == ".":
+ File_dir = os.getcwd() # Need this for daemonizing to work?
+
+Doc_rootdir = os.path.join(File_dir, "www")
+Default_auth_file = os.path.join(App_dir, "graphterm_auth")
+Gterm_secret_file = os.path.join(App_dir, "graphterm_secret")
+
+Gterm_secret = "1%018d" % random.randrange(0, 10**18) # 1 prefix to keep leading zeros when stringified
+
+MAX_COOKIE_STATES = 100
+MAX_WEBCASTS = 500
+
+COOKIE_NAME = "GRAPHTERM_AUTH"
+COOKIE_TIMEOUT = 10800
+
+HEX_DIGITS = 16
+
+PROTOCOL = "http"
+
+SUPER_USERS = set(["root"])