diff options
| author | root <root@tonbuntu> | 2011-01-12 09:24:57 +0000 |
|---|---|---|
| committer | Tarmac <> | 2011-01-12 09:24:57 +0000 |
| commit | 76fdd667f2efe7e2dc710fe0254437d176efb45c (patch) | |
| tree | dc3640acddec70efd4fef418a298ca53a1e6aa55 | |
| parent | 78882d496b94915b8a6e2f2edce13e8129299982 (diff) | |
| parent | 7cfca5208766539ae368a9f0b8daba6103041f7f (diff) | |
| download | nova-76fdd667f2efe7e2dc710fe0254437d176efb45c.tar.gz nova-76fdd667f2efe7e2dc710fe0254437d176efb45c.tar.xz nova-76fdd667f2efe7e2dc710fe0254437d176efb45c.zip | |
This branch adds web based serial console access. Here is an overview of how it works (for libvirt):
1. User requests an ajax console for an instance_id (either through OS api, or tools/euca-get-ajax-console)
a. api server calls compute worker to complete request
b. compute worker parses an instance's xml to locate its pseudo terminal (/dev/pts/x)
c. compute worker spawns an ajaxterm daemon, bound to a random port in a specified range. socat is used to connect to /dev/pts/x. Note that ajaxterm was modified in the following ways:
i. dies after 5 minutes of inactivity
ii. now requires token authentication. Previously it was trivial to hijack an ajaxterm
d. compute worker returns ajaxterm connect information to the api server: port, host, token
e. api server casts connect information to the nova-ajax-console-proxy (a new service)
f. api server returns a url for the ajaxterm (eg. http://nova-ajax-console-proxy/?token=123)
2. User now has a url, and can paste it in a browser
a. Browser sends request to https://nova-ajax-console-proxy/?token=123
b. nova-ajax-console-proxy maps token to connect information
c. nova-ajax-console-proxy constructs a proxy to the ajaxterm that is running on the host machine. This is now done with eventlet, though previously it was done using twisted
3. User interacts with console through web browser
NOTE: For this to work as expected, serial console login must be enabled in the instance. Instructions for how to do this on ubuntu can be found here: https://help.ubuntu.com/community/SerialConsoleHowto. Note that you must actively log out of the serial console when you are finished, otherwise the console will remain open even after the ajaxterm term session has ended.
Also note that nova.sh has been modified in this branch to launch nova-ajax-console-proxy.
31 files changed, 3857 insertions, 2 deletions
diff --git a/bin/nova-ajax-console-proxy b/bin/nova-ajax-console-proxy new file mode 100755 index 000000000..2bc407658 --- /dev/null +++ b/bin/nova-ajax-console-proxy @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Ajax Console Proxy Server""" + +from eventlet import greenthread +from eventlet.green import urllib2 + +import exceptions +import gettext +import logging +import os +import sys +import time +import urlparse + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +gettext.install('nova', unicode=1) + +from nova import flags +from nova import log as logging +from nova import rpc +from nova import utils +from nova import wsgi + +FLAGS = flags.FLAGS + +flags.DEFINE_integer('ajax_console_idle_timeout', 300, + 'Seconds before idle connection destroyed') + +LOG = logging.getLogger('nova.ajax_console_proxy') +LOG.setLevel(logging.DEBUG) +LOG.addHandler(logging.StreamHandler()) + + +class AjaxConsoleProxy(object): + tokens = {} + + def __call__(self, env, start_response): + try: + req_url = '%s://%s%s?%s' % (env['wsgi.url_scheme'], + env['HTTP_HOST'], + env['PATH_INFO'], + env['QUERY_STRING']) + if 'HTTP_REFERER' in env: + auth_url = env['HTTP_REFERER'] + else: + auth_url = req_url + + auth_params = urlparse.parse_qs(urlparse.urlparse(auth_url).query) + parsed_url = urlparse.urlparse(req_url) + + auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]] + args = auth_info['args'] + auth_info['last_activity'] = time.time() + + remote_url = ("http://%s:%s%s?token=%s" % ( + str(args['host']), + str(args['port']), + parsed_url.path, + str(args['token']))) + + opener = urllib2.urlopen(remote_url, env['wsgi.input'].read()) + body = opener.read() + info = opener.info() + + start_response("200 OK", info.dict.items()) + return body + except (exceptions.KeyError): + if env['PATH_INFO'] != '/favicon.ico': + LOG.audit("Unauthorized request %s, %s" + % (req_url, str(env))) + start_response("401 NOT AUTHORIZED", []) + return "Not Authorized" + except Exception: + start_response("500 ERROR", []) + return "Server Error" + + def register_listeners(self): + class Callback: + def __call__(self, data, message): + if data['method'] == 'authorize_ajax_console': + AjaxConsoleProxy.tokens[data['args']['token']] = \ + {'args': data['args'], 'last_activity': time.time()} + + conn = rpc.Connection.instance(new=True) + consumer = rpc.TopicConsumer( + connection=conn, + topic=FLAGS.ajax_console_proxy_topic) + consumer.register_callback(Callback()) + + def delete_expired_tokens(): + now = time.time() + to_delete = [] + for k, v in AjaxConsoleProxy.tokens.items(): + if now - v['last_activity'] > FLAGS.ajax_console_idle_timeout: + to_delete.append(k) + + for k in to_delete: + del AjaxConsoleProxy.tokens[k] + + utils.LoopingCall(consumer.fetch, auto_ack=True, + enable_callbacks=True).start(0.1) + utils.LoopingCall(delete_expired_tokens).start(1) + +if __name__ == '__main__': + utils.default_flagfile() + FLAGS(sys.argv) + server = wsgi.Server() + acp = AjaxConsoleProxy() + acp.register_listeners() + server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0') + server.wait() diff --git a/contrib/nova.sh b/contrib/nova.sh index da1ba030c..e06706295 100755 --- a/contrib/nova.sh +++ b/contrib/nova.sh @@ -78,6 +78,7 @@ if [ "$CMD" == "install" ]; then sudo apt-get install -y user-mode-linux kvm libvirt-bin sudo apt-get install -y screen euca2ools vlan curl rabbitmq-server sudo apt-get install -y lvm2 iscsitarget open-iscsi + sudo apt-get install -y socat echo "ISCSITARGET_ENABLE=true" | sudo tee /etc/default/iscsitarget sudo /etc/init.d/iscsitarget restart sudo modprobe kvm @@ -155,6 +156,7 @@ if [ "$CMD" == "run" ]; then screen_it network "$NOVA_DIR/bin/nova-network" screen_it scheduler "$NOVA_DIR/bin/nova-scheduler" screen_it volume "$NOVA_DIR/bin/nova-volume" + screen_it ajax_console_proxy "$NOVA_DIR/bin/nova-ajax-console-proxy" screen_it test ". $NOVA_DIR/novarc" screen -S nova -x fi diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 135836348..39174d554 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -501,6 +501,11 @@ class CloudController(object): "Timestamp": now, "output": base64.b64encode(output)} + def get_ajax_console(self, context, instance_id, **kwargs): + ec2_id = instance_id[0] + internal_id = ec2_id_to_id(ec2_id) + return self.compute_api.get_ajax_console(context, internal_id) + def describe_volumes(self, context, volume_id=None, **kwargs): volumes = self.volume_api.get_all(context) # NOTE(vish): volume_id is an optional list of volume ids to filter by. diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index c8a9947f3..29af82533 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -283,6 +283,15 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPUnprocessableEntity()) return exc.HTTPAccepted() + def get_ajax_console(self, req, id): + """ Returns a url to an instance's ajaxterm console. """ + try: + self.compute_api.get_ajax_console(req.environ['nova.context'], + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" ctxt = req.environ["nova.context"] diff --git a/nova/compute/api.py b/nova/compute/api.py index fbe3c6e75..56402c11b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -414,7 +414,26 @@ class API(base.Base): rpc.cast(context, self.db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "unrescue_instance", - "args": {"instance_id": instance_id}}) + "args": {"instance_id": instance['id']}}) + + def get_ajax_console(self, context, instance_id): + """Get a url to an AJAX Console""" + + instance = self.get(context, instance_id) + + output = rpc.call(context, + '%s.%s' % (FLAGS.compute_topic, + instance['host']), + {'method': 'get_ajax_console', + 'args': {'instance_id': instance['id']}}) + + rpc.cast(context, '%s' % FLAGS.ajax_console_proxy_topic, + {'method': 'authorize_ajax_console', + 'args': {'token': output['token'], 'host': output['host'], + 'port': output['port']}}) + + return {'url': '%s?token=%s' % (FLAGS.ajax_console_proxy_url, + output['token'])} def lock(self, context, instance_id): """ diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 018e0bbbe..6b2fc4adb 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -471,6 +471,14 @@ class ComputeManager(manager.Manager): return self.driver.get_console_output(instance_ref) @exception.wrap_exception + def get_ajax_console(self, context, instance_id): + """Return connection information for an ajax console""" + context = context.elevated() + logging.debug(_("instance %s: getting ajax console"), instance_id) + instance_ref = self.db.instance_get(context, instance_id) + + return self.driver.get_ajax_console(instance_ref) + @checks_instance_lock def attach_volume(self, context, instance_id, volume_id, mountpoint): """Attach a volume to an instance.""" diff --git a/nova/flags.py b/nova/flags.py index 25362f883..76ab2f788 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -234,7 +234,14 @@ DEFINE_string('scheduler_topic', 'scheduler', 'the topic scheduler nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') - +DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy', + 'the topic ajax proxy nodes listen on') +DEFINE_string('ajax_console_proxy_url', + 'http://127.0.0.1:8000', + 'location of ajax console proxy, \ + in the form "http://127.0.0.1:8000"') +DEFINE_string('ajax_console_proxy_port', + 8000, 'port that ajax_console_proxy binds') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py index b8a15c7b2..8e43eec00 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/test_cloud.py @@ -167,6 +167,19 @@ class CloudTestCase(test.TestCase): greenthread.sleep(0.3) rv = self.cloud.terminate_instances(self.context, [instance_id]) + def test_ajax_console(self): + kwargs = {'image_id': image_id} + rv = yield self.cloud.run_instances(self.context, **kwargs) + instance_id = rv['instancesSet'][0]['instanceId'] + output = yield self.cloud.get_console_output(context=self.context, + instance_id=[instance_id]) + self.assertEquals(b64decode(output['output']), + 'http://fakeajaxconsole.com/?token=FAKETOKEN') + # TODO(soren): We need this until we can stop polling in the rpc code + # for unit tests. + greenthread.sleep(0.3) + rv = yield self.cloud.terminate_instances(self.context, [instance_id]) + def test_key_generation(self): result = self._create_key('test') private_key = result['private_key'] diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 1d407c5a3..52660ee74 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -169,6 +169,16 @@ class ComputeTestCase(test.TestCase): self.assert_(console) self.compute.terminate_instance(self.context, instance_id) + def test_ajax_console(self): + """Make sure we can get console output from instance""" + instance_id = self._create_instance() + self.compute.run_instance(self.context, instance_id) + + console = self.compute.get_ajax_console(self.context, + instance_id) + self.assert_(console) + self.compute.terminate_instance(self.context, instance_id) + def test_run_instance_existing(self): """Ensure failure when running an instance that already exists""" instance_id = self._create_instance() diff --git a/nova/utils.py b/nova/utils.py index aadbec532..45adb7b38 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -153,6 +153,11 @@ def abspath(s): return os.path.join(os.path.dirname(__file__), s) +def novadir(): + import nova + return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0] + + def default_flagfile(filename='nova.conf'): for arg in sys.argv: if arg.find('flagfile') != -1: diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 849261f07..9186d885e 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -289,6 +289,9 @@ class FakeConnection(object): def get_console_output(self, instance): return 'FAKE CONSOLE OUTPUT' + def get_ajax_console(self, instance): + return 'http://fakeajaxconsole.com/?token=FAKETOKEN' + def get_console_pool_info(self, console_type): return {'address': '127.0.0.1', 'username': 'fakeuser', diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template index 3fb2243da..2eb7d9488 100644 --- a/nova/virt/libvirt.xml.template +++ b/nova/virt/libvirt.xml.template @@ -71,9 +71,22 @@ #end if </filterref> </interface> + + <!-- The order is significant here. File must be defined first --> <serial type="file"> <source path='${basepath}/console.log'/> <target port='1'/> </serial> + + <console type='pty' tty='/dev/pts/2'> + <source path='/dev/pts/2'/> + <target port='0'/> + </console> + + <serial type='pty'> + <source path='/dev/pts/2'/> + <target port='0'/> + </serial> + </devices> </domain> diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 1bd4d250e..655c55fa1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -38,6 +38,11 @@ Supports KVM, QEMU, UML, and XEN. import os import shutil +import random +import subprocess +import uuid +from xml.dom import minidom + from eventlet import greenthread from eventlet import event @@ -86,6 +91,9 @@ flags.DEFINE_string('libvirt_uri', flags.DEFINE_bool('allow_project_net_traffic', True, 'Whether to allow in project network traffic') +flags.DEFINE_string('ajaxterm_portrange', + '10000-12000', + 'Range of ports that ajaxterm should randomly try to bind') flags.DEFINE_string('firewall_driver', 'nova.virt.libvirt_conn.IptablesFirewallDriver', 'Firewall driver (defaults to iptables)') @@ -433,6 +441,43 @@ class LibvirtConnection(object): return self._dump_file(fpath) + @exception.wrap_exception + def get_ajax_console(self, instance): + def get_open_port(): + start_port, end_port = FLAGS.ajaxterm_portrange.split("-") + for i in xrange(0, 100): # don't loop forever + port = random.randint(int(start_port), int(end_port)) + # netcat will exit with 0 only if the port is in use, + # so a nonzero return value implies it is unused + cmd = 'netcat 0.0.0.0 %s -w 1 </dev/null || echo free' % (port) + stdout, stderr = utils.execute(cmd) + if stdout.strip() == 'free': + return port + raise Exception(_('Unable to find an open port')) + + def get_pty_for_instance(instance_name): + virt_dom = self._conn.lookupByName(instance_name) + xml = virt_dom.XMLDesc(0) + dom = minidom.parseString(xml) + + for serial in dom.getElementsByTagName('serial'): + if serial.getAttribute('type') == 'pty': + source = serial.getElementsByTagName('source')[0] + return source.getAttribute('path') + + port = get_open_port() + token = str(uuid.uuid4()) + host = instance['host'] + + ajaxterm_cmd = 'sudo socat - %s' \ + % get_pty_for_instance(instance['name']) + + cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \ + % (utils.novadir(), ajaxterm_cmd, token, port) + + subprocess.Popen(cmd, shell=True) + return {'token': token, 'host': host, 'port': port} + def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None): # syntactic nicety basepath = lambda fname = '', prefix = prefix: os.path.join( diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index e20930fe8..7e3585991 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -284,6 +284,11 @@ class VMOps(object): # TODO: implement this to fix pylint! return 'FAKE CONSOLE OUTPUT of instance' + def get_ajax_console(self, instance): + """Return link to instance's ajax console""" + # TODO: implement this! + return 'http://fakeajaxconsole/fake_url' + def list_from_xenstore(self, vm, path): """Runs the xenstore-ls command to get a listing of all records from 'path' downward. Returns a dict with the sub-paths as keys, diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 11473ddf8..45d0738a5 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -181,6 +181,10 @@ class XenAPIConnection(object): """Return snapshot of console""" return self._vmops.get_console_output(instance) + def get_ajax_console(self, instance): + """Return link to instance's ajax console""" + return self._vmops.get_ajax_console(instance) + def attach_volume(self, instance_name, device_path, mountpoint): """Attach volume storage to VM instance""" return self._volumeops.attach_volume(instance_name, diff --git a/tools/ajaxterm/README.txt b/tools/ajaxterm/README.txt new file mode 100644 index 000000000..4b0ae99af --- /dev/null +++ b/tools/ajaxterm/README.txt @@ -0,0 +1,120 @@ += [http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm Ajaxterm] =
+
+Ajaxterm is a web based terminal. It was totally inspired and works almost
+exactly like http://anyterm.org/ except it's much easier to install (see
+comparaison with anyterm below).
+
+Ajaxterm written in python (and some AJAX javascript for client side) and depends only on python2.3 or better.[[BR]]
+Ajaxterm is '''very simple to install''' on Linux, MacOS X, FreeBSD, Solaris, cygwin and any Unix that runs python2.3.[[BR]]
+Ajaxterm was written by Antony Lesuisse (email: al AT udev.org), License Public Domain.
+
+Use the [/qweb/forum/viewforum.php?id=2 Forum], if you have any question or remark.
+
+== News ==
+
+ * 2006-10-29: v0.10 allow space in login, cgi launch fix, redhat init
+ * 2006-07-12: v0.9 change uid, daemon fix (Daniel Fischer)
+ * 2006-07-04: v0.8 add login support to ssh (Sven Geggus), change max width to 256
+ * 2006-05-31: v0.7 minor fixes, daemon option
+ * 2006-05-23: v0.6 Applied debian and gentoo patches, renamed to Ajaxterm, default port 8022
+
+== Download and Install ==
+
+ * Release: [/qweb/files/Ajaxterm-0.10.tar.gz Ajaxterm-0.10.tar.gz]
+ * Browse src: [/qweb/trac/browser/trunk/ajaxterm/ ajaxterm/]
+
+To install Ajaxterm issue the following commands:
+{{{
+wget http://antony.lesuisse.org/qweb/files/Ajaxterm-0.10.tar.gz
+tar zxvf Ajaxterm-0.10.tar.gz
+cd Ajaxterm-0.10
+./ajaxterm.py
+}}}
+Then point your browser to this URL : http://localhost:8022/
+
+== Screenshot ==
+
+{{{
+#!html
+<center><img src="/qweb/trac/attachment/wiki/AjaxTerm/scr.png?format=raw" alt="ajaxterm screenshot" style=""/></center>
+}}}
+
+== Documentation and Caveats ==
+
+ * Ajaxterm only support latin1, if you use Ubuntu or any LANG==en_US.UTF-8 distribution don't forget to "unset LANG".
+
+ * If run as root ajaxterm will run /bin/login, otherwise it will run ssh
+ localhost. To use an other command use the -c option.
+
+ * By default Ajaxterm only listen at 127.0.0.1:8022. For remote access, it is
+ strongly recommended to use '''https SSL/TLS''', and that is simple to
+ configure if you use the apache web server using mod_proxy.[[BR]][[BR]]
+ Using ssl will also speed up ajaxterm (probably because of keepalive).[[BR]][[BR]]
+ Here is an configuration example:
+
+{{{
+ Listen 443
+ NameVirtualHost *:443
+
+ <VirtualHost *:443>
+ ServerName localhost
+ SSLEngine On
+ SSLCertificateKeyFile ssl/apache.pem
+ SSLCertificateFile ssl/apache.pem
+
+ ProxyRequests Off
+ <Proxy *>
+ Order deny,allow
+ Allow from all
+ </Proxy>
+ ProxyPass /ajaxterm/ http://localhost:8022/
+ ProxyPassReverse /ajaxterm/ http://localhost:8022/
+ </VirtualHost>
+}}}
+
+ * Using GET HTTP request seems to speed up ajaxterm, just click on GET in the
+ interface, but be warned that your keystrokes might be loggued (by apache or
+ any proxy). I usually enable it after the login.
+
+ * Ajaxterm commandline usage:
+
+{{{
+usage: ajaxterm.py [options]
+
+options:
+ -h, --help show this help message and exit
+ -pPORT, --port=PORT Set the TCP port (default: 8022)
+ -cCMD, --command=CMD set the command (default: /bin/login or ssh localhost)
+ -l, --log log requests to stderr (default: quiet mode)
+ -d, --daemon run as daemon in the background
+ -PPIDFILE, --pidfile=PIDFILE
+ set the pidfile (default: /var/run/ajaxterm.pid)
+ -iINDEX_FILE, --index=INDEX_FILE
+ default index file (default: ajaxterm.html)
+ -uUID, --uid=UID Set the daemon's user id
+}}}
+
+ * Ajaxterm was first written as a demo for qweb (my web framework), but
+ actually doesn't use many features of qweb.
+
+ * Compared to anyterm:
+ * There are no partial updates, ajaxterm updates either all the screen or
+ nothing. That make the code simpler and I also think it's faster. HTTP
+ replies are always gzencoded. When used in 80x25 mode, almost all of
+ them are below the 1500 bytes (size of an ethernet frame) and we just
+ replace the screen with the reply (no javascript string handling).
+ * Ajaxterm polls the server for updates with an exponentially growing
+ timeout when the screen hasn't changed. The timeout is also resetted as
+ soon as a key is pressed. Anyterm blocks on a pending request and use a
+ parallel connection for keypresses. The anyterm approch is better
+ when there aren't any keypress.
+
+ * Ajaxterm files are released in the Public Domain, (except [http://sarissa.sourceforge.net/doc/ sarissa*] which are LGPL).
+
+== TODO ==
+
+ * insert mode ESC [ 4 h
+ * change size x,y from gui (sending signal)
+ * vt102 graphic codepage
+ * use innerHTML or prototype instead of sarissa
+
diff --git a/tools/ajaxterm/ajaxterm.1 b/tools/ajaxterm/ajaxterm.1 new file mode 100644 index 000000000..46f2acb33 --- /dev/null +++ b/tools/ajaxterm/ajaxterm.1 @@ -0,0 +1,35 @@ +.TH ajaxterm "1" "May 2006" "ajaxterm 0.5" "User commands" +.SH NAME +ajaxterm \- Web based terminal written in python + +.SH DESCRITPION +\fBajaxterm\fR is a web based terminal written in python and some AJAX +javascript for client side. +It can use almost any web browser and even works through firewalls. + +.SH USAGE +\fBajaxterm.py\fR [options] + +.SH OPTIONS +A summary of the options supported by \fBajaxterm\fR is included below. + \fB-h, --help\fR show this help message and exit + \fB-pPORT, --port=PORT\fR Set the TCP port (default: 8022) + \fB-cCMD, --command=CMD\fR set the command (default: /bin/login or ssh localhost) + \fB-l, --log\fR log requests to stderr (default: quiet mode) + +.SH AUTHOR +Antony Lesuisse <al@udev.org> + +This manual page was written for the Debian system by +Julien Valroff <julien@kirya.net> (but may be used by others). + +.SH "REPORTING BUGS" +Report any bugs to the author: Antony Lesuisse <al@udev.org> + +.SH COPYRIGHT +Copyright Antony Lesuisse <al@udev.org> + +.SH SEE ALSO +- \fBajaxterm\fR wiki page: http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm +.br +- \fBajaxterm\fR forum: http://antony.lesuisse.org/qweb/forum/viewforum.php?id=2 diff --git a/tools/ajaxterm/ajaxterm.css b/tools/ajaxterm/ajaxterm.css new file mode 100644 index 000000000..b9a5f8771 --- /dev/null +++ b/tools/ajaxterm/ajaxterm.css @@ -0,0 +1,64 @@ +pre.stat { + margin: 0px; + padding: 4px; + display: block; + font-family: monospace; + white-space: pre; + background-color: black; + border-top: 1px solid black; + color: white; +} +pre.stat span { + padding: 0px; +} +pre.stat .on { + background-color: #080; + font-weight: bold; + color: white; + cursor: pointer; +} +pre.stat .off { + background-color: #888; + font-weight: bold; + color: white; + cursor: pointer; +} +pre.term { + margin: 0px; + padding: 4px; + display: block; + font-family: monospace; + white-space: pre; + background-color: black; + border-top: 1px solid white; + color: #eee; +} +pre.term span.f0 { color: #000; } +pre.term span.f1 { color: #b00; } +pre.term span.f2 { color: #0b0; } +pre.term span.f3 { color: #bb0; } +pre.term span.f4 { color: #00b; } +pre.term span.f5 { color: #b0b; } +pre.term span.f6 { color: #0bb; } +pre.term span.f7 { color: #bbb; } +pre.term span.f8 { color: #666; } +pre.term span.f9 { color: #f00; } +pre.term span.f10 { color: #0f0; } +pre.term span.f11 { color: #ff0; } +pre.term span.f12 { color: #00f; } +pre.term span.f13 { color: #f0f; } +pre.term span.f14 { color: #0ff; } +pre.term span.f15 { color: #fff; } +pre.term span.b0 { background-color: #000; } +pre.term span.b1 { background-color: #b00; } +pre.term span.b2 { background-color: #0b0; } +pre.term span.b3 { background-color: #bb0; } +pre.term span.b4 { background-color: #00b; } +pre.term span.b5 { background-color: #b0b; } +pre.term span.b6 { background-color: #0bb; } +pre.term span.b7 { background-color: #bbb; } + +body { background-color: #888; } +#term { + float: left; +} diff --git a/tools/ajaxterm/ajaxterm.html b/tools/ajaxterm/ajaxterm.html new file mode 100644 index 000000000..7fdef5e94 --- /dev/null +++ b/tools/ajaxterm/ajaxterm.html @@ -0,0 +1,25 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> +<head> + <title>Ajaxterm</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> + <link rel="stylesheet" type="text/css" href="ajaxterm.css"/> + <script type="text/javascript" src="sarissa.js"></script> + <script type="text/javascript" src="sarissa_dhtml.js"></script> + <script type="text/javascript" src="ajaxterm.js"></script> + <script type="text/javascript"> + /* + ajaxterm.py creates a random session_id to demultiplex multiple connections, + and to add a layer of security - in its shipping form, ajaxterm accepted any session_id + and was susceptible to an easy exploit + */ + SESSION_ID = '$session_id'; + window.onload=function() { + t=ajaxterm.Terminal("term",80,25); + }; + </script> +</head> +<body> +<div id="term"></div> +</body> +</html> diff --git a/tools/ajaxterm/ajaxterm.js b/tools/ajaxterm/ajaxterm.js new file mode 100644 index 000000000..32b401930 --- /dev/null +++ b/tools/ajaxterm/ajaxterm.js @@ -0,0 +1,279 @@ +ajaxterm={}; +ajaxterm.Terminal_ctor=function(id,width,height) { + var ie=0; + if(window.ActiveXObject) + ie=1; + var sid=""+SESSION_ID; + var query0="s="+sid+"&w="+width+"&h="+height; + var query1=query0+"&c=1&k="; + var buf=""; + var timeout; + var error_timeout; + var keybuf=[]; + var sending=0; + var rmax=1; + + var div=document.getElementById(id); + var dstat=document.createElement('pre'); + var sled=document.createElement('span'); + var opt_get=document.createElement('a'); + var opt_color=document.createElement('a'); + var opt_paste=document.createElement('a'); + var sdebug=document.createElement('span'); + var dterm=document.createElement('div'); + + function debug(s) { + sdebug.innerHTML=s; + } + function error() { + sled.className='off'; + debug("Connection lost timeout ts:"+((new Date).getTime())); + } + function opt_add(opt,name) { + opt.className='off'; + opt.innerHTML=' '+name+' '; + dstat.appendChild(opt); + dstat.appendChild(document.createTextNode(' ')); + } + function do_get(event) { + opt_get.className=(opt_get.className=='off')?'on':'off'; + debug('GET '+opt_get.className); + } + function do_color(event) { + var o=opt_color.className=(opt_color.className=='off')?'on':'off'; + if(o=='on') + query1=query0+"&c=1&k="; + else + query1=query0+"&k="; + debug('Color '+opt_color.className); + } + function mozilla_clipboard() { + // mozilla sucks + try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + } catch (err) { + debug('Access denied, <a href="http://kb.mozillazine.org/Granting_JavaScript_access_to_the_clipboard" target="_blank">more info</a>'); + return undefined; + } + var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard); + var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable); + if (!clip || !trans) { + return undefined; + } + trans.addDataFlavor("text/unicode"); + clip.getData(trans,clip.kGlobalClipboard); + var str=new Object(); + var strLength=new Object(); + try { + trans.getTransferData("text/unicode",str,strLength); + } catch(err) { + return ""; + } + if (str) { + str=str.value.QueryInterface(Components.interfaces.nsISupportsString); + } + if (str) { + return str.data.substring(0,strLength.value / 2); + } else { + return ""; + } + } + function do_paste(event) { + var p=undefined; + if (window.clipboardData) { + p=window.clipboardData.getData("Text"); + } else if(window.netscape) { + p=mozilla_clipboard(); + } + if (p) { + debug('Pasted'); + queue(encodeURIComponent(p)); + } else { + } + } + function update() { +// debug("ts: "+((new Date).getTime())+" rmax:"+rmax); + if(sending==0) { + sending=1; + sled.className='on'; + var r=new XMLHttpRequest(); + var send=""; + while(keybuf.length>0) { + send+=keybuf.pop(); + } + var query=query1+send; + if(opt_get.className=='on') { + r.open("GET","u?"+query,true); + if(ie) { + r.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT"); + } + } else { + r.open("POST","u",true); + } + r.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); + r.onreadystatechange = function () { +// debug("xhr:"+((new Date).getTime())+" state:"+r.readyState+" status:"+r.status+" statusText:"+r.statusText); + if (r.readyState==4) { + if(r.status==200) { + window.clearTimeout(error_timeout); + de=r.responseXML.documentElement; + if(de.tagName=="pre") { + if(ie) { + Sarissa.updateContentFromNode(de, dterm); + } else { + Sarissa.updateContentFromNode(de, dterm); +// old=div.firstChild; +// div.replaceChild(de,old); + } + rmax=100; + } else { + rmax*=2; + if(rmax>2000) + rmax=2000; + } + sending=0; + sled.className='off'; + timeout=window.setTimeout(update,rmax); + } else { + debug("Connection error status:"+r.status); + } + } + } + error_timeout=window.setTimeout(error,5000); + if(opt_get.className=='on') { + r.send(null); + } else { + r.send(query); + } + } + } + function queue(s) { + keybuf.unshift(s); + if(sending==0) { + window.clearTimeout(timeout); + timeout=window.setTimeout(update,1); + } + } + function keypress(ev) { + if (!ev) var ev=window.event; +// s="kp keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey; +// debug(s); +// return false; +// else { if (!ev.ctrlKey || ev.keyCode==17) { return; } + var kc; + var k=""; + if (ev.keyCode) + kc=ev.keyCode; + if (ev.which) + kc=ev.which; + if (ev.altKey) { + if (kc>=65 && kc<=90) + kc+=32; + if (kc>=97 && kc<=122) { + k=String.fromCharCode(27)+String.fromCharCode(kc); + } + } else if (ev.ctrlKey) { + if (kc>=65 && kc<=90) k=String.fromCharCode(kc-64); // Ctrl-A..Z + else if (kc>=97 && kc<=122) k=String.fromCharCode(kc-96); // Ctrl-A..Z + else if (kc==54) k=String.fromCharCode(30); // Ctrl-^ + else if (kc==109) k=String.fromCharCode(31); // Ctrl-_ + else if (kc==219) k=String.fromCharCode(27); // Ctrl-[ + else if (kc==220) k=String.fromCharCode(28); // Ctrl-\ + else if (kc==221) k=String.fromCharCode(29); // Ctrl-] + else if (kc==219) k=String.fromCharCode(29); // Ctrl-] + else if (kc==219) k=String.fromCharCode(0); // Ctrl-@ + } else if (ev.which==0) { + if (kc==9) k=String.fromCharCode(9); // Tab + else if (kc==8) k=String.fromCharCode(127); // Backspace + else if (kc==27) k=String.fromCharCode(27); // Escape + else { + if (kc==33) k="[5~"; // PgUp + else if (kc==34) k="[6~"; // PgDn + else if (kc==35) k="[4~"; // End + else if (kc==36) k="[1~"; // Home + else if (kc==37) k="[D"; // Left + else if (kc==38) k="[A"; // Up + else if (kc==39) k="[C"; // Right + else if (kc==40) k="[B"; // Down + else if (kc==45) k="[2~"; // Ins + else if (kc==46) k="[3~"; // Del + else if (kc==112) k="[[A"; // F1 + else if (kc==113) k="[[B"; // F2 + else if (kc==114) k="[[C"; // F3 + else if (kc==115) k="[[D"; // F4 + else if (kc==116) k="[[E"; // F5 + else if (kc==117) k="[17~"; // F6 + else if (kc==118) k="[18~"; // F7 + else if (kc==119) k="[19~"; // F8 + else if (kc==120) k="[20~"; // F9 + else if (kc==121) k="[21~"; // F10 + else if (kc==122) k="[23~"; // F11 + else if (kc==123) k="[24~"; // F12 + if (k.length) { + k=String.fromCharCode(27)+k; + } + } + } else { + if (kc==8) + k=String.fromCharCode(127); // Backspace + else + k=String.fromCharCode(kc); + } + if(k.length) { +// queue(encodeURIComponent(k)); + if(k=="+") { + queue("%2B"); + } else { + queue(escape(k)); + } + } + ev.cancelBubble=true; + if (ev.stopPropagation) ev.stopPropagation(); + if (ev.preventDefault) ev.preventDefault(); + return false; + } + function keydown(ev) { + if (!ev) var ev=window.event; + if (ie) { +// s="kd keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey; +// debug(s); + o={9:1,8:1,27:1,33:1,34:1,35:1,36:1,37:1,38:1,39:1,40:1,45:1,46:1,112:1, + 113:1,114:1,115:1,116:1,117:1,118:1,119:1,120:1,121:1,122:1,123:1}; + if (o[ev.keyCode] || ev.ctrlKey || ev.altKey) { + ev.which=0; + return keypress(ev); + } + } + } + function init() { + sled.appendChild(document.createTextNode('\xb7')); + sled.className='off'; + dstat.appendChild(sled); + dstat.appendChild(document.createTextNode(' ')); + opt_add(opt_color,'Colors'); + opt_color.className='on'; + opt_add(opt_get,'GET'); + opt_add(opt_paste,'Paste'); + dstat.appendChild(sdebug); + dstat.className='stat'; + div.appendChild(dstat); + div.appendChild(dterm); + if(opt_color.addEventListener) { + opt_get.addEventListener('click',do_get,true); + opt_color.addEventListener('click',do_color,true); + opt_paste.addEventListener('click',do_paste,true); + } else { + opt_get.attachEvent("onclick", do_get); + opt_color.attachEvent("onclick", do_color); + opt_paste.attachEvent("onclick", do_paste); + } + document.onkeypress=keypress; + document.onkeydown=keydown; + timeout=window.setTimeout(update,100); + } + init(); +} +ajaxterm.Terminal=function(id,width,height) { + return new this.Terminal_ctor(id,width,height); +} + diff --git a/tools/ajaxterm/ajaxterm.py b/tools/ajaxterm/ajaxterm.py new file mode 100755 index 000000000..bf27b264a --- /dev/null +++ b/tools/ajaxterm/ajaxterm.py @@ -0,0 +1,586 @@ +#!/usr/bin/env python + +""" Ajaxterm """ + +import array,cgi,fcntl,glob,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,struct,pwd + +os.chdir(os.path.normpath(os.path.dirname(__file__))) +# Optional: Add QWeb in sys path +sys.path[0:0]=glob.glob('../../python') + +import qweb +import string, subprocess, uuid + +global g_server +TIMEOUT=300 + +class Terminal: + def __init__(self,width=80,height=24): + self.width=width + self.height=height + self.init() + self.reset() + def init(self): + self.esc_seq={ + "\x00": None, + "\x05": self.esc_da, + "\x07": None, + "\x08": self.esc_0x08, + "\x09": self.esc_0x09, + "\x0a": self.esc_0x0a, + "\x0b": self.esc_0x0a, + "\x0c": self.esc_0x0a, + "\x0d": self.esc_0x0d, + "\x0e": None, + "\x0f": None, + "\x1b#8": None, + "\x1b=": None, + "\x1b>": None, + "\x1b(0": None, + "\x1b(A": None, + "\x1b(B": None, + "\x1b[c": self.esc_da, + "\x1b[0c": self.esc_da, + "\x1b]R": None, + "\x1b7": self.esc_save, + "\x1b8": self.esc_restore, + "\x1bD": None, + "\x1bE": None, + "\x1bH": None, + "\x1bM": self.esc_ri, + "\x1bN": None, + "\x1bO": None, + "\x1bZ": self.esc_da, + "\x1ba": None, + "\x1bc": self.reset, + "\x1bn": None, + "\x1bo": None, + } + for k,v in self.esc_seq.items(): + if v==None: + self.esc_seq[k]=self.esc_ignore + # regex + d={ + r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch, + r'\]([^\x07]+)\x07' : self.esc_ignore, + } + self.esc_re=[] + for k,v in d.items(): + self.esc_re.append((re.compile('\x1b'+k),v)) + # define csi sequences + self.csi_seq={ + '@': (self.csi_at,[1]), + '`': (self.csi_G,[1]), + 'J': (self.csi_J,[0]), + 'K': (self.csi_K,[0]), + } + for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]: + if not self.csi_seq.has_key(i): + self.csi_seq[i]=(getattr(self,'csi_'+i),[1]) + # Init 0-256 to latin1 and html translation table + self.trl1="" + for i in range(256): + if i<32: + self.trl1+=" " + elif i<127 or i>160: + self.trl1+=chr(i) + else: + self.trl1+="?" + self.trhtml="" + for i in range(256): + if i==0x0a or (i>32 and i<127) or i>160: + self.trhtml+=chr(i) + elif i<=32: + self.trhtml+="\xa0" + else: + self.trhtml+="?" + def reset(self,s=""): + self.scr=array.array('i',[0x000700]*(self.width*self.height)) + self.st=0 + self.sb=self.height-1 + self.cx_bak=self.cx=0 + self.cy_bak=self.cy=0 + self.cl=0 + self.sgr=0x000700 + self.buf="" + self.outbuf="" + self.last_html="" + def peek(self,y1,x1,y2,x2): + return self.scr[self.width*y1+x1:self.width*y2+x2] + def poke(self,y,x,s): + pos=self.width*y+x + self.scr[pos:pos+len(s)]=s + def zero(self,y1,x1,y2,x2): + w=self.width*(y2-y1)+x2-x1+1 + z=array.array('i',[0x000700]*w) + self.scr[self.width*y1+x1:self.width*y2+x2+1]=z + def scroll_up(self,y1,y2): + self.poke(y1,0,self.peek(y1+1,0,y2,self.width)) + self.zero(y2,0,y2,self.width-1) + def scroll_down(self,y1,y2): + self.poke(y1+1,0,self.peek(y1,0,y2-1,self.width)) + self.zero(y1,0,y1,self.width-1) + def scroll_right(self,y,x): + self.poke(y,x+1,self.peek(y,x,y,self.width)) + self.zero(y,x,y,x) + def cursor_down(self): + if self.cy>=self.st and self.cy<=self.sb: + self.cl=0 + q,r=divmod(self.cy+1,self.sb+1) + if q: + self.scroll_up(self.st,self.sb) + self.cy=self.sb + else: + self.cy=r + def cursor_right(self): + q,r=divmod(self.cx+1,self.width) + if q: + self.cl=1 + else: + self.cx=r + def echo(self,c): + if self.cl: + self.cursor_down() + self.cx=0 + self.scr[(self.cy*self.width)+self.cx]=self.sgr|ord(c) + self.cursor_right() + def esc_0x08(self,s): + self.cx=max(0,self.cx-1) + def esc_0x09(self,s): + x=self.cx+8 + q,r=divmod(x,8) + self.cx=(q*8)%self.width + def esc_0x0a(self,s): + self.cursor_down() + def esc_0x0d(self,s): + self.cl=0 + self.cx=0 + def esc_save(self,s): + self.cx_bak=self.cx + self.cy_bak=self.cy + def esc_restore(self,s): + self.cx=self.cx_bak + self.cy=self.cy_bak + self.cl=0 + def esc_da(self,s): + self.outbuf="\x1b[?6c" + def esc_ri(self,s): + self.cy=max(self.st,self.cy-1) + if self.cy==self.st: + self.scroll_down(self.st,self.sb) + def esc_ignore(self,*s): + pass +# print "term:ignore: %s"%repr(s) + def csi_dispatch(self,seq,mo): + # CSI sequences + s=mo.group(1) + c=mo.group(2) + f=self.csi_seq.get(c,None) + if f: + try: + l=[min(int(i),1024) for i in s.split(';') if len(i)<4] + except ValueError: + l=[] + if len(l)==0: + l=f[1] + f[0](l) +# else: +# print 'csi ignore',c,l + def csi_at(self,l): + for i in range(l[0]): + self.scroll_right(self.cy,self.cx) + def csi_A(self,l): + self.cy=max(self.st,self.cy-l[0]) + def csi_B(self,l): + self.cy=min(self.sb,self.cy+l[0]) + def csi_C(self,l): + self.cx=min(self.width-1,self.cx+l[0]) + self.cl=0 + def csi_D(self,l): + self.cx=max(0,self.cx-l[0]) + self.cl=0 + def csi_E(self,l): + self.csi_B(l) + self.cx=0 + self.cl=0 + def csi_F(self,l): + self.csi_A(l) + self.cx=0 + self.cl=0 + def csi_G(self,l): + self.cx=min(self.width,l[0])-1 + def csi_H(self,l): + if len(l)<2: l=[1,1] + self.cx=min(self.width,l[1])-1 + self.cy=min(self.height,l[0])-1 + self.cl=0 + def csi_J(self,l): + if l[0]==0: + self.zero(self.cy,self.cx,self.height-1,self.width-1) + elif l[0]==1: + self.zero(0,0,self.cy,self.cx) + elif l[0]==2: + self.zero(0,0,self.height-1,self.width-1) + def csi_K(self,l): + if l[0]==0: + self.zero(self.cy,self.cx,self.cy,self.width-1) + elif l[0]==1: + self.zero(self.cy,0,self.cy,self.cx) + elif l[0]==2: + self.zero(self.cy,0,self.cy,self.width-1) + def csi_L(self,l): + for i in range(l[0]): + if self.cy<self.sb: + self.scroll_down(self.cy,self.sb) + def csi_M(self,l): + if self.cy>=self.st and self.cy<=self.sb: + for i in range(l[0]): + self.scroll_up(self.cy,self.sb) + def csi_P(self,l): + w,cx,cy=self.width,self.cx,self.cy + end=self.peek(cy,cx,cy,w) + self.csi_K([0]) + self.poke(cy,cx,end[l[0]:]) + def csi_X(self,l): + self.zero(self.cy,self.cx,self.cy,self.cx+l[0]) + def csi_a(self,l): + self.csi_C(l) + def csi_c(self,l): + #'\x1b[?0c' 0-8 cursor size + pass + def csi_d(self,l): + self.cy=min(self.height,l[0])-1 + def csi_e(self,l): + self.csi_B(l) + def csi_f(self,l): + self.csi_H(l) + def csi_h(self,l): + if l[0]==4: + pass +# print "insert on" + def csi_l(self,l): + if l[0]==4: + pass +# print "insert off" + def csi_m(self,l): + for i in l: + if i==0 or i==39 or i==49 or i==27: + self.sgr=0x000700 + elif i==1: + self.sgr=(self.sgr|0x000800) + elif i==7: + self.sgr=0x070000 + elif i>=30 and i<=37: + c=i-30 + self.sgr=(self.sgr&0xff08ff)|(c<<8) + elif i>=40 and i<=47: + c=i-40 + self.sgr=(self.sgr&0x00ffff)|(c<<16) +# else: +# print "CSI sgr ignore",l,i +# print 'sgr: %r %x'%(l,self.sgr) + def csi_r(self,l): + if len(l)<2: l=[0,self.height] + self.st=min(self.height-1,l[0]-1) + self.sb=min(self.height-1,l[1]-1) + self.sb=max(self.st,self.sb) + def csi_s(self,l): + self.esc_save(0) + def csi_u(self,l): + self.esc_restore(0) + def escape(self): + e=self.buf + if len(e)>32: +# print "error %r"%e + self.buf="" + elif e in self.esc_seq: + self.esc_seq[e](e) + self.buf="" + else: + for r,f in self.esc_re: + mo=r.match(e) + if mo: + f(e,mo) + self.buf="" + break +# if self.buf=='': print "ESC %r\n"%e + def write(self,s): + for i in s: + if len(self.buf) or (i in self.esc_seq): + self.buf+=i + self.escape() + elif i == '\x1b': + self.buf+=i + else: + self.echo(i) + def read(self): + b=self.outbuf + self.outbuf="" + return b + def dump(self): + r='' + for i in self.scr: + r+=chr(i&255) + return r + def dumplatin1(self): + return self.dump().translate(self.trl1) + def dumphtml(self,color=1): + h=self.height + w=self.width + r="" + span="" + span_bg,span_fg=-1,-1 + for i in range(h*w): + q,c=divmod(self.scr[i],256) + if color: + bg,fg=divmod(q,256) + else: + bg,fg=0,7 + if i==self.cy*w+self.cx: + bg,fg=1,7 + if (bg!=span_bg or fg!=span_fg or i==h*w-1): + if len(span): + r+='<span class="f%d b%d">%s</span>'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml))) + span="" + span_bg,span_fg=bg,fg + span+=chr(c) + if i%w==w-1: + span+='\n' + r='<?xml version="1.0" encoding="ISO-8859-1"?><pre class="term">%s</pre>'%r + if self.last_html==r: + return '<?xml version="1.0"?><idem></idem>' + else: + self.last_html=r +# print self + return r + def __repr__(self): + d=self.dumplatin1() + r="" + for i in range(self.height): + r+="|%s|\n"%d[self.width*i:self.width*(i+1)] + return r + +class SynchronizedMethod: + def __init__(self,lock,orig): + self.lock=lock + self.orig=orig + def __call__(self,*l): + self.lock.acquire() + r=self.orig(*l) + self.lock.release() + return r + +class Multiplex: + def __init__(self,cmd=None): + signal.signal(signal.SIGCHLD, signal.SIG_IGN) + self.cmd=cmd + self.proc={} + self.lock=threading.RLock() + self.thread=threading.Thread(target=self.loop) + self.alive=1 + self.lastActivity=time.time() + # synchronize methods + for name in ['create','fds','proc_read','proc_write','dump','die','run']: + orig=getattr(self,name) + setattr(self,name,SynchronizedMethod(self.lock,orig)) + self.thread.start() + def create(self,w=80,h=25): + pid,fd=pty.fork() + if pid==0: + try: + fdl=[int(i) for i in os.listdir('/proc/self/fd')] + except OSError: + fdl=range(256) + for i in [i for i in fdl if i>2]: + try: + os.close(i) + except OSError: + pass + if self.cmd: + cmd=['/bin/sh','-c',self.cmd] + elif os.getuid()==0: + cmd=['/bin/login'] + else: + sys.stdout.write("Login: ") + login=sys.stdin.readline().strip() + if re.match('^[0-9A-Za-z-_. ]+$',login): + cmd=['ssh'] + cmd+=['-oPreferredAuthentications=keyboard-interactive,password'] + cmd+=['-oNoHostAuthenticationForLocalhost=yes'] + cmd+=['-oLogLevel=FATAL'] + cmd+=['-F/dev/null','-l',login,'localhost'] + else: + os._exit(0) + env={} + env["COLUMNS"]=str(w) + env["LINES"]=str(h) + env["TERM"]="linux" + env["PATH"]=os.environ['PATH'] + os.execvpe(cmd[0],cmd,env) + else: + fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) + # python bug http://python.org/sf/1112949 on amd64 + fcntl.ioctl(fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], struct.pack("HHHH",h,w,0,0)) + self.proc[fd]={'pid':pid,'term':Terminal(w,h),'buf':'','time':time.time()} + return fd + def die(self): + self.alive=0 + def run(self): + return self.alive + def fds(self): + return self.proc.keys() + def proc_kill(self,fd): + if fd in self.proc: + self.proc[fd]['time']=0 + t=time.time() + for i in self.proc.keys(): + t0=self.proc[i]['time'] + if (t-t0)>TIMEOUT: + try: + os.close(i) + os.kill(self.proc[i]['pid'],signal.SIGTERM) + except (IOError,OSError): + pass + del self.proc[i] + def proc_read(self,fd): + try: + t=self.proc[fd]['term'] + t.write(os.read(fd,65536)) + reply=t.read() + if reply: + os.write(fd,reply) + self.proc[fd]['time']=time.time() + except (KeyError,IOError,OSError): + self.proc_kill(fd) + def proc_write(self,fd,s): + try: + os.write(fd,s) + except (IOError,OSError): + self.proc_kill(fd) + def dump(self,fd,color=1): + try: + return self.proc[fd]['term'].dumphtml(color) + except KeyError: + return False + def loop(self): + while self.run(): + fds=self.fds() + i,o,e=select.select(fds, [], [], 1.0) + if time.time() - self.lastActivity > TIMEOUT: + global g_server + g_server.shutdown() + for fd in i: + self.proc_read(fd) + if len(i): + time.sleep(0.002) + for i in self.proc.keys(): + try: + os.close(i) + os.kill(self.proc[i]['pid'],signal.SIGTERM) + except (IOError,OSError): + pass + +class AjaxTerm: + def __init__(self,cmd=None,index_file='ajaxterm.html',token=None): + self.files={} + self.token=token + for i in ['css','html','js']: + for j in glob.glob('*.%s'%i): + self.files[j]=file(j).read() + self.files['index']=file(index_file).read() + self.mime = mimetypes.types_map.copy() + self.mime['.html']= 'text/html; charset=UTF-8' + self.multi = Multiplex(cmd) + self.session = {} + def __call__(self, environ, start_response): + req = qweb.QWebRequest(environ, start_response,session=None) + if req.PATH_INFO.endswith('/u'): + s=req.REQUEST["s"] + k=req.REQUEST["k"] + c=req.REQUEST["c"] + w=req.REQUEST.int("w") + h=req.REQUEST.int("h") + if s in self.session: + term=self.session[s] + else: + raise Exception('Not Authorized') + # The original code below was insecure, because it allowed unauthorized sessions to be created + # if not (w>2 and w<256 and h>2 and h<100): + # w,h=80,25 + # term=self.session[s]=self.multi.create(w,h) + if k: + self.multi.proc_write(term,k) + time.sleep(0.002) + self.multi.lastActivity = time.time(); + dump=self.multi.dump(term,c) + req.response_headers['Content-Type']='text/xml' + if isinstance(dump,str): + req.write(dump) + req.response_gzencode=1 + else: + del self.session[s] + req.write('<?xml version="1.0"?><idem></idem>') +# print "sessions %r"%self.session + else: + n=os.path.basename(req.PATH_INFO) + if n in self.files: + req.response_headers['Content-Type'] = self.mime.get(os.path.splitext(n)[1].lower(), 'application/octet-stream') + req.write(self.files[n]) + elif req.REQUEST['token'] == self.token: + req.response_headers['Content-Type'] = 'text/html; charset=UTF-8' + session_id = str(uuid.uuid4()) + req.write(string.Template(self.files['index']).substitute(session_id=session_id)) + term=self.session[session_id]=self.multi.create(80,25) + else: + raise Exception("Not Authorized") + return req + +def main(): + parser = optparse.OptionParser() + parser.add_option("-p", "--port", dest="port", default="8022", help="Set the TCP port (default: 8022)") + parser.add_option("-c", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh 0.0.0.0)") + parser.add_option("-l", "--log", action="store_true", dest="log",default=0,help="log requests to stderr (default: quiet mode)") + parser.add_option("-d", "--daemon", action="store_true", dest="daemon", default=0, help="run as daemon in the background") + parser.add_option("-P", "--pidfile",dest="pidfile",default="/var/run/ajaxterm.pid",help="set the pidfile (default: /var/run/ajaxterm.pid)") + parser.add_option("-i", "--index", dest="index_file", default="ajaxterm.html",help="default index file (default: ajaxterm.html)") + parser.add_option("-u", "--uid", dest="uid", help="Set the daemon's user id") + parser.add_option("-t", "--token", dest="token", help="Set authorization token") + (o, a) = parser.parse_args() + if o.daemon: + pid=os.fork() + if pid == 0: + #os.setsid() ? + os.setpgrp() + nullin = file('/dev/null', 'r') + nullout = file('/dev/null', 'w') + os.dup2(nullin.fileno(), sys.stdin.fileno()) + os.dup2(nullout.fileno(), sys.stdout.fileno()) + os.dup2(nullout.fileno(), sys.stderr.fileno()) + if os.getuid()==0 and o.uid: + try: + os.setuid(int(o.uid)) + except: + os.setuid(pwd.getpwnam(o.uid).pw_uid) + else: + try: + file(o.pidfile,'w+').write(str(pid)+'\n') + except: + pass + print 'AjaxTerm at http://0.0.0.0:%s/ pid: %d' % (o.port,pid) + sys.exit(0) + else: + print 'AjaxTerm at http://0.0.0.0:%s/' % o.port + at=AjaxTerm(o.cmd,o.index_file,o.token) +# f=lambda:os.system('firefox http://localhost:%s/&'%o.port) +# qweb.qweb_wsgi_autorun(at,ip='localhost',port=int(o.port),threaded=0,log=o.log,callback_ready=None) + try: + global g_server + g_server = qweb.QWebWSGIServer(at,ip='0.0.0.0',port=int(o.port),threaded=0,log=o.log) + g_server.serve_forever() + except KeyboardInterrupt,e: + sys.excepthook(*sys.exc_info()) + at.multi.die() + +if __name__ == '__main__': + main() + diff --git a/tools/ajaxterm/configure b/tools/ajaxterm/configure new file mode 100755 index 000000000..45391f484 --- /dev/null +++ b/tools/ajaxterm/configure @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +import optparse,os + +parser = optparse.OptionParser() +parser.add_option("", "--prefix", dest="prefix",default="/usr/local",help="installation prefix (default: /usr/local)") +parser.add_option("", "--confdir", dest="confdir", default="/etc",help="configuration files directory prefix (default: /etc)") +parser.add_option("", "--port", dest="port", default="8022", help="set the listening TCP port (default: 8022)") +parser.add_option("", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)") +(o, a) = parser.parse_args() + +print "Configuring prefix=",o.prefix," port=",o.port + +etc=o.confdir +port=o.port +cmd=o.cmd +bin=os.path.join(o.prefix,"bin") +lib=os.path.join(o.prefix,"share/ajaxterm") +man=os.path.join(o.prefix,"share/man/man1") + +file("ajaxterm.bin","w").write(file("configure.ajaxterm.bin").read()%locals()) +file("Makefile","w").write(file("configure.makefile").read()%locals()) + +if os.path.isfile("/etc/gentoo-release"): + file("ajaxterm.initd","w").write(file("configure.initd.gentoo").read()%locals()) +elif os.path.isfile("/etc/fedora-release") or os.path.isfile("/etc/redhat-release"): + file("ajaxterm.initd","w").write(file("configure.initd.redhat").read()%locals()) +else: + file("ajaxterm.initd","w").write(file("configure.initd.debian").read()%locals()) + +os.system("chmod a+x ajaxterm.bin") +os.system("chmod a+x ajaxterm.initd") diff --git a/tools/ajaxterm/configure.ajaxterm.bin b/tools/ajaxterm/configure.ajaxterm.bin new file mode 100644 index 000000000..4d1f5a98f --- /dev/null +++ b/tools/ajaxterm/configure.ajaxterm.bin @@ -0,0 +1,2 @@ +#!/bin/sh +PYTHONPATH=%(lib)s exec %(lib)s/ajaxterm.py $@ diff --git a/tools/ajaxterm/configure.initd.debian b/tools/ajaxterm/configure.initd.debian new file mode 100644 index 000000000..901082707 --- /dev/null +++ b/tools/ajaxterm/configure.initd.debian @@ -0,0 +1,33 @@ +#!/bin/sh + +PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin +DAEMON=%(bin)s/ajaxterm +PORT=%(port)s +PIDFILE=/var/run/ajaxterm.pid + +[ -x "$DAEMON" ] || exit 0 + +#. /lib/lsb/init-functions + +case "$1" in + start) + echo "Starting ajaxterm on port $PORT" + start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody || return 2 + ;; + stop) + echo "Stopping ajaxterm" + start-stop-daemon --stop --pidfile $PIDFILE + rm -f $PIDFILE + ;; + restart|force-reload) + $0 stop + sleep 1 + $0 start + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/tools/ajaxterm/configure.initd.gentoo b/tools/ajaxterm/configure.initd.gentoo new file mode 100644 index 000000000..ac28ef0b6 --- /dev/null +++ b/tools/ajaxterm/configure.initd.gentoo @@ -0,0 +1,27 @@ +#!/sbin/runscript + +# AjaxTerm Gentoo script, 08 May 2006 Mark Gillespie + +DAEMON=%(bin)s/ajaxterm +PORT=%(port)s +PIDFILE=/var/run/ajaxterm.pid + +depend() +{ + need net +} + +start() +{ + ebegin "Starting AjaxTerm on port $PORT" + start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody + eend $? +} + +stop() +{ + ebegin "Stopping AjaxTerm" + start-stop-daemon --stop --pidfile $PIDFILE + rm -f $PIDFILE + eend $? +} diff --git a/tools/ajaxterm/configure.initd.redhat b/tools/ajaxterm/configure.initd.redhat new file mode 100644 index 000000000..5c9788574 --- /dev/null +++ b/tools/ajaxterm/configure.initd.redhat @@ -0,0 +1,75 @@ +# +# ajaxterm Startup script for ajaxterm +# +# chkconfig: - 99 99 +# description: Ajaxterm is a yadda yadda yadda +# processname: ajaxterm +# pidfile: /var/run/ajaxterm.pid +# version: 1.0 Kevin Reichhart - ajaxterminit at lastname dot org + +# Source function library. +. /etc/rc.d/init.d/functions + +if [ -f /etc/sysconfig/ajaxterm ]; then + . /etc/sysconfig/ajaxterm +fi + +ajaxterm=/usr/local/bin/ajaxterm +prog=ajaxterm +pidfile=${PIDFILE-/var/run/ajaxterm.pid} +lockfile=${LOCKFILE-/var/lock/subsys/ajaxterm} +port=${PORT-8022} +user=${xUSER-nobody} +RETVAL=0 + + +start() { + echo -n $"Starting $prog: " + daemon $ajaxterm --daemon --port=$port --uid=$user $OPTIONS + RETVAL=$? + echo + [ $RETVAL = 0 ] && touch ${lockfile} + return $RETVAL +} +stop() { + echo -n $"Stopping $prog: " + killproc $ajaxterm + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile} +} +reload() { + echo -n $"Reloading $prog: " + killproc $ajaxterm -HUP + RETVAL=$? + echo +} + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + status python ajaxterm + RETVAL=$? + ;; + restart) + stop + start + ;; + condrestart) + if [ -f ${pidfile} ] ; then + stop + start + fi + ;; + *) + echo $"Usage: $prog {start|stop|restart|condrestart}" + exit 1 +esac + +exit $RETVAL diff --git a/tools/ajaxterm/configure.makefile b/tools/ajaxterm/configure.makefile new file mode 100644 index 000000000..6bd80853d --- /dev/null +++ b/tools/ajaxterm/configure.makefile @@ -0,0 +1,20 @@ +build: + true + +install: + install -d "%(bin)s" + install -d "%(lib)s" + install ajaxterm.bin "%(bin)s/ajaxterm" + install ajaxterm.initd "%(etc)s/init.d/ajaxterm" + install -m 644 ajaxterm.css ajaxterm.html ajaxterm.js qweb.py sarissa.js sarissa_dhtml.js "%(lib)s" + install -m 755 ajaxterm.py "%(lib)s" + gzip --best -c ajaxterm.1 > ajaxterm.1.gz + install -d "%(man)s" + install ajaxterm.1.gz "%(man)s" + +clean: + rm ajaxterm.bin + rm ajaxterm.initd + rm ajaxterm.1.gz + rm Makefile + diff --git a/tools/ajaxterm/qweb.py b/tools/ajaxterm/qweb.py new file mode 100644 index 000000000..20c509230 --- /dev/null +++ b/tools/ajaxterm/qweb.py @@ -0,0 +1,1356 @@ +#!/usr/bin/python2.3 +# +# vim:set et ts=4 fdc=0 fdn=2 fdl=0: +# +# There are no blank lines between blocks beacause i use folding from: +# http://www.vim.org/scripts/script.php?script_id=515 +# + +"""= QWeb Framework = + +== What is QWeb ? == + +QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI] +compatible web framework, it provides an infratructure to quickly build web +applications consisting of: + + * A lightweight request handler (QWebRequest) + * An xml templating engine (QWebXml and QWebHtml) + * A simple name based controler (qweb_control) + * A standalone WSGI Server (QWebWSGIServer) + * A cgi and fastcgi WSGI wrapper (taken from flup) + * A startup function that starts cgi, factgi or standalone according to the + evironement (qweb_autorun). + +QWeb applications are runnable in standalone mode (from commandline), via +FastCGI, Regular CGI or by any python WSGI compliant server. + +QWeb doesn't provide any database access but it integrates nicely with ORMs +such as SQLObject, SQLAlchemy or plain DB-API. + +Written by Antony Lesuisse (email al AT udev.org) + +Homepage: http://antony.lesuisse.org/qweb/trac/ + +Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum] + +== Quick Start (for Linux, MacOS X and cygwin) == + +Make sure you have at least python 2.3 installed and run the following commands: + +{{{ +$ wget http://antony.lesuisse.org/qweb/files/QWeb-0.7.tar.gz +$ tar zxvf QWeb-0.7.tar.gz +$ cd QWeb-0.7/examples/blog +$ ./blog.py +}}} + +And point your browser to http://localhost:8080/ + +You may also try AjaxTerm which uses qweb request handler. + +== Download == + + * Version 0.7: + * Source [/qweb/files/QWeb-0.7.tar.gz QWeb-0.7.tar.gz] + * Python 2.3 Egg [/qweb/files/QWeb-0.7-py2.3.egg QWeb-0.7-py2.3.egg] + * Python 2.4 Egg [/qweb/files/QWeb-0.7-py2.4.egg QWeb-0.7-py2.4.egg] + + * [/qweb/trac/browser Browse the source repository] + +== Documentation == + + * [/qweb/trac/browser/trunk/README.txt?format=raw Read the included documentation] + * QwebTemplating + +== Mailin-list == + + * Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum] + * No mailing-list exists yet, discussion should happen on: [http://mail.python.org/mailman/listinfo/web-sig web-sig] [http://mail.python.org/pipermail/web-sig/ archives] + +QWeb Components: +---------------- + +QWeb also feature a simple components api, that enables developers to easily +produces reusable components. + +Default qweb components: + + - qweb_static: + A qweb component to serve static content from the filesystem or from + zipfiles. + + - qweb_dbadmin: + scaffolding for sqlobject + +License +------- +qweb/fcgi.py wich is BSD-like from saddi.com. +Everything else is put in the public domain. + + +TODO +---- + Announce QWeb to python-announce-list@python.org web-sig@python.org + qweb_core + rename request methods into + request_save_files + response_404 + response_redirect + response_download + request callback_generator, callback_function ? + wsgi callback_server_local + xml tags explicitly call render_attributes(t_att)? + priority form-checkbox over t-value (for t-option) + +""" + +import BaseHTTPServer,SocketServer,Cookie +import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom +try: + import cPickle as pickle +except ImportError: + import pickle +try: + import cStringIO as StringIO +except ImportError: + import StringIO + +#---------------------------------------------------------- +# Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim +#---------------------------------------------------------- +class QWebEval: + def __init__(self,data): + self.data=data + def __getitem__(self,expr): + if self.data.has_key(expr): + return self.data[expr] + r=None + try: + r=eval(expr,self.data) + except NameError,e: + pass + except AttributeError,e: + pass + except Exception,e: + print "qweb: expression error '%s' "%expr,e + if self.data.has_key("__builtins__"): + del self.data["__builtins__"] + return r + def eval_object(self,expr): + return self[expr] + def eval_str(self,expr): + if expr=="0": + return self.data[0] + if isinstance(self[expr],unicode): + return self[expr].encode("utf8") + return str(self[expr]) + def eval_format(self,expr): + try: + return str(expr%self) + except: + return "qweb: format error '%s' "%expr +# if isinstance(r,unicode): +# return r.encode("utf8") + def eval_bool(self,expr): + if self.eval_object(expr): + return 1 + else: + return 0 +class QWebXml: + """QWeb Xml templating engine + + The templating engine use a very simple syntax, "magic" xml attributes, to + produce any kind of texutal output (even non-xml). + + QWebXml: + the template engine core implements the basic magic attributes: + + t-att t-raw t-esc t-if t-foreach t-set t-call t-trim + + """ + def __init__(self,x=None,zipname=None): + self.node=xml.dom.Node + self._t={} + self._render_tag={} + prefix='render_tag_' + for i in [j for j in dir(self) if j.startswith(prefix)]: + name=i[len(prefix):].replace('_','-') + self._render_tag[name]=getattr(self.__class__,i) + + self._render_att={} + prefix='render_att_' + for i in [j for j in dir(self) if j.startswith(prefix)]: + name=i[len(prefix):].replace('_','-') + self._render_att[name]=getattr(self.__class__,i) + + if x!=None: + if zipname!=None: + import zipfile + zf=zipfile.ZipFile(zipname, 'r') + self.add_template(zf.read(x)) + else: + self.add_template(x) + def register_tag(self,tag,func): + self._render_tag[tag]=func + def add_template(self,x): + if hasattr(x,'documentElement'): + dom=x + elif x.startswith("<?xml"): + import xml.dom.minidom + dom=xml.dom.minidom.parseString(x) + else: + import xml.dom.minidom + dom=xml.dom.minidom.parse(x) + for n in dom.documentElement.childNodes: + if n.nodeName=="t": + self._t[str(n.getAttribute("t-name"))]=n + def get_template(self,name): + return self._t[name] + + def eval_object(self,expr,v): + return QWebEval(v).eval_object(expr) + def eval_str(self,expr,v): + return QWebEval(v).eval_str(expr) + def eval_format(self,expr,v): + return QWebEval(v).eval_format(expr) + def eval_bool(self,expr,v): + return QWebEval(v).eval_bool(expr) + + def render(self,tname,v={},out=None): + if self._t.has_key(tname): + return self.render_node(self._t[tname],v) + else: + return 'qweb: template "%s" not found'%tname + def render_node(self,e,v): + r="" + if e.nodeType==self.node.TEXT_NODE or e.nodeType==self.node.CDATA_SECTION_NODE: + r=e.data.encode("utf8") + elif e.nodeType==self.node.ELEMENT_NODE: + pre="" + g_att="" + t_render=None + t_att={} + for (an,av) in e.attributes.items(): + an=str(an) + if isinstance(av,types.UnicodeType): + av=av.encode("utf8") + else: + av=av.nodeValue.encode("utf8") + if an.startswith("t-"): + for i in self._render_att: + if an[2:].startswith(i): + g_att+=self._render_att[i](self,e,an,av,v) + break + else: + if self._render_tag.has_key(an[2:]): + t_render=an[2:] + t_att[an[2:]]=av + else: + g_att+=' %s="%s"'%(an,cgi.escape(av,1)); + if t_render: + if self._render_tag.has_key(t_render): + r=self._render_tag[t_render](self,e,t_att,g_att,v) + else: + r=self.render_element(e,g_att,v,pre,t_att.get("trim",0)) + return r + def render_element(self,e,g_att,v,pre="",trim=0): + g_inner=[] + for n in e.childNodes: + g_inner.append(self.render_node(n,v)) + name=str(e.nodeName) + inner="".join(g_inner) + if trim==0: + pass + elif trim=='left': + inner=inner.lstrip() + elif trim=='right': + inner=inner.rstrip() + elif trim=='both': + inner=inner.strip() + if name=="t": + return inner + elif len(inner): + return "<%s%s>%s%s</%s>"%(name,g_att,pre,inner,name) + else: + return "<%s%s/>"%(name,g_att) + + # Attributes + def render_att_att(self,e,an,av,v): + if an.startswith("t-attf-"): + att,val=an[7:],self.eval_format(av,v) + elif an.startswith("t-att-"): + att,val=(an[6:],self.eval_str(av,v)) + else: + att,val=self.eval_object(av,v) + return ' %s="%s"'%(att,cgi.escape(val,1)) + + # Tags + def render_tag_raw(self,e,t_att,g_att,v): + return self.eval_str(t_att["raw"],v) + def render_tag_rawf(self,e,t_att,g_att,v): + return self.eval_format(t_att["rawf"],v) + def render_tag_esc(self,e,t_att,g_att,v): + return cgi.escape(self.eval_str(t_att["esc"],v)) + def render_tag_escf(self,e,t_att,g_att,v): + return cgi.escape(self.eval_format(t_att["escf"],v)) + def render_tag_foreach(self,e,t_att,g_att,v): + expr=t_att["foreach"] + enum=self.eval_object(expr,v) + if enum!=None: + var=t_att.get('as',expr).replace('.','_') + d=v.copy() + size=-1 + if isinstance(enum,types.ListType): + size=len(enum) + elif isinstance(enum,types.TupleType): + size=len(enum) + elif hasattr(enum,'count'): + size=enum.count() + d["%s_size"%var]=size + d["%s_all"%var]=enum + index=0 + ru=[] + for i in enum: + d["%s_value"%var]=i + d["%s_index"%var]=index + d["%s_first"%var]=index==0 + d["%s_even"%var]=index%2 + d["%s_odd"%var]=(index+1)%2 + d["%s_last"%var]=index+1==size + if index%2: + d["%s_parity"%var]='odd' + else: + d["%s_parity"%var]='even' + if isinstance(i,types.DictType): + d.update(i) + else: + d[var]=i + ru.append(self.render_element(e,g_att,d)) + index+=1 + return "".join(ru) + else: + return "qweb: t-foreach %s not found."%expr + def render_tag_if(self,e,t_att,g_att,v): + if self.eval_bool(t_att["if"],v): + return self.render_element(e,g_att,v) + else: + return "" + def render_tag_call(self,e,t_att,g_att,v): + # TODO t-prefix + if t_att.has_key("import"): + d=v + else: + d=v.copy() + d[0]=self.render_element(e,g_att,d) + return self.render(t_att["call"],d) + def render_tag_set(self,e,t_att,g_att,v): + if t_att.has_key("eval"): + v[t_att["set"]]=self.eval_object(t_att["eval"],v) + else: + v[t_att["set"]]=self.render_element(e,g_att,v) + return "" + +#---------------------------------------------------------- +# QWeb HTML (+deprecated QWebFORM and QWebOLD) +#---------------------------------------------------------- +class QWebURL: + """ URL helper + assert req.PATH_INFO== "/site/admin/page_edit" + u = QWebURL(root_path="/site/",req_path=req.PATH_INFO) + s=u.url2_href("user/login",{'a':'1'}) + assert s=="../user/login?a=1" + + """ + def __init__(self, root_path="/", req_path="/",defpath="",defparam={}): + self.defpath=defpath + self.defparam=defparam + self.root_path=root_path + self.req_path=req_path + self.req_list=req_path.split("/")[:-1] + self.req_len=len(self.req_list) + def decode(self,s): + h={} + for k,v in cgi.parse_qsl(s,1): + h[k]=v + return h + def encode(self,h): + return urllib.urlencode(h.items()) + def request(self,req): + return req.REQUEST + def copy(self,path=None,param=None): + npath=self.defpath + if path: + npath=path + nparam=self.defparam.copy() + if param: + nparam.update(param) + return QWebURL(self.root_path,self.req_path,npath,nparam) + def path(self,path=''): + if not path: + path=self.defpath + pl=(self.root_path+path).split('/') + i=0 + for i in range(min(len(pl), self.req_len)): + if pl[i]!=self.req_list[i]: + break + else: + i+=1 + dd=self.req_len-i + if dd<0: + dd=0 + return '/'.join(['..']*dd+pl[i:]) + def href(self,path='',arg={}): + p=self.path(path) + tmp=self.defparam.copy() + tmp.update(arg) + s=self.encode(tmp) + if len(s): + return p+"?"+s + else: + return p + def form(self,path='',arg={}): + p=self.path(path) + tmp=self.defparam.copy() + tmp.update(arg) + r=''.join(['<input type="hidden" name="%s" value="%s"/>'%(k,cgi.escape(str(v),1)) for k,v in tmp.items()]) + return (p,r) +class QWebField: + def __init__(self,name=None,default="",check=None): + self.name=name + self.default=default + self.check=check + # optional attributes + self.type=None + self.trim=1 + self.required=1 + self.cssvalid="form_valid" + self.cssinvalid="form_invalid" + # set by addfield + self.form=None + # set by processing + self.input=None + self.css=None + self.value=None + self.valid=None + self.invalid=None + self.validate(1) + def validate(self,val=1,update=1): + if val: + self.valid=1 + self.invalid=0 + self.css=self.cssvalid + else: + self.valid=0 + self.invalid=1 + self.css=self.cssinvalid + if update and self.form: + self.form.update() + def invalidate(self,update=1): + self.validate(0,update) +class QWebForm: + class QWebFormF: + pass + def __init__(self,e=None,arg=None,default=None): + self.fields={} + # all fields have been submitted + self.submitted=False + self.missing=[] + # at least one field is invalid or missing + self.invalid=False + self.error=[] + # all fields have been submitted and are valid + self.valid=False + # fields under self.f for convenience + self.f=self.QWebFormF() + if e: + self.add_template(e) + # assume that the fields are done with the template + if default: + self.set_default(default,e==None) + if arg!=None: + self.process_input(arg) + def __getitem__(self,k): + return self.fields[k] + def set_default(self,default,add_missing=1): + for k,v in default.items(): + if self.fields.has_key(k): + self.fields[k].default=str(v) + elif add_missing: + self.add_field(QWebField(k,v)) + def add_field(self,f): + self.fields[f.name]=f + f.form=self + setattr(self.f,f.name,f) + def add_template(self,e): + att={} + for (an,av) in e.attributes.items(): + an=str(an) + if an.startswith("t-"): + att[an[2:]]=av.encode("utf8") + for i in ["form-text", "form-password", "form-radio", "form-checkbox", "form-select","form-textarea"]: + if att.has_key(i): + name=att[i].split(".")[-1] + default=att.get("default","") + check=att.get("check",None) + f=QWebField(name,default,check) + if i=="form-textarea": + f.type="textarea" + f.trim=0 + if i=="form-checkbox": + f.type="checkbox" + f.required=0 + self.add_field(f) + for n in e.childNodes: + if n.nodeType==n.ELEMENT_NODE: + self.add_template(n) + def process_input(self,arg): + for f in self.fields.values(): + if arg.has_key(f.name): + f.input=arg[f.name] + f.value=f.input + if f.trim: + f.input=f.input.strip() + f.validate(1,False) + if f.check==None: + continue + elif callable(f.check): + pass + elif isinstance(f.check,str): + v=f.check + if f.check=="email": + v=r"/^[^@#!& ]+@[A-Za-z0-9-][.A-Za-z0-9-]{0,64}\.[A-Za-z]{2,5}$/" + if f.check=="date": + v=r"/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/" + if not re.match(v[1:-1],f.input): + f.validate(0,False) + else: + f.value=f.default + self.update() + def validate_all(self,val=1): + for f in self.fields.values(): + f.validate(val,0) + self.update() + def invalidate_all(self): + self.validate_all(0) + def update(self): + self.submitted=True + self.valid=True + self.errors=[] + for f in self.fields.values(): + if f.required and f.input==None: + self.submitted=False + self.valid=False + self.missing.append(f.name) + if f.invalid: + self.valid=False + self.error.append(f.name) + # invalid have been submitted and + self.invalid=self.submitted and self.valid==False + def collect(self): + d={} + for f in self.fields.values(): + d[f.name]=f.value + return d +class QWebURLEval(QWebEval): + def __init__(self,data): + QWebEval.__init__(self,data) + def __getitem__(self,expr): + r=QWebEval.__getitem__(self,expr) + if isinstance(r,str): + return urllib.quote_plus(r) + else: + return r +class QWebHtml(QWebXml): + """QWebHtml + QWebURL: + QWebField: + QWebForm: + QWebHtml: + an extended template engine, with a few utility class to easily produce + HTML, handle URLs and process forms, it adds the following magic attributes: + + t-href t-action t-form-text t-form-password t-form-textarea t-form-radio + t-form-checkbox t-form-select t-option t-selected t-checked t-pager + + # explication URL: + # v['tableurl']=QWebUrl({p=afdmin,saar=,orderby=,des=,mlink;meta_active=}) + # t-href="tableurl?desc=1" + # + # explication FORM: t-if="form.valid()" + # Foreach i + # email: <input type="text" t-esc-name="i" t-esc-value="form[i].value" t-esc-class="form[i].css"/> + # <input type="radio" name="spamtype" t-esc-value="i" t-selected="i==form.f.spamtype.value"/> + # <option t-esc-value="cc" t-selected="cc==form.f.country.value"><t t-esc="cname"></option> + # Simple forms: + # <input t-form-text="form.email" t-check="email"/> + # <input t-form-password="form.email" t-check="email"/> + # <input t-form-radio="form.email" /> + # <input t-form-checkbox="form.email" /> + # <textarea t-form-textarea="form.email" t-check="email"/> + # <select t-form-select="form.email"/> + # <option t-value="1"> + # <input t-form-radio="form.spamtype" t-value="1"/> Cars + # <input t-form-radio="form.spamtype" t-value="2"/> Sprt + """ + # QWebForm from a template + def form(self,tname,arg=None,default=None): + form=QWebForm(self._t[tname],arg,default) + return form + + # HTML Att + def eval_url(self,av,v): + s=QWebURLEval(v).eval_format(av) + a=s.split('?',1) + arg={} + if len(a)>1: + for k,v in cgi.parse_qsl(a[1],1): + arg[k]=v + b=a[0].split('/',1) + path='' + if len(b)>1: + path=b[1] + u=b[0] + return u,path,arg + def render_att_url_(self,e,an,av,v): + u,path,arg=self.eval_url(av,v) + if not isinstance(v.get(u,0),QWebURL): + out='qweb: missing url %r %r %r'%(u,path,arg) + else: + out=v[u].href(path,arg) + return ' %s="%s"'%(an[6:],cgi.escape(out,1)) + def render_att_href(self,e,an,av,v): + return self.render_att_url_(e,"t-url-href",av,v) + def render_att_checked(self,e,an,av,v): + if self.eval_bool(av,v): + return ' %s="%s"'%(an[2:],an[2:]) + else: + return '' + def render_att_selected(self,e,an,av,v): + return self.render_att_checked(e,an,av,v) + + # HTML Tags forms + def render_tag_rawurl(self,e,t_att,g_att,v): + u,path,arg=self.eval_url(t_att["rawurl"],v) + return v[u].href(path,arg) + def render_tag_escurl(self,e,t_att,g_att,v): + u,path,arg=self.eval_url(t_att["escurl"],v) + return cgi.escape(v[u].href(path,arg)) + def render_tag_action(self,e,t_att,g_att,v): + u,path,arg=self.eval_url(t_att["action"],v) + if not isinstance(v.get(u,0),QWebURL): + action,input=('qweb: missing url %r %r %r'%(u,path,arg),'') + else: + action,input=v[u].form(path,arg) + g_att+=' action="%s"'%action + return self.render_element(e,g_att,v,input) + def render_tag_form_text(self,e,t_att,g_att,v): + f=self.eval_object(t_att["form-text"],v) + g_att+=' type="text" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css) + return self.render_element(e,g_att,v) + def render_tag_form_password(self,e,t_att,g_att,v): + f=self.eval_object(t_att["form-password"],v) + g_att+=' type="password" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css) + return self.render_element(e,g_att,v) + def render_tag_form_textarea(self,e,t_att,g_att,v): + type="textarea" + f=self.eval_object(t_att["form-textarea"],v) + g_att+=' name="%s" class="%s"'%(f.name,f.css) + r="<%s%s>%s</%s>"%(type,g_att,cgi.escape(f.value,1),type) + return r + def render_tag_form_radio(self,e,t_att,g_att,v): + f=self.eval_object(t_att["form-radio"],v) + val=t_att["value"] + g_att+=' type="radio" name="%s" value="%s"'%(f.name,val) + if f.value==val: + g_att+=' checked="checked"' + return self.render_element(e,g_att,v) + def render_tag_form_checkbox(self,e,t_att,g_att,v): + f=self.eval_object(t_att["form-checkbox"],v) + val=t_att["value"] + g_att+=' type="checkbox" name="%s" value="%s"'%(f.name,val) + if f.value==val: + g_att+=' checked="checked"' + return self.render_element(e,g_att,v) + def render_tag_form_select(self,e,t_att,g_att,v): + f=self.eval_object(t_att["form-select"],v) + g_att+=' name="%s" class="%s"'%(f.name,f.css) + return self.render_element(e,g_att,v) + def render_tag_option(self,e,t_att,g_att,v): + f=self.eval_object(e.parentNode.getAttribute("t-form-select"),v) + val=t_att["option"] + g_att+=' value="%s"'%(val) + if f.value==val: + g_att+=' selected="selected"' + return self.render_element(e,g_att,v) + + # HTML Tags others + def render_tag_pager(self,e,t_att,g_att,v): + pre=t_att["pager"] + total=int(self.eval_str(t_att["total"],v)) + start=int(self.eval_str(t_att["start"],v)) + step=int(self.eval_str(t_att.get("step","100"),v)) + scope=int(self.eval_str(t_att.get("scope","5"),v)) + # Compute Pager + p=pre+"_" + d={} + d[p+"tot_size"]=total + d[p+"tot_page"]=tot_page=total/step + d[p+"win_start0"]=total and start + d[p+"win_start1"]=total and start+1 + d[p+"win_end0"]=max(0,min(start+step-1,total-1)) + d[p+"win_end1"]=min(start+step,total) + d[p+"win_page0"]=win_page=start/step + d[p+"win_page1"]=win_page+1 + d[p+"prev"]=(win_page!=0) + d[p+"prev_start"]=(win_page-1)*step + d[p+"next"]=(tot_page>=win_page+1) + d[p+"next_start"]=(win_page+1)*step + l=[] + begin=win_page-scope + end=win_page+scope + if begin<0: + end-=begin + if end>tot_page: + begin-=(end-tot_page) + i=max(0,begin) + while i<=min(end,tot_page) and total!=step: + l.append( { p+"page0":i, p+"page1":i+1, p+"start":i*step, p+"sel":(win_page==i) }) + i+=1 + d[p+"active"]=len(l)>1 + d[p+"list"]=l + # Update v + v.update(d) + return "" + +#---------------------------------------------------------- +# QWeb Simple Controller +#---------------------------------------------------------- +def qweb_control(self,jump='main',p=[]): + """ qweb_control(self,jump='main',p=[]): + A simple function to handle the controler part of your application. It + dispatch the control to the jump argument, while ensuring that prefix + function have been called. + + qweb_control replace '/' to '_' and strip '_' from the jump argument. + + name1 + name1_name2 + name1_name2_name3 + + """ + jump=jump.replace('/','_').strip('_') + if not hasattr(self,jump): + return 0 + done={} + todo=[] + while 1: + if jump!=None: + tmp="" + todo=[] + for i in jump.split("_"): + tmp+=i+"_"; + if not done.has_key(tmp[:-1]): + todo.append(tmp[:-1]) + jump=None + elif len(todo): + i=todo.pop(0) + done[i]=1 + if hasattr(self,i): + f=getattr(self,i) + r=f(*p) + if isinstance(r,types.StringType): + jump=r + else: + break + return 1 + +#---------------------------------------------------------- +# QWeb WSGI Request handler +#---------------------------------------------------------- +class QWebSession(dict): + def __init__(self,environ,**kw): + dict.__init__(self) + default={ + "path" : tempfile.gettempdir(), + "cookie_name" : "QWEBSID", + "cookie_lifetime" : 0, + "cookie_path" : '/', + "cookie_domain" : '', + "limit_cache" : 1, + "probability" : 0.01, + "maxlifetime" : 3600, + "disable" : 0, + } + for k,v in default.items(): + setattr(self,'session_%s'%k,kw.get(k,v)) + # Try to find session + self.session_found_cookie=0 + self.session_found_url=0 + self.session_found=0 + self.session_orig="" + # Try cookie + c=Cookie.SimpleCookie() + c.load(environ.get('HTTP_COOKIE', '')) + if c.has_key(self.session_cookie_name): + sid=c[self.session_cookie_name].value[:64] + if re.match('[a-f0-9]+$',sid) and self.session_load(sid): + self.session_id=sid + self.session_found_cookie=1 + self.session_found=1 + # Try URL + if not self.session_found_cookie: + mo=re.search('&%s=([a-f0-9]+)'%self.session_cookie_name,environ.get('QUERY_STRING','')) + if mo and self.session_load(mo.group(1)): + self.session_id=mo.group(1) + self.session_found_url=1 + self.session_found=1 + # New session + if not self.session_found: + self.session_id='%032x'%random.randint(1,2**128) + self.session_trans_sid="&%s=%s"%(self.session_cookie_name,self.session_id) + # Clean old session + if random.random() < self.session_probability: + self.session_clean() + def session_get_headers(self): + h=[] + if (not self.session_disable) and (len(self) or len(self.session_orig)): + self.session_save() + if not self.session_found_cookie: + c=Cookie.SimpleCookie() + c[self.session_cookie_name] = self.session_id + c[self.session_cookie_name]['path'] = self.session_cookie_path + if self.session_cookie_domain: + c[self.session_cookie_name]['domain'] = self.session_cookie_domain +# if self.session_cookie_lifetime: +# c[self.session_cookie_name]['expires'] = TODO date localtime or not, datetime.datetime(1970, 1, 1) + h.append(("Set-Cookie", c[self.session_cookie_name].OutputString())) + if self.session_limit_cache: + h.append(('Cache-Control','no-store, no-cache, must-revalidate, post-check=0, pre-check=0')) + h.append(('Expires','Thu, 19 Nov 1981 08:52:00 GMT')) + h.append(('Pragma','no-cache')) + return h + def session_load(self,sid): + fname=os.path.join(self.session_path,'qweb_sess_%s'%sid) + try: + orig=file(fname).read() + d=pickle.loads(orig) + except: + return + self.session_orig=orig + self.update(d) + return 1 + def session_save(self): + if not os.path.isdir(self.session_path): + os.makedirs(self.session_path) + fname=os.path.join(self.session_path,'qweb_sess_%s'%self.session_id) + try: + oldtime=os.path.getmtime(fname) + except OSError,IOError: + oldtime=0 + dump=pickle.dumps(self.copy()) + if (dump != self.session_orig) or (time.time() > oldtime+self.session_maxlifetime/4): + tmpname=os.path.join(self.session_path,'qweb_sess_%s_%x'%(self.session_id,random.randint(1,2**32))) + f=file(tmpname,'wb') + f.write(dump) + f.close() + if sys.platform=='win32' and os.path.isfile(fname): + os.remove(fname) + os.rename(tmpname,fname) + def session_clean(self): + t=time.time() + try: + for i in [os.path.join(self.session_path,i) for i in os.listdir(self.session_path) if i.startswith('qweb_sess_')]: + if (t > os.path.getmtime(i)+self.session_maxlifetime): + os.unlink(i) + except OSError,IOError: + pass +class QWebSessionMem(QWebSession): + def session_load(self,sid): + global _qweb_sessions + if not "_qweb_sessions" in globals(): + _qweb_sessions={} + if _qweb_sessions.has_key(sid): + self.session_orig=_qweb_sessions[sid] + self.update(self.session_orig) + return 1 + def session_save(self): + global _qweb_sessions + if not "_qweb_sessions" in globals(): + _qweb_sessions={} + _qweb_sessions[self.session_id]=self.copy() +class QWebSessionService: + def __init__(self, wsgiapp, url_rewrite=0): + self.wsgiapp=wsgiapp + self.url_rewrite_tags="a=href,area=href,frame=src,form=,fieldset=" + def __call__(self, environ, start_response): + # TODO + # use QWebSession to provide environ["qweb.session"] + return self.wsgiapp(environ,start_response) +class QWebDict(dict): + def __init__(self,*p): + dict.__init__(self,*p) + def __getitem__(self,key): + return self.get(key,"") + def int(self,key): + try: + return int(self.get(key,"0")) + except ValueError: + return 0 +class QWebListDict(dict): + def __init__(self,*p): + dict.__init__(self,*p) + def __getitem__(self,key): + return self.get(key,[]) + def appendlist(self,key,val): + if self.has_key(key): + self[key].append(val) + else: + self[key]=[val] + def get_qwebdict(self): + d=QWebDict() + for k,v in self.items(): + d[k]=v[-1] + return d +class QWebRequest: + """QWebRequest a WSGI request handler. + + QWebRequest is a WSGI request handler that feature GET, POST and POST + multipart methods, handles cookies and headers and provide a dict-like + SESSION Object (either on the filesystem or in memory). + + It is constructed with the environ and start_response WSGI arguments: + + req=qweb.QWebRequest(environ, start_response) + + req has the folowing attributes : + + req.environ standard WSGI dict (CGI and wsgi ones) + + Some CGI vars as attributes from environ for convenience: + + req.SCRIPT_NAME + req.PATH_INFO + req.REQUEST_URI + + Some computed value (also for convenience) + + req.FULL_URL full URL recontructed (http://host/query) + req.FULL_PATH (URL path before ?querystring) + + Dict constructed from querystring and POST datas, PHP-like. + + req.GET contains GET vars + req.POST contains POST vars + req.REQUEST contains merge of GET and POST + req.FILES contains uploaded files + req.GET_LIST req.POST_LIST req.REQUEST_LIST req.FILES_LIST multiple arguments versions + req.debug() returns an HTML dump of those vars + + A dict-like session object. + + req.SESSION the session start when the dict is not empty. + + Attribute for handling the response + + req.response_headers dict-like to set headers + req.response_cookies a SimpleCookie to set cookies + req.response_status a string to set the status like '200 OK' + + req.write() to write to the buffer + + req itselfs is an iterable object with the buffer, it will also also call + start_response automatically before returning anything via the iterator. + + To make it short, it means that you may use + + return req + + at the end of your request handling to return the reponse to any WSGI + application server. + """ + # + # This class contains part ripped from colubrid (with the permission of + # mitsuhiko) see http://wsgiarea.pocoo.org/colubrid/ + # + # - the class HttpHeaders + # - the method load_post_data (tuned version) + # + class HttpHeaders(object): + def __init__(self): + self.data = [('Content-Type', 'text/html')] + def __setitem__(self, key, value): + self.set(key, value) + def __delitem__(self, key): + self.remove(key) + def __contains__(self, key): + key = key.lower() + for k, v in self.data: + if k.lower() == key: + return True + return False + def add(self, key, value): + self.data.append((key, value)) + def remove(self, key, count=-1): + removed = 0 + data = [] + for _key, _value in self.data: + if _key.lower() != key.lower(): + if count > -1: + if removed >= count: + break + else: + removed += 1 + data.append((_key, _value)) + self.data = data + def clear(self): + self.data = [] + def set(self, key, value): + self.remove(key) + self.add(key, value) + def get(self, key=False, httpformat=False): + if not key: + result = self.data + else: + result = [] + for _key, _value in self.data: + if _key.lower() == key.lower(): + result.append((_key, _value)) + if httpformat: + return '\n'.join(['%s: %s' % item for item in result]) + return result + def load_post_data(self,environ,POST,FILES): + length = int(environ['CONTENT_LENGTH']) + DATA = environ['wsgi.input'].read(length) + if environ.get('CONTENT_TYPE', '').startswith('multipart'): + lines = ['Content-Type: %s' % environ.get('CONTENT_TYPE', '')] + for key, value in environ.items(): + if key.startswith('HTTP_'): + lines.append('%s: %s' % (key, value)) + raw = '\r\n'.join(lines) + '\r\n\r\n' + DATA + msg = email.message_from_string(raw) + for sub in msg.get_payload(): + if not isinstance(sub, email.Message.Message): + continue + name_dict = cgi.parse_header(sub['Content-Disposition'])[1] + if 'filename' in name_dict: + # Nested MIME Messages are not supported' + if type([]) == type(sub.get_payload()): + continue + if not name_dict['filename'].strip(): + continue + filename = name_dict['filename'] + # why not keep all the filename? because IE always send 'C:\documents and settings\blub\blub.png' + filename = filename[filename.rfind('\\') + 1:] + if 'Content-Type' in sub: + content_type = sub['Content-Type'] + else: + content_type = None + s = { "name":filename, "type":content_type, "data":sub.get_payload() } + FILES.appendlist(name_dict['name'], s) + else: + POST.appendlist(name_dict['name'], sub.get_payload()) + else: + POST.update(cgi.parse_qs(DATA,keep_blank_values=1)) + return DATA + + def __init__(self,environ,start_response,session=QWebSession): + self.environ=environ + self.start_response=start_response + self.buffer=[] + + self.SCRIPT_NAME = environ.get('SCRIPT_NAME', '') + self.PATH_INFO = environ.get('PATH_INFO', '') + # extensions: + self.FULL_URL = environ['FULL_URL'] = self.get_full_url(environ) + # REQUEST_URI is optional, fake it if absent + if not environ.has_key("REQUEST_URI"): + environ["REQUEST_URI"]=urllib.quote(self.SCRIPT_NAME+self.PATH_INFO) + if environ.get('QUERY_STRING'): + environ["REQUEST_URI"]+='?'+environ['QUERY_STRING'] + self.REQUEST_URI = environ["REQUEST_URI"] + # full quote url path before the ? + self.FULL_PATH = environ['FULL_PATH'] = self.REQUEST_URI.split('?')[0] + + self.request_cookies=Cookie.SimpleCookie() + self.request_cookies.load(environ.get('HTTP_COOKIE', '')) + + self.response_started=False + self.response_gzencode=False + self.response_cookies=Cookie.SimpleCookie() + # to delete a cookie use: c[key]['expires'] = datetime.datetime(1970, 1, 1) + self.response_headers=self.HttpHeaders() + self.response_status="200 OK" + + self.php=None + if self.environ.has_key("php"): + self.php=environ["php"] + self.SESSION=self.php._SESSION + self.GET=self.php._GET + self.POST=self.php._POST + self.REQUEST=self.php._ARG + self.FILES=self.php._FILES + else: + if isinstance(session,QWebSession): + self.SESSION=session + elif session: + self.SESSION=session(environ) + else: + self.SESSION=None + self.GET_LIST=QWebListDict(cgi.parse_qs(environ.get('QUERY_STRING', ''),keep_blank_values=1)) + self.POST_LIST=QWebListDict() + self.FILES_LIST=QWebListDict() + self.REQUEST_LIST=QWebListDict(self.GET_LIST) + if environ['REQUEST_METHOD'] == 'POST': + self.DATA=self.load_post_data(environ,self.POST_LIST,self.FILES_LIST) + self.REQUEST_LIST.update(self.POST_LIST) + self.GET=self.GET_LIST.get_qwebdict() + self.POST=self.POST_LIST.get_qwebdict() + self.FILES=self.FILES_LIST.get_qwebdict() + self.REQUEST=self.REQUEST_LIST.get_qwebdict() + def get_full_url(environ): + # taken from PEP 333 + if 'FULL_URL' in environ: + return environ['FULL_URL'] + url = environ['wsgi.url_scheme']+'://' + if environ.get('HTTP_HOST'): + url += environ['HTTP_HOST'] + else: + url += environ['SERVER_NAME'] + if environ['wsgi.url_scheme'] == 'https': + if environ['SERVER_PORT'] != '443': + url += ':' + environ['SERVER_PORT'] + else: + if environ['SERVER_PORT'] != '80': + url += ':' + environ['SERVER_PORT'] + if environ.has_key('REQUEST_URI'): + url += environ['REQUEST_URI'] + else: + url += urllib.quote(environ.get('SCRIPT_NAME', '')) + url += urllib.quote(environ.get('PATH_INFO', '')) + if environ.get('QUERY_STRING'): + url += '?' + environ['QUERY_STRING'] + return url + get_full_url=staticmethod(get_full_url) + def save_files(self): + for k,v in self.FILES.items(): + if not v.has_key("tmp_file"): + f=tempfile.NamedTemporaryFile() + f.write(v["data"]) + f.flush() + v["tmp_file"]=f + v["tmp_name"]=f.name + def debug(self): + body='' + for name,d in [ + ("GET",self.GET), ("POST",self.POST), ("REQUEST",self.REQUEST), ("FILES",self.FILES), + ("GET_LIST",self.GET_LIST), ("POST_LIST",self.POST_LIST), ("REQUEST_LIST",self.REQUEST_LIST), ("FILES_LIST",self.FILES_LIST), + ("SESSION",self.SESSION), ("environ",self.environ), + ]: + body+='<table border="1" width="100%" align="center">\n' + body+='<tr><th colspan="2" align="center">%s</th></tr>\n'%name + keys=d.keys() + keys.sort() + body+=''.join(['<tr><td>%s</td><td>%s</td></tr>\n'%(k,cgi.escape(repr(d[k]))) for k in keys]) + body+='</table><br><br>\n\n' + return body + def write(self,s): + self.buffer.append(s) + def echo(self,*s): + self.buffer.extend([str(i) for i in s]) + def response(self): + if not self.response_started: + if not self.php: + for k,v in self.FILES.items(): + if v.has_key("tmp_file"): + try: + v["tmp_file"].close() + except OSError: + pass + if self.response_gzencode and self.environ.get('HTTP_ACCEPT_ENCODING','').find('gzip')!=-1: + zbuf=StringIO.StringIO() + zfile=gzip.GzipFile(mode='wb', fileobj=zbuf) + zfile.write(''.join(self.buffer)) + zfile.close() + zbuf=zbuf.getvalue() + self.buffer=[zbuf] + self.response_headers['Content-Encoding']="gzip" + self.response_headers['Content-Length']=str(len(zbuf)) + headers = self.response_headers.get() + if isinstance(self.SESSION, QWebSession): + headers.extend(self.SESSION.session_get_headers()) + headers.extend([('Set-Cookie', self.response_cookies[i].OutputString()) for i in self.response_cookies]) + self.start_response(self.response_status, headers) + self.response_started=True + return self.buffer + def __iter__(self): + return self.response().__iter__() + def http_redirect(self,url,permanent=1): + if permanent: + self.response_status="301 Moved Permanently" + else: + self.response_status="302 Found" + self.response_headers["Location"]=url + def http_404(self,msg="<h1>404 Not Found</h1>"): + self.response_status="404 Not Found" + if msg: + self.write(msg) + def http_download(self,fname,fstr,partial=0): +# allow fstr to be a file-like object +# if parital: +# say accept ranages +# parse range headers... +# if range: +# header("HTTP/1.1 206 Partial Content"); +# header("Content-Range: bytes $offset-".($fsize-1)."/".$fsize); +# header("Content-Length: ".($fsize-$offset)); +# fseek($fd,$offset); +# else: + self.response_headers["Content-Type"]="application/octet-stream" + self.response_headers["Content-Disposition"]="attachment; filename=\"%s\""%fname + self.response_headers["Content-Transfer-Encoding"]="binary" + self.response_headers["Content-Length"]="%d"%len(fstr) + self.write(fstr) + +#---------------------------------------------------------- +# QWeb WSGI HTTP Server to run any WSGI app +# autorun, run an app as FCGI or CGI otherwise launch the server +#---------------------------------------------------------- +class QWebWSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def log_message(self,*p): + if self.server.log: + return BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,*p) + def address_string(self): + return self.client_address[0] + def start_response(self,status,headers): + l=status.split(' ',1) + self.send_response(int(l[0]),l[1]) + ctype_sent=0 + for i in headers: + if i[0].lower()=="content-type": + ctype_sent=1 + self.send_header(*i) + if not ctype_sent: + self.send_header("Content-type", "text/html") + self.end_headers() + return self.write + def write(self,data): + try: + self.wfile.write(data) + except (socket.error, socket.timeout),e: + print e + def bufferon(self): + if not getattr(self,'wfile_buf',0): + self.wfile_buf=1 + self.wfile_bak=self.wfile + self.wfile=StringIO.StringIO() + def bufferoff(self): + if self.wfile_buf: + buf=self.wfile + self.wfile=self.wfile_bak + self.write(buf.getvalue()) + self.wfile_buf=0 + def serve(self,type): + path_info, parameters, query = urlparse.urlparse(self.path)[2:5] + environ = { + 'wsgi.version': (1,0), + 'wsgi.url_scheme': 'http', + 'wsgi.input': self.rfile, + 'wsgi.errors': sys.stderr, + 'wsgi.multithread': 0, + 'wsgi.multiprocess': 0, + 'wsgi.run_once': 0, + 'REQUEST_METHOD': self.command, + 'SCRIPT_NAME': '', + 'QUERY_STRING': query, + 'CONTENT_TYPE': self.headers.get('Content-Type', ''), + 'CONTENT_LENGTH': self.headers.get('Content-Length', ''), + 'REMOTE_ADDR': self.client_address[0], + 'REMOTE_PORT': str(self.client_address[1]), + 'SERVER_NAME': self.server.server_address[0], + 'SERVER_PORT': str(self.server.server_address[1]), + 'SERVER_PROTOCOL': self.request_version, + # extention + 'FULL_PATH': self.path, + 'qweb.mode': 'standalone', + } + if path_info: + environ['PATH_INFO'] = urllib.unquote(path_info) + for key, value in self.headers.items(): + environ['HTTP_' + key.upper().replace('-', '_')] = value + # Hack to avoid may TCP packets + self.bufferon() + appiter=self.server.wsgiapp(environ, self.start_response) + for data in appiter: + self.write(data) + self.bufferoff() + self.bufferoff() + def do_GET(self): + self.serve('GET') + def do_POST(self): + self.serve('GET') +class QWebWSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): + """ QWebWSGIServer + qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1) + A WSGI HTTP server threaded or not and a function to automatically run your + app according to the environement (either standalone, CGI or FastCGI). + + This feature is called QWeb autorun. If you want to To use it on your + application use the following lines at the end of the main application + python file: + + if __name__ == '__main__': + qweb.qweb_wsgi_autorun(your_wsgi_app) + + this function will select the approriate running mode according to the + calling environement (http-server, FastCGI or CGI). + """ + def __init__(self, wsgiapp, ip, port, threaded=1, log=1): + BaseHTTPServer.HTTPServer.__init__(self, (ip, port), QWebWSGIHandler) + self.wsgiapp = wsgiapp + self.threaded = threaded + self.log = log + def process_request(self,*p): + if self.threaded: + return SocketServer.ThreadingMixIn.process_request(self,*p) + else: + return BaseHTTPServer.HTTPServer.process_request(self,*p) +def qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1,log=1,callback_ready=None): + if sys.platform=='win32': + fcgi=0 + else: + fcgi=1 + sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM) + try: + sock.getpeername() + except socket.error, e: + if e[0] == errno.ENOTSOCK: + fcgi=0 + if fcgi or os.environ.has_key('REQUEST_METHOD'): + import fcgi + fcgi.WSGIServer(wsgiapp,multithreaded=False).run() + else: + if log: + print 'Serving on %s:%d'%(ip,port) + s=QWebWSGIServer(wsgiapp,ip=ip,port=port,threaded=threaded,log=log) + if callback_ready: + callback_ready() + try: + s.serve_forever() + except KeyboardInterrupt,e: + sys.excepthook(*sys.exc_info()) + +#---------------------------------------------------------- +# Qweb Documentation +#---------------------------------------------------------- +def qweb_doc(): + body=__doc__ + for i in [QWebXml ,QWebHtml ,QWebForm ,QWebURL ,qweb_control ,QWebRequest ,QWebSession ,QWebWSGIServer ,qweb_wsgi_autorun]: + n=i.__name__ + d=i.__doc__ + body+='\n\n%s\n%s\n\n%s'%(n,'-'*len(n),d) + return body + + print qweb_doc() + +# diff --git a/tools/ajaxterm/sarissa.js b/tools/ajaxterm/sarissa.js new file mode 100644 index 000000000..6d13aa2e2 --- /dev/null +++ b/tools/ajaxterm/sarissa.js @@ -0,0 +1,647 @@ +/** + * ==================================================================== + * About + * ==================================================================== + * Sarissa is an ECMAScript library acting as a cross-browser wrapper for native XML APIs. + * The library supports Gecko based browsers like Mozilla and Firefox, + * Internet Explorer (5.5+ with MSXML3.0+), Konqueror, Safari and a little of Opera + * @version 0.9.6.1 + * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net + * ==================================================================== + * Licence + * ==================================================================== + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or + * the GNU Lesser General Public License version 2.1 as published by + * the Free Software Foundation (your choice between the two). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License or GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * or GNU Lesser General Public License along with this program; if not, + * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * or visit http://www.gnu.org + * + */ +/** + * <p>Sarissa is a utility class. Provides "static" methods for DOMDocument and + * XMLHTTP objects, DOM Node serializatrion to XML strings and other goodies.</p> + * @constructor + */ +function Sarissa(){}; +/** @private */ +Sarissa.PARSED_OK = "Document contains no parsing errors"; +/** + * Tells you whether transformNode and transformNodeToObject are available. This functionality + * is contained in sarissa_ieemu_xslt.js and is deprecated. If you want to control XSLT transformations + * use the XSLTProcessor + * @deprecated + * @type boolean + */ +Sarissa.IS_ENABLED_TRANSFORM_NODE = false; +/** + * tells you whether XMLHttpRequest (or equivalent) is available + * @type boolean + */ +Sarissa.IS_ENABLED_XMLHTTP = false; +/** + * tells you whether selectNodes/selectSingleNode is available + * @type boolean + */ +Sarissa.IS_ENABLED_SELECT_NODES = false; +var _sarissa_iNsCounter = 0; +var _SARISSA_IEPREFIX4XSLPARAM = ""; +var _SARISSA_HAS_DOM_IMPLEMENTATION = document.implementation && true; +var _SARISSA_HAS_DOM_CREATE_DOCUMENT = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.createDocument; +var _SARISSA_HAS_DOM_FEATURE = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.hasFeature; +var _SARISSA_IS_MOZ = _SARISSA_HAS_DOM_CREATE_DOCUMENT && _SARISSA_HAS_DOM_FEATURE; +var _SARISSA_IS_SAFARI = (navigator.userAgent && navigator.vendor && (navigator.userAgent.toLowerCase().indexOf("applewebkit") != -1 || navigator.vendor.indexOf("Apple") != -1)); +var _SARISSA_IS_IE = document.all && window.ActiveXObject && navigator.userAgent.toLowerCase().indexOf("msie") > -1 && navigator.userAgent.toLowerCase().indexOf("opera") == -1; +if(!window.Node || !window.Node.ELEMENT_NODE){ + var Node = {ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12}; +}; + +// IE initialization +if(_SARISSA_IS_IE){ + // for XSLT parameter names, prefix needed by IE + _SARISSA_IEPREFIX4XSLPARAM = "xsl:"; + // used to store the most recent ProgID available out of the above + var _SARISSA_DOM_PROGID = ""; + var _SARISSA_XMLHTTP_PROGID = ""; + /** + * Called when the Sarissa_xx.js file is parsed, to pick most recent + * ProgIDs for IE, then gets destroyed. + * @param idList an array of MSXML PROGIDs from which the most recent will be picked for a given object + * @param enabledList an array of arrays where each array has two items; the index of the PROGID for which a certain feature is enabled + */ + pickRecentProgID = function (idList, enabledList){ + // found progID flag + var bFound = false; + for(var i=0; i < idList.length && !bFound; i++){ + try{ + var oDoc = new ActiveXObject(idList[i]); + o2Store = idList[i]; + bFound = true; + for(var j=0;j<enabledList.length;j++) + if(i <= enabledList[j][1]) + Sarissa["IS_ENABLED_"+enabledList[j][0]] = true; + }catch (objException){ + // trap; try next progID + }; + }; + if (!bFound) + throw "Could not retreive a valid progID of Class: " + idList[idList.length-1]+". (original exception: "+e+")"; + idList = null; + return o2Store; + }; + // pick best available MSXML progIDs + _SARISSA_DOM_PROGID = pickRecentProgID(["Msxml2.DOMDocument.5.0", "Msxml2.DOMDocument.4.0", "Msxml2.DOMDocument.3.0", "MSXML2.DOMDocument", "MSXML.DOMDocument", "Microsoft.XMLDOM"], [["SELECT_NODES", 2],["TRANSFORM_NODE", 2]]); + _SARISSA_XMLHTTP_PROGID = pickRecentProgID(["Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"], [["XMLHTTP", 4]]); + _SARISSA_THREADEDDOM_PROGID = pickRecentProgID(["Msxml2.FreeThreadedDOMDocument.5.0", "MSXML2.FreeThreadedDOMDocument.4.0", "MSXML2.FreeThreadedDOMDocument.3.0"]); + _SARISSA_XSLTEMPLATE_PROGID = pickRecentProgID(["Msxml2.XSLTemplate.5.0", "Msxml2.XSLTemplate.4.0", "MSXML2.XSLTemplate.3.0"], [["XSLTPROC", 2]]); + // we dont need this anymore + pickRecentProgID = null; + //============================================ + // Factory methods (IE) + //============================================ + // see non-IE version + Sarissa.getDomDocument = function(sUri, sName){ + var oDoc = new ActiveXObject(_SARISSA_DOM_PROGID); + // if a root tag name was provided, we need to load it in the DOM + // object + if (sName){ + // if needed, create an artifical namespace prefix the way Moz + // does + if (sUri){ + oDoc.loadXML("<a" + _sarissa_iNsCounter + ":" + sName + " xmlns:a" + _sarissa_iNsCounter + "=\"" + sUri + "\" />"); + // don't use the same prefix again + ++_sarissa_iNsCounter; + } + else + oDoc.loadXML("<" + sName + "/>"); + }; + return oDoc; + }; + // see non-IE version + Sarissa.getParseErrorText = function (oDoc) { + var parseErrorText = Sarissa.PARSED_OK; + if(oDoc.parseError != 0){ + parseErrorText = "XML Parsing Error: " + oDoc.parseError.reason + + "\nLocation: " + oDoc.parseError.url + + "\nLine Number " + oDoc.parseError.line + ", Column " + + oDoc.parseError.linepos + + ":\n" + oDoc.parseError.srcText + + "\n"; + for(var i = 0; i < oDoc.parseError.linepos;i++){ + parseErrorText += "-"; + }; + parseErrorText += "^\n"; + }; + return parseErrorText; + }; + // see non-IE version + Sarissa.setXpathNamespaces = function(oDoc, sNsSet) { + oDoc.setProperty("SelectionLanguage", "XPath"); + oDoc.setProperty("SelectionNamespaces", sNsSet); + }; + /** + * Basic implementation of Mozilla's XSLTProcessor for IE. + * Reuses the same XSLT stylesheet for multiple transforms + * @constructor + */ + XSLTProcessor = function(){ + this.template = new ActiveXObject(_SARISSA_XSLTEMPLATE_PROGID); + this.processor = null; + }; + /** + * Impoprts the given XSLT DOM and compiles it to a reusable transform + * @argument xslDoc The XSLT DOMDocument to import + */ + XSLTProcessor.prototype.importStylesheet = function(xslDoc){ + // convert stylesheet to free threaded + var converted = new ActiveXObject(_SARISSA_THREADEDDOM_PROGID); + converted.loadXML(xslDoc.xml); + this.template.stylesheet = converted; + this.processor = this.template.createProcessor(); + // (re)set default param values + this.paramsSet = new Array(); + }; + /** + * Transform the given XML DOM + * @argument sourceDoc The XML DOMDocument to transform + * @return The transformation result as a DOM Document + */ + XSLTProcessor.prototype.transformToDocument = function(sourceDoc){ + this.processor.input = sourceDoc; + var outDoc = new ActiveXObject(_SARISSA_DOM_PROGID); + this.processor.output = outDoc; + this.processor.transform(); + return outDoc; + }; + /** + * Set global XSLT parameter of the imported stylesheet + * @argument nsURI The parameter namespace URI + * @argument name The parameter base name + * @argument value The new parameter value + */ + XSLTProcessor.prototype.setParameter = function(nsURI, name, value){ + /* nsURI is optional but cannot be null */ + if(nsURI){ + this.processor.addParameter(name, value, nsURI); + }else{ + this.processor.addParameter(name, value); + }; + /* update updated params for getParameter */ + if(!this.paramsSet[""+nsURI]){ + this.paramsSet[""+nsURI] = new Array(); + }; + this.paramsSet[""+nsURI][name] = value; + }; + /** + * Gets a parameter if previously set by setParameter. Returns null + * otherwise + * @argument name The parameter base name + * @argument value The new parameter value + * @return The parameter value if reviously set by setParameter, null otherwise + */ + XSLTProcessor.prototype.getParameter = function(nsURI, name){ + nsURI = nsURI || ""; + if(nsURI in this.paramsSet && name in this.paramsSet[nsURI]){ + return this.paramsSet[nsURI][name]; + }else{ + return null; + }; + }; +} +else{ /* end IE initialization, try to deal with real browsers now ;-) */ + if(_SARISSA_HAS_DOM_CREATE_DOCUMENT){ + /** + * <p>Ensures the document was loaded correctly, otherwise sets the + * parseError to -1 to indicate something went wrong. Internal use</p> + * @private + */ + Sarissa.__handleLoad__ = function(oDoc){ + if (!oDoc.documentElement || oDoc.documentElement.tagName == "parsererror") + oDoc.parseError = -1; + Sarissa.__setReadyState__(oDoc, 4); + }; + /** + * <p>Attached by an event handler to the load event. Internal use.</p> + * @private + */ + _sarissa_XMLDocument_onload = function(){ + Sarissa.__handleLoad__(this); + }; + /** + * <p>Sets the readyState property of the given DOM Document object. + * Internal use.</p> + * @private + * @argument oDoc the DOM Document object to fire the + * readystatechange event + * @argument iReadyState the number to change the readystate property to + */ + Sarissa.__setReadyState__ = function(oDoc, iReadyState){ + oDoc.readyState = iReadyState; + if (oDoc.onreadystatechange != null && typeof oDoc.onreadystatechange == "function") + oDoc.onreadystatechange(); + }; + Sarissa.getDomDocument = function(sUri, sName){ + var oDoc = document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null); + oDoc.addEventListener("load", _sarissa_XMLDocument_onload, false); + return oDoc; + }; + if(false && window.XMLDocument){ + /** + * <p>Emulate IE's onreadystatechange attribute</p> + */ + XMLDocument.prototype.onreadystatechange = null; + /** + * <p>Emulates IE's readyState property, which always gives an integer from 0 to 4:</p> + * <ul><li>1 == LOADING,</li> + * <li>2 == LOADED,</li> + * <li>3 == INTERACTIVE,</li> + * <li>4 == COMPLETED</li></ul> + */ + XMLDocument.prototype.readyState = 0; + /** + * <p>Emulate IE's parseError attribute</p> + */ + XMLDocument.prototype.parseError = 0; + + // NOTE: setting async to false will only work with documents + // called over HTTP (meaning a server), not the local file system, + // unless you are using Moz 1.4+. + // BTW the try>catch block is for 1.4; I haven't found a way to check if + // the property is implemented without + // causing an error and I dont want to use user agent stuff for that... + var _SARISSA_SYNC_NON_IMPLEMENTED = false;// ("async" in XMLDocument.prototype) ? false: true; + /** + * <p>Keeps a handle to the original load() method. Internal use and only + * if Mozilla version is lower than 1.4</p> + * @private + */ + XMLDocument.prototype._sarissa_load = XMLDocument.prototype.load; + + /** + * <p>Overrides the original load method to provide synchronous loading for + * Mozilla versions prior to 1.4, using an XMLHttpRequest object (if + * async is set to false)</p> + * @returns the DOM Object as it was before the load() call (may be empty) + */ + XMLDocument.prototype.load = function(sURI) { + var oDoc = document.implementation.createDocument("", "", null); + Sarissa.copyChildNodes(this, oDoc); + this.parseError = 0; + Sarissa.__setReadyState__(this, 1); + try { + if(this.async == false && _SARISSA_SYNC_NON_IMPLEMENTED) { + var tmp = new XMLHttpRequest(); + tmp.open("GET", sURI, false); + tmp.send(null); + Sarissa.__setReadyState__(this, 2); + Sarissa.copyChildNodes(tmp.responseXML, this); + Sarissa.__setReadyState__(this, 3); + } + else { + this._sarissa_load(sURI); + }; + } + catch (objException) { + this.parseError = -1; + } + finally { + if(this.async == false){ + Sarissa.__handleLoad__(this); + }; + }; + return oDoc; + }; + + + }//if(window.XMLDocument) + else if(document.implementation && document.implementation.hasFeature && document.implementation.hasFeature('LS', '3.0')){ + Document.prototype.async = true; + Document.prototype.onreadystatechange = null; + Document.prototype.parseError = 0; + Document.prototype.load = function(sURI) { + var parser = document.implementation.createLSParser(this.async ? document.implementation.MODE_ASYNCHRONOUS : document.implementation.MODE_SYNCHRONOUS, null); + if(this.async){ + var self = this; + parser.addEventListener("load", + function(e) { + self.readyState = 4; + Sarissa.copyChildNodes(e.newDocument, self.documentElement, false); + self.onreadystatechange.call(); + }, + false); + }; + try { + var oDoc = parser.parseURI(sURI); + } + catch(e){ + this.parseError = -1; + }; + if(!this.async) + Sarissa.copyChildNodes(oDoc, this.documentElement, false); + return oDoc; + }; + /** + * <p>Factory method to obtain a new DOM Document object</p> + * @argument sUri the namespace of the root node (if any) + * @argument sUri the local name of the root node (if any) + * @returns a new DOM Document + */ + Sarissa.getDomDocument = function(sUri, sName){ + return document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null); + }; + }; + };//if(_SARISSA_HAS_DOM_CREATE_DOCUMENT) +}; +//========================================== +// Common stuff +//========================================== +if(!window.DOMParser){ + /* + * DOMParser is a utility class, used to construct DOMDocuments from XML strings + * @constructor + */ + DOMParser = function() { + }; + if(_SARISSA_IS_SAFARI){ + /** + * Construct a new DOM Document from the given XMLstring + * @param sXml the given XML string + * @param contentType the content type of the document the given string represents (one of text/xml, application/xml, application/xhtml+xml). + * @return a new DOM Document from the given XML string + */ + DOMParser.prototype.parseFromString = function(sXml, contentType){ + if(contentType.toLowerCase() != "application/xml"){ + throw "Cannot handle content type: \"" + contentType + "\""; + }; + var xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", "data:text/xml;charset=utf-8," + encodeURIComponent(str), false); + xmlhttp.send(null); + return xmlhttp.responseXML; + }; + }else if(Sarissa.getDomDocument && Sarissa.getDomDocument() && "loadXML" in Sarissa.getDomDocument()){ + DOMParser.prototype.parseFromString = function(sXml, contentType){ + var doc = Sarissa.getDomDocument(); + doc.loadXML(sXml); + return doc; + }; + }; +}; + +if(window.XMLHttpRequest){ + Sarissa.IS_ENABLED_XMLHTTP = true; +} +else if(_SARISSA_IS_IE){ + /** + * Emulate XMLHttpRequest + * @constructor + */ + XMLHttpRequest = function() { + return new ActiveXObject(_SARISSA_XMLHTTP_PROGID); + }; + Sarissa.IS_ENABLED_XMLHTTP = true; +}; + +if(!window.document.importNode && _SARISSA_IS_IE){ + try{ + /** + * Implements importNode for the current window document in IE using innerHTML. + * Testing showed that DOM was multiple times slower than innerHTML for this, + * sorry folks. If you encounter trouble (who knows what IE does behind innerHTML) + * please gimme a call. + * @param oNode the Node to import + * @param bChildren whether to include the children of oNode + * @returns the imported node for further use + */ + window.document.importNode = function(oNode, bChildren){ + var importNode = document.createElement("div"); + if(bChildren) + importNode.innerHTML = Sarissa.serialize(oNode); + else + importNode.innerHTML = Sarissa.serialize(oNode.cloneNode(false)); + return importNode.firstChild; + }; + }catch(e){}; +}; +if(!Sarissa.getParseErrorText){ + /** + * <p>Returns a human readable description of the parsing error. Usefull + * for debugging. Tip: append the returned error string in a <pre> + * element if you want to render it.</p> + * <p>Many thanks to Christian Stocker for the initial patch.</p> + * @argument oDoc The target DOM document + * @returns The parsing error description of the target Document in + * human readable form (preformated text) + */ + Sarissa.getParseErrorText = function (oDoc){ + var parseErrorText = Sarissa.PARSED_OK; + if(oDoc && oDoc.parseError && oDoc.parseError != 0){ + /*moz*/ + if(oDoc.documentElement.tagName == "parsererror"){ + parseErrorText = oDoc.documentElement.firstChild.data; + parseErrorText += "\n" + oDoc.documentElement.firstChild.nextSibling.firstChild.data; + }/*konq*/ + else{ + parseErrorText = Sarissa.getText(oDoc.documentElement);/*.getElementsByTagName("h1")[0], false) + "\n"; + parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("body")[0], false) + "\n"; + parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("pre")[0], false);*/ + }; + }; + return parseErrorText; + }; +}; +Sarissa.getText = function(oNode, deep){ + var s = ""; + var nodes = oNode.childNodes; + for(var i=0; i < nodes.length; i++){ + var node = nodes[i]; + var nodeType = node.nodeType; + if(nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE){ + s += node.data; + }else if(deep == true + && (nodeType == Node.ELEMENT_NODE + || nodeType == Node.DOCUMENT_NODE + || nodeType == Node.DOCUMENT_FRAGMENT_NODE)){ + s += Sarissa.getText(node, true); + }; + }; + return s; +}; +if(window.XMLSerializer){ + /** + * <p>Factory method to obtain the serialization of a DOM Node</p> + * @returns the serialized Node as an XML string + */ + Sarissa.serialize = function(oDoc){ + var s = null; + if(oDoc){ + s = oDoc.innerHTML?oDoc.innerHTML:(new XMLSerializer()).serializeToString(oDoc); + }; + return s; + }; +}else{ + if(Sarissa.getDomDocument && (Sarissa.getDomDocument("","foo", null)).xml){ + // see non-IE version + Sarissa.serialize = function(oDoc) { + var s = null; + if(oDoc){ + s = oDoc.innerHTML?oDoc.innerHTML:oDoc.xml; + }; + return s; + }; + /** + * Utility class to serialize DOM Node objects to XML strings + * @constructor + */ + XMLSerializer = function(){}; + /** + * Serialize the given DOM Node to an XML string + * @param oNode the DOM Node to serialize + */ + XMLSerializer.prototype.serializeToString = function(oNode) { + return oNode.xml; + }; + }; +}; + +/** + * strips tags from a markup string + */ +Sarissa.stripTags = function (s) { + return s.replace(/<[^>]+>/g,""); +}; +/** + * <p>Deletes all child nodes of the given node</p> + * @argument oNode the Node to empty + */ +Sarissa.clearChildNodes = function(oNode) { + // need to check for firstChild due to opera 8 bug with hasChildNodes + while(oNode.firstChild){ + oNode.removeChild(oNode.firstChild); + }; +}; +/** + * <p> Copies the childNodes of nodeFrom to nodeTo</p> + * <p> <b>Note:</b> The second object's original content is deleted before + * the copy operation, unless you supply a true third parameter</p> + * @argument nodeFrom the Node to copy the childNodes from + * @argument nodeTo the Node to copy the childNodes to + * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is false + */ +Sarissa.copyChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) { + if((!nodeFrom) || (!nodeTo)){ + throw "Both source and destination nodes must be provided"; + }; + if(!bPreserveExisting){ + Sarissa.clearChildNodes(nodeTo); + }; + var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument; + var nodes = nodeFrom.childNodes; + if(ownerDoc.importNode && (!_SARISSA_IS_IE)) { + for(var i=0;i < nodes.length;i++) { + nodeTo.appendChild(ownerDoc.importNode(nodes[i], true)); + }; + } + else{ + for(var i=0;i < nodes.length;i++) { + nodeTo.appendChild(nodes[i].cloneNode(true)); + }; + }; +}; + +/** + * <p> Moves the childNodes of nodeFrom to nodeTo</p> + * <p> <b>Note:</b> The second object's original content is deleted before + * the move operation, unless you supply a true third parameter</p> + * @argument nodeFrom the Node to copy the childNodes from + * @argument nodeTo the Node to copy the childNodes to + * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is + */ +Sarissa.moveChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) { + if((!nodeFrom) || (!nodeTo)){ + throw "Both source and destination nodes must be provided"; + }; + if(!bPreserveExisting){ + Sarissa.clearChildNodes(nodeTo); + }; + var nodes = nodeFrom.childNodes; + // if within the same doc, just move, else copy and delete + if(nodeFrom.ownerDocument == nodeTo.ownerDocument){ + while(nodeFrom.firstChild){ + nodeTo.appendChild(nodeFrom.firstChild); + }; + }else{ + var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument; + if(ownerDoc.importNode && (!_SARISSA_IS_IE)) { + for(var i=0;i < nodes.length;i++) { + nodeTo.appendChild(ownerDoc.importNode(nodes[i], true)); + }; + }else{ + for(var i=0;i < nodes.length;i++) { + nodeTo.appendChild(nodes[i].cloneNode(true)); + }; + }; + Sarissa.clearChildNodes(nodeFrom); + }; +}; + +/** + * <p>Serialize any object to an XML string. All properties are serialized using the property name + * as the XML element name. Array elements are rendered as <code>array-item</code> elements, + * using their index/key as the value of the <code>key</code> attribute.</p> + * @argument anyObject the object to serialize + * @argument objectName a name for that object + * @return the XML serializationj of the given object as a string + */ +Sarissa.xmlize = function(anyObject, objectName, indentSpace){ + indentSpace = indentSpace?indentSpace:''; + var s = indentSpace + '<' + objectName + '>'; + var isLeaf = false; + if(!(anyObject instanceof Object) || anyObject instanceof Number || anyObject instanceof String + || anyObject instanceof Boolean || anyObject instanceof Date){ + s += Sarissa.escape(""+anyObject); + isLeaf = true; + }else{ + s += "\n"; + var itemKey = ''; + var isArrayItem = anyObject instanceof Array; + for(var name in anyObject){ + s += Sarissa.xmlize(anyObject[name], (isArrayItem?"array-item key=\""+name+"\"":name), indentSpace + " "); + }; + s += indentSpace; + }; + return s += (objectName.indexOf(' ')!=-1?"</array-item>\n":"</" + objectName + ">\n"); +}; + +/** + * Escape the given string chacters that correspond to the five predefined XML entities + * @param sXml the string to escape + */ +Sarissa.escape = function(sXml){ + return sXml.replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +}; + +/** + * Unescape the given string. This turns the occurences of the predefined XML + * entities to become the characters they represent correspond to the five predefined XML entities + * @param sXml the string to unescape + */ +Sarissa.unescape = function(sXml){ + return sXml.replace(/'/g,"'") + .replace(/"/g,"\"") + .replace(/>/g,">") + .replace(/</g,"<") + .replace(/&/g,"&"); +}; +// EOF diff --git a/tools/ajaxterm/sarissa_dhtml.js b/tools/ajaxterm/sarissa_dhtml.js new file mode 100644 index 000000000..2d85c817e --- /dev/null +++ b/tools/ajaxterm/sarissa_dhtml.js @@ -0,0 +1,105 @@ +/** + * ==================================================================== + * About + * ==================================================================== + * Sarissa cross browser XML library - AJAX module + * @version 0.9.6.1 + * @author: Copyright Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net + * + * This module contains some convinient AJAX tricks based on Sarissa + * + * ==================================================================== + * Licence + * ==================================================================== + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 or + * the GNU Lesser General Public License version 2.1 as published by + * the Free Software Foundation (your choice between the two). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License or GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * or GNU Lesser General Public License along with this program; if not, + * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * or visit http://www.gnu.org + * + */ +/** + * Update an element with response of a GET request on the given URL. + * @addon + * @param sFromUrl the URL to make the request to + * @param oTargetElement the element to update + * @param xsltproc (optional) the transformer to use on the returned + * content before updating the target element with it + */ +Sarissa.updateContentFromURI = function(sFromUrl, oTargetElement, xsltproc) { + try{ + oTargetElement.style.cursor = "wait"; + var xmlhttp = new XMLHttpRequest(); + xmlhttp.open("GET", sFromUrl); + function sarissa_dhtml_loadHandler() { + if (xmlhttp.readyState == 4) { + oTargetElement.style.cursor = "auto"; + Sarissa.updateContentFromNode(xmlhttp.responseXML, oTargetElement, xsltproc); + }; + }; + xmlhttp.onreadystatechange = sarissa_dhtml_loadHandler; + xmlhttp.send(null); + oTargetElement.style.cursor = "auto"; + } + catch(e){ + oTargetElement.style.cursor = "auto"; + throw e; + }; +}; + +/** + * Update an element's content with the given DOM node. + * @addon + * @param sFromUrl the URL to make the request to + * @param oTargetElement the element to update + * @param xsltproc (optional) the transformer to use on the given + * DOM node before updating the target element with it + */ +Sarissa.updateContentFromNode = function(oNode, oTargetElement, xsltproc) { + try { + oTargetElement.style.cursor = "wait"; + Sarissa.clearChildNodes(oTargetElement); + // check for parsing errors + var ownerDoc = oNode.nodeType == Node.DOCUMENT_NODE?oNode:oNode.ownerDocument; + if(ownerDoc.parseError && ownerDoc.parseError != 0) { + var pre = document.createElement("pre"); + pre.appendChild(document.createTextNode(Sarissa.getParseErrorText(ownerDoc))); + oTargetElement.appendChild(pre); + } + else { + // transform if appropriate + if(xsltproc) { + oNode = xsltproc.transformToDocument(oNode); + }; + // be smart, maybe the user wants to display the source instead + if(oTargetElement.tagName.toLowerCase == "textarea" || oTargetElement.tagName.toLowerCase == "input") { + oTargetElement.value = Sarissa.serialize(oNode); + } + else { + // ok that was not smart; it was paranoid. Keep up the good work by trying to use DOM instead of innerHTML + if(oNode.nodeType == Node.DOCUMENT_NODE || oNode.ownerDocument.documentElement == oNode) { + oTargetElement.innerHTML = Sarissa.serialize(oNode); + } + else{ + oTargetElement.appendChild(oTargetElement.ownerDocument.importNode(oNode, true)); + }; + }; + }; + } + catch(e) { + throw e; + } + finally{ + oTargetElement.style.cursor = "auto"; + }; +}; + diff --git a/tools/euca-get-ajax-console b/tools/euca-get-ajax-console new file mode 100755 index 000000000..37060e74f --- /dev/null +++ b/tools/euca-get-ajax-console @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# pylint: disable-msg=C0103 +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Euca add-on to use ajax console""" + +import getopt +import os +import sys + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +import boto +import nova +from boto.ec2.connection import EC2Connection +from euca2ools import Euca2ool, InstanceValidationError, Util, ConnectionFailed + +usage_string = """ +Retrieves a url to an ajax console terminal + +euca-get-ajax-console [-h, --help] [--version] [--debug] instance_id + +REQUIRED PARAMETERS + +instance_id: unique identifier for the instance show the console output for. + +OPTIONAL PARAMETERS + +""" + + +# This class extends boto to add AjaxConsole functionality +class NovaEC2Connection(EC2Connection): + + def get_ajax_console(self, instance_id): + """ + Retrieves a console connection for the specified instance. + + :type instance_id: string + :param instance_id: The instance ID of a running instance on the cloud. + + :rtype: :class:`AjaxConsole` + """ + + class AjaxConsole: + def __init__(self, parent=None): + self.parent = parent + self.instance_id = None + self.url = None + + def startElement(self, name, attrs, connection): + return None + + def endElement(self, name, value, connection): + if name == 'instanceId': + self.instance_id = value + elif name == 'url': + self.url = value + else: + setattr(self, name, value) + + params = {} + self.build_list_params(params, [instance_id], 'InstanceId') + return self.get_object('GetAjaxConsole', params, AjaxConsole) + pass + + +def override_connect_ec2(aws_access_key_id=None, + aws_secret_access_key=None, **kwargs): + return NovaEC2Connection(aws_access_key_id, + aws_secret_access_key, **kwargs) + +# override boto's connect_ec2 method, so that we can use NovaEC2Connection +boto.connect_ec2 = override_connect_ec2 + + +def usage(status=1): + print usage_string + Util().usage() + sys.exit(status) + + +def version(): + print Util().version() + sys.exit() + + +def display_console_output(console_output): + print console_output.instance_id + print console_output.timestamp + print console_output.output + + +def display_ajax_console_output(console_output): + print console_output.url + + +def main(): + try: + euca = Euca2ool() + except Exception, e: + print e + usage() + + instance_id = None + + for name, value in euca.opts: + if name in ('-h', '--help'): + usage(0) + elif name == '--version': + version() + elif name == '--debug': + debug = True + + for arg in euca.args: + instance_id = arg + break + + if instance_id: + try: + euca.validate_instance_id(instance_id) + except InstanceValidationError: + print 'Invalid instance id' + sys.exit(1) + + try: + euca_conn = euca.make_connection() + except ConnectionFailed, e: + print e.message + sys.exit(1) + try: + console_output = euca_conn.get_ajax_console(instance_id) + except Exception, ex: + euca.display_error_and_exit('%s' % ex) + + display_ajax_console_output(console_output) + else: + print 'instance_id must be specified' + usage() + +if __name__ == "__main__": + main() |
