Merge lp:~anso/nova/ajaxterm into lp:~hudson-openstack/nova/trunk

Proposed by Anthony Young
Status: Merged
Approved by: Soren Hansen
Approved revision: 283
Merged at revision: 549
Proposed branch: lp:~anso/nova/ajaxterm
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 4122 lines (+3857/-2)
31 files modified
bin/nova-ajax-console-proxy (+137/-0)
contrib/nova.sh (+2/-0)
nova/api/ec2/cloud.py (+5/-0)
nova/api/openstack/servers.py (+9/-0)
nova/compute/api.py (+20/-1)
nova/compute/manager.py (+8/-0)
nova/flags.py (+8/-1)
nova/tests/test_cloud.py (+13/-0)
nova/tests/test_compute.py (+10/-0)
nova/utils.py (+5/-0)
nova/virt/fake.py (+3/-0)
nova/virt/libvirt.xml.template (+13/-0)
nova/virt/libvirt_conn.py (+45/-0)
nova/virt/xenapi/vmops.py (+5/-0)
nova/virt/xenapi_conn.py (+4/-0)
tools/ajaxterm/README.txt (+120/-0)
tools/ajaxterm/ajaxterm.1 (+35/-0)
tools/ajaxterm/ajaxterm.css (+64/-0)
tools/ajaxterm/ajaxterm.html (+25/-0)
tools/ajaxterm/ajaxterm.js (+279/-0)
tools/ajaxterm/ajaxterm.py (+586/-0)
tools/ajaxterm/configure (+32/-0)
tools/ajaxterm/configure.ajaxterm.bin (+2/-0)
tools/ajaxterm/configure.initd.debian (+33/-0)
tools/ajaxterm/configure.initd.gentoo (+27/-0)
tools/ajaxterm/configure.initd.redhat (+75/-0)
tools/ajaxterm/configure.makefile (+20/-0)
tools/ajaxterm/qweb.py (+1356/-0)
tools/ajaxterm/sarissa.js (+647/-0)
tools/ajaxterm/sarissa_dhtml.js (+105/-0)
tools/euca-get-ajax-console (+164/-0)
To merge this branch: bzr merge lp:~anso/nova/ajaxterm
Reviewer Review Type Date Requested Status
Devin Carlen (community) Approve
Eric Day (community) Needs Information
Vish Ishaya (community) Approve
Review via email: mp+45068@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Vish Ishaya (vishvananda) wrote :

Couple small thing:

1. strings should be surrounded by _() for i18n

2. looks like you changed the memory on m1.tiny for testing, probably don't want to merge that in trunk.

review: Needs Fixing
Revision history for this message
Anthony Young (sleepsonthefloor) wrote :

Thanks vish - fixed the issues you mentioned, and did a bit more cleanup.

Revision history for this message
Vish Ishaya (vishvananda) wrote :

lgtm now.

review: Approve
Revision history for this message
Vish Ishaya (vishvananda) wrote :

although looks like it needs another trunk merge

Revision history for this message
Eric Day (eday) wrote :

nova/boto_extensions.py should go in contrib, no? Doesn't seem like nova/ is a good place for it.

review: Needs Information
Revision history for this message
Devin Carlen (devcamcar) wrote :

lgtm

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (4.0 KiB)

The attempt to merge lp:~anso/nova/ajaxterm into lp:nova failed. Below is the output from the failed tests.

nova/compute/api.py:429:77: W291 trailing whitespace
                  'args': {'token': output['token'], 'host': output['host'],
                                                                            ^
    JCR: Trailing whitespace is superfluous.
    FBM: Except when it occurs as part of a blank line (i.e. the line is
         nothing but whitespace). According to Python docs[1] a line with only
         whitespace is considered a blank line, and is to be ignored. However,
         matching a blank line to its indentation level avoids mistakenly
         terminating a multi-line statement (e.g. class declaration) when
         pasting code into the standard Python interpreter.

         [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines

    The warning returned varies on whether the line itself is blank, for easier
    filtering for those who want to indent their blank lines.

    Okay: spam(1)
    W291: spam(1)\s
    W293: class Foo(object):\n \n bang = 12
nova/compute/api.py:430:25: E231 missing whitespace after ':'
                  'port':output['port']}})
                        ^
    JCR: Each comma, semicolon or colon should be followed by whitespace.

    Okay: [a, b]
    Okay: (3,)
    Okay: a[1:4]
    Okay: a[:4]
    Okay: a[1:]
    Okay: a[1:4:2]
    E231: ['a','b']
    E231: foo(bar,baz)
nova/tests/test_cloud.py:171:39: E202 whitespace before '}'
        kwargs = {'image_id': image_id }
                                      ^
    Avoid extraneous whitespace in the following situations:

    - Immediately inside parentheses, brackets or braces.

    - Immediately before a comma, semicolon, or colon.

    Okay: spam(ham[1], {eggs: 2})
    E201: spam( ham[1], {eggs: 2})
    E201: spam(ham[ 1], {eggs: 2})
    E201: spam(ham[1], { eggs: 2})
    E202: spam(ham[1], {eggs: 2} )
    E202: spam(ham[1 ], {eggs: 2})
    E202: spam(ham[1], {eggs: 2 })

    E203: if x == 4: print x, y; x, y = y , x
    E203: if x == 4: print x, y ; x, y = y, x
    E203: if x == 4 : print x, y; x, y = y, x
nova/virt/fake.py:295:1: E302 expected 2 blank lines, found 1
class FakeInstance(object):
^
    Separate top-level function and class definitions with two blank lines.

    Method definitions inside a class are separated by a single blank line.

    Extra blank lines may be used (sparingly) to separate groups of related
    functions. Blank lines may be omitted between a bunch of related
    one-liners (e.g. a set of dummy implementations).

    Use blank lines in functions, sparingly, to indicate logical sections.

    Okay: def a():\n pass\n\n\ndef b():\n pass
    Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass

    E301: class Foo:\n b = 0\n def bar():\n pass
    E302: def a():\n pass\n\ndef b(n):\n pass
    E303: def a():\n pass\n\n\n\ndef b(n):\n pass
    E303: def a():\n\n\n\n pass
    E304: @decorator\n\ndef a():\n pass
nova/virt/libvirt_conn.py:448:30: E231 missing whitespace after ','
            for i in xrange(0,100): # don't loop forever
                ...

Read more...

Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

Attempt to merge into lp:nova failed due to conflicts:

text conflict in nova/virt/fake.py

lp:~anso/nova/ajaxterm updated
283. By Anthony Young

merge trunk, fix conflict

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'bin/nova-ajax-console-proxy'
--- bin/nova-ajax-console-proxy 1970-01-01 00:00:00 +0000
+++ bin/nova-ajax-console-proxy 2011-01-12 03:17:16 +0000
@@ -0,0 +1,137 @@
1#!/usr/bin/env python
2# pylint: disable-msg=C0103
3# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
5# Copyright 2010 United States Government as represented by the
6# Administrator of the National Aeronautics and Space Administration.
7# All Rights Reserved.
8#
9# Licensed under the Apache License, Version 2.0 (the "License");
10# you may not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20
21"""Ajax Console Proxy Server"""
22
23from eventlet import greenthread
24from eventlet.green import urllib2
25
26import exceptions
27import gettext
28import logging
29import os
30import sys
31import time
32import urlparse
33
34# If ../nova/__init__.py exists, add ../ to Python search path, so that
35# it will override what happens to be installed in /usr/(local/)lib/python...
36possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
37 os.pardir,
38 os.pardir))
39if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
40 sys.path.insert(0, possible_topdir)
41
42gettext.install('nova', unicode=1)
43
44from nova import flags
45from nova import log as logging
46from nova import rpc
47from nova import utils
48from nova import wsgi
49
50FLAGS = flags.FLAGS
51
52flags.DEFINE_integer('ajax_console_idle_timeout', 300,
53 'Seconds before idle connection destroyed')
54
55LOG = logging.getLogger('nova.ajax_console_proxy')
56LOG.setLevel(logging.DEBUG)
57LOG.addHandler(logging.StreamHandler())
58
59
60class AjaxConsoleProxy(object):
61 tokens = {}
62
63 def __call__(self, env, start_response):
64 try:
65 req_url = '%s://%s%s?%s' % (env['wsgi.url_scheme'],
66 env['HTTP_HOST'],
67 env['PATH_INFO'],
68 env['QUERY_STRING'])
69 if 'HTTP_REFERER' in env:
70 auth_url = env['HTTP_REFERER']
71 else:
72 auth_url = req_url
73
74 auth_params = urlparse.parse_qs(urlparse.urlparse(auth_url).query)
75 parsed_url = urlparse.urlparse(req_url)
76
77 auth_info = AjaxConsoleProxy.tokens[auth_params['token'][0]]
78 args = auth_info['args']
79 auth_info['last_activity'] = time.time()
80
81 remote_url = ("http://%s:%s%s?token=%s" % (
82 str(args['host']),
83 str(args['port']),
84 parsed_url.path,
85 str(args['token'])))
86
87 opener = urllib2.urlopen(remote_url, env['wsgi.input'].read())
88 body = opener.read()
89 info = opener.info()
90
91 start_response("200 OK", info.dict.items())
92 return body
93 except (exceptions.KeyError):
94 if env['PATH_INFO'] != '/favicon.ico':
95 LOG.audit("Unauthorized request %s, %s"
96 % (req_url, str(env)))
97 start_response("401 NOT AUTHORIZED", [])
98 return "Not Authorized"
99 except Exception:
100 start_response("500 ERROR", [])
101 return "Server Error"
102
103 def register_listeners(self):
104 class Callback:
105 def __call__(self, data, message):
106 if data['method'] == 'authorize_ajax_console':
107 AjaxConsoleProxy.tokens[data['args']['token']] = \
108 {'args': data['args'], 'last_activity': time.time()}
109
110 conn = rpc.Connection.instance(new=True)
111 consumer = rpc.TopicConsumer(
112 connection=conn,
113 topic=FLAGS.ajax_console_proxy_topic)
114 consumer.register_callback(Callback())
115
116 def delete_expired_tokens():
117 now = time.time()
118 to_delete = []
119 for k, v in AjaxConsoleProxy.tokens.items():
120 if now - v['last_activity'] > FLAGS.ajax_console_idle_timeout:
121 to_delete.append(k)
122
123 for k in to_delete:
124 del AjaxConsoleProxy.tokens[k]
125
126 utils.LoopingCall(consumer.fetch, auto_ack=True,
127 enable_callbacks=True).start(0.1)
128 utils.LoopingCall(delete_expired_tokens).start(1)
129
130if __name__ == '__main__':
131 utils.default_flagfile()
132 FLAGS(sys.argv)
133 server = wsgi.Server()
134 acp = AjaxConsoleProxy()
135 acp.register_listeners()
136 server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0')
137 server.wait()
0138
=== modified file 'contrib/nova.sh'
--- contrib/nova.sh 2010-12-16 11:35:46 +0000
+++ contrib/nova.sh 2011-01-12 03:17:16 +0000
@@ -78,6 +78,7 @@
78 sudo apt-get install -y user-mode-linux kvm libvirt-bin78 sudo apt-get install -y user-mode-linux kvm libvirt-bin
79 sudo apt-get install -y screen euca2ools vlan curl rabbitmq-server79 sudo apt-get install -y screen euca2ools vlan curl rabbitmq-server
80 sudo apt-get install -y lvm2 iscsitarget open-iscsi80 sudo apt-get install -y lvm2 iscsitarget open-iscsi
81 sudo apt-get install -y socat
81 echo "ISCSITARGET_ENABLE=true" | sudo tee /etc/default/iscsitarget82 echo "ISCSITARGET_ENABLE=true" | sudo tee /etc/default/iscsitarget
82 sudo /etc/init.d/iscsitarget restart83 sudo /etc/init.d/iscsitarget restart
83 sudo modprobe kvm84 sudo modprobe kvm
@@ -155,6 +156,7 @@
155 screen_it network "$NOVA_DIR/bin/nova-network"156 screen_it network "$NOVA_DIR/bin/nova-network"
156 screen_it scheduler "$NOVA_DIR/bin/nova-scheduler"157 screen_it scheduler "$NOVA_DIR/bin/nova-scheduler"
157 screen_it volume "$NOVA_DIR/bin/nova-volume"158 screen_it volume "$NOVA_DIR/bin/nova-volume"
159 screen_it ajax_console_proxy "$NOVA_DIR/bin/nova-ajax-console-proxy"
158 screen_it test ". $NOVA_DIR/novarc"160 screen_it test ". $NOVA_DIR/novarc"
159 screen -S nova -x161 screen -S nova -x
160fi162fi
161163
=== modified file 'nova/api/ec2/cloud.py'
--- nova/api/ec2/cloud.py 2011-01-11 23:03:15 +0000
+++ nova/api/ec2/cloud.py 2011-01-12 03:17:16 +0000
@@ -501,6 +501,11 @@
501 "Timestamp": now,501 "Timestamp": now,
502 "output": base64.b64encode(output)}502 "output": base64.b64encode(output)}
503503
504 def get_ajax_console(self, context, instance_id, **kwargs):
505 ec2_id = instance_id[0]
506 internal_id = ec2_id_to_id(ec2_id)
507 return self.compute_api.get_ajax_console(context, internal_id)
508
504 def describe_volumes(self, context, volume_id=None, **kwargs):509 def describe_volumes(self, context, volume_id=None, **kwargs):
505 volumes = self.volume_api.get_all(context)510 volumes = self.volume_api.get_all(context)
506 # NOTE(vish): volume_id is an optional list of volume ids to filter by.511 # NOTE(vish): volume_id is an optional list of volume ids to filter by.
507512
=== modified file 'nova/api/openstack/servers.py'
--- nova/api/openstack/servers.py 2011-01-11 06:47:35 +0000
+++ nova/api/openstack/servers.py 2011-01-12 03:17:16 +0000
@@ -283,6 +283,15 @@
283 return faults.Fault(exc.HTTPUnprocessableEntity())283 return faults.Fault(exc.HTTPUnprocessableEntity())
284 return exc.HTTPAccepted()284 return exc.HTTPAccepted()
285285
286 def get_ajax_console(self, req, id):
287 """ Returns a url to an instance's ajaxterm console. """
288 try:
289 self.compute_api.get_ajax_console(req.environ['nova.context'],
290 int(id))
291 except exception.NotFound:
292 return faults.Fault(exc.HTTPNotFound())
293 return exc.HTTPAccepted()
294
286 def diagnostics(self, req, id):295 def diagnostics(self, req, id):
287 """Permit Admins to retrieve server diagnostics."""296 """Permit Admins to retrieve server diagnostics."""
288 ctxt = req.environ["nova.context"]297 ctxt = req.environ["nova.context"]
289298
=== modified file 'nova/compute/api.py'
--- nova/compute/api.py 2011-01-11 06:47:35 +0000
+++ nova/compute/api.py 2011-01-12 03:17:16 +0000
@@ -414,7 +414,26 @@
414 rpc.cast(context,414 rpc.cast(context,
415 self.db.queue_get_for(context, FLAGS.compute_topic, host),415 self.db.queue_get_for(context, FLAGS.compute_topic, host),
416 {"method": "unrescue_instance",416 {"method": "unrescue_instance",
417 "args": {"instance_id": instance_id}})417 "args": {"instance_id": instance['id']}})
418
419 def get_ajax_console(self, context, instance_id):
420 """Get a url to an AJAX Console"""
421
422 instance = self.get(context, instance_id)
423
424 output = rpc.call(context,
425 '%s.%s' % (FLAGS.compute_topic,
426 instance['host']),
427 {'method': 'get_ajax_console',
428 'args': {'instance_id': instance['id']}})
429
430 rpc.cast(context, '%s' % FLAGS.ajax_console_proxy_topic,
431 {'method': 'authorize_ajax_console',
432 'args': {'token': output['token'], 'host': output['host'],
433 'port': output['port']}})
434
435 return {'url': '%s?token=%s' % (FLAGS.ajax_console_proxy_url,
436 output['token'])}
418437
419 def lock(self, context, instance_id):438 def lock(self, context, instance_id):
420 """439 """
421440
=== modified file 'nova/compute/manager.py'
--- nova/compute/manager.py 2011-01-10 17:37:06 +0000
+++ nova/compute/manager.py 2011-01-12 03:17:16 +0000
@@ -471,6 +471,14 @@
471 return self.driver.get_console_output(instance_ref)471 return self.driver.get_console_output(instance_ref)
472472
473 @exception.wrap_exception473 @exception.wrap_exception
474 def get_ajax_console(self, context, instance_id):
475 """Return connection information for an ajax console"""
476 context = context.elevated()
477 logging.debug(_("instance %s: getting ajax console"), instance_id)
478 instance_ref = self.db.instance_get(context, instance_id)
479
480 return self.driver.get_ajax_console(instance_ref)
481
474 @checks_instance_lock482 @checks_instance_lock
475 def attach_volume(self, context, instance_id, volume_id, mountpoint):483 def attach_volume(self, context, instance_id, volume_id, mountpoint):
476 """Attach a volume to an instance."""484 """Attach a volume to an instance."""
477485
=== modified file 'nova/flags.py'
--- nova/flags.py 2011-01-11 06:47:35 +0000
+++ nova/flags.py 2011-01-12 03:17:16 +0000
@@ -234,7 +234,14 @@
234 'the topic scheduler nodes listen on')234 'the topic scheduler nodes listen on')
235DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on')235DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on')
236DEFINE_string('network_topic', 'network', 'the topic network nodes listen on')236DEFINE_string('network_topic', 'network', 'the topic network nodes listen on')
237237DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy',
238 'the topic ajax proxy nodes listen on')
239DEFINE_string('ajax_console_proxy_url',
240 'http://127.0.0.1:8000',
241 'location of ajax console proxy, \
242 in the form "http://127.0.0.1:8000"')
243DEFINE_string('ajax_console_proxy_port',
244 8000, 'port that ajax_console_proxy binds')
238DEFINE_bool('verbose', False, 'show debug output')245DEFINE_bool('verbose', False, 'show debug output')
239DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit')246DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit')
240DEFINE_bool('fake_network', False,247DEFINE_bool('fake_network', False,
241248
=== modified file 'nova/tests/test_cloud.py'
--- nova/tests/test_cloud.py 2011-01-10 07:01:10 +0000
+++ nova/tests/test_cloud.py 2011-01-12 03:17:16 +0000
@@ -167,6 +167,19 @@
167 greenthread.sleep(0.3)167 greenthread.sleep(0.3)
168 rv = self.cloud.terminate_instances(self.context, [instance_id])168 rv = self.cloud.terminate_instances(self.context, [instance_id])
169169
170 def test_ajax_console(self):
171 kwargs = {'image_id': image_id}
172 rv = yield self.cloud.run_instances(self.context, **kwargs)
173 instance_id = rv['instancesSet'][0]['instanceId']
174 output = yield self.cloud.get_console_output(context=self.context,
175 instance_id=[instance_id])
176 self.assertEquals(b64decode(output['output']),
177 'http://fakeajaxconsole.com/?token=FAKETOKEN')
178 # TODO(soren): We need this until we can stop polling in the rpc code
179 # for unit tests.
180 greenthread.sleep(0.3)
181 rv = yield self.cloud.terminate_instances(self.context, [instance_id])
182
170 def test_key_generation(self):183 def test_key_generation(self):
171 result = self._create_key('test')184 result = self._create_key('test')
172 private_key = result['private_key']185 private_key = result['private_key']
173186
=== modified file 'nova/tests/test_compute.py'
--- nova/tests/test_compute.py 2011-01-07 14:46:17 +0000
+++ nova/tests/test_compute.py 2011-01-12 03:17:16 +0000
@@ -169,6 +169,16 @@
169 self.assert_(console)169 self.assert_(console)
170 self.compute.terminate_instance(self.context, instance_id)170 self.compute.terminate_instance(self.context, instance_id)
171171
172 def test_ajax_console(self):
173 """Make sure we can get console output from instance"""
174 instance_id = self._create_instance()
175 self.compute.run_instance(self.context, instance_id)
176
177 console = self.compute.get_ajax_console(self.context,
178 instance_id)
179 self.assert_(console)
180 self.compute.terminate_instance(self.context, instance_id)
181
172 def test_run_instance_existing(self):182 def test_run_instance_existing(self):
173 """Ensure failure when running an instance that already exists"""183 """Ensure failure when running an instance that already exists"""
174 instance_id = self._create_instance()184 instance_id = self._create_instance()
175185
=== modified file 'nova/utils.py'
--- nova/utils.py 2011-01-10 02:08:54 +0000
+++ nova/utils.py 2011-01-12 03:17:16 +0000
@@ -153,6 +153,11 @@
153 return os.path.join(os.path.dirname(__file__), s)153 return os.path.join(os.path.dirname(__file__), s)
154154
155155
156def novadir():
157 import nova
158 return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0]
159
160
156def default_flagfile(filename='nova.conf'):161def default_flagfile(filename='nova.conf'):
157 for arg in sys.argv:162 for arg in sys.argv:
158 if arg.find('flagfile') != -1:163 if arg.find('flagfile') != -1:
159164
=== modified file 'nova/virt/fake.py'
--- nova/virt/fake.py 2011-01-06 01:45:46 +0000
+++ nova/virt/fake.py 2011-01-12 03:17:16 +0000
@@ -289,6 +289,9 @@
289 def get_console_output(self, instance):289 def get_console_output(self, instance):
290 return 'FAKE CONSOLE OUTPUT'290 return 'FAKE CONSOLE OUTPUT'
291291
292 def get_ajax_console(self, instance):
293 return 'http://fakeajaxconsole.com/?token=FAKETOKEN'
294
292 def get_console_pool_info(self, console_type):295 def get_console_pool_info(self, console_type):
293 return {'address': '127.0.0.1',296 return {'address': '127.0.0.1',
294 'username': 'fakeuser',297 'username': 'fakeuser',
295298
=== modified file 'nova/virt/libvirt.xml.template'
--- nova/virt/libvirt.xml.template 2010-12-23 21:41:54 +0000
+++ nova/virt/libvirt.xml.template 2011-01-12 03:17:16 +0000
@@ -71,9 +71,22 @@
71#end if71#end if
72 </filterref>72 </filterref>
73 </interface>73 </interface>
74
75 <!-- The order is significant here. File must be defined first -->
74 <serial type="file">76 <serial type="file">
75 <source path='${basepath}/console.log'/>77 <source path='${basepath}/console.log'/>
76 <target port='1'/>78 <target port='1'/>
77 </serial>79 </serial>
80
81 <console type='pty' tty='/dev/pts/2'>
82 <source path='/dev/pts/2'/>
83 <target port='0'/>
84 </console>
85
86 <serial type='pty'>
87 <source path='/dev/pts/2'/>
88 <target port='0'/>
89 </serial>
90
78 </devices>91 </devices>
79</domain>92</domain>
8093
=== modified file 'nova/virt/libvirt_conn.py'
--- nova/virt/libvirt_conn.py 2011-01-11 19:49:18 +0000
+++ nova/virt/libvirt_conn.py 2011-01-12 03:17:16 +0000
@@ -38,6 +38,11 @@
3838
39import os39import os
40import shutil40import shutil
41import random
42import subprocess
43import uuid
44from xml.dom import minidom
45
4146
42from eventlet import greenthread47from eventlet import greenthread
43from eventlet import event48from eventlet import event
@@ -86,6 +91,9 @@
86flags.DEFINE_bool('allow_project_net_traffic',91flags.DEFINE_bool('allow_project_net_traffic',
87 True,92 True,
88 'Whether to allow in project network traffic')93 'Whether to allow in project network traffic')
94flags.DEFINE_string('ajaxterm_portrange',
95 '10000-12000',
96 'Range of ports that ajaxterm should randomly try to bind')
89flags.DEFINE_string('firewall_driver',97flags.DEFINE_string('firewall_driver',
90 'nova.virt.libvirt_conn.IptablesFirewallDriver',98 'nova.virt.libvirt_conn.IptablesFirewallDriver',
91 'Firewall driver (defaults to iptables)')99 'Firewall driver (defaults to iptables)')
@@ -433,6 +441,43 @@
433441
434 return self._dump_file(fpath)442 return self._dump_file(fpath)
435443
444 @exception.wrap_exception
445 def get_ajax_console(self, instance):
446 def get_open_port():
447 start_port, end_port = FLAGS.ajaxterm_portrange.split("-")
448 for i in xrange(0, 100): # don't loop forever
449 port = random.randint(int(start_port), int(end_port))
450 # netcat will exit with 0 only if the port is in use,
451 # so a nonzero return value implies it is unused
452 cmd = 'netcat 0.0.0.0 %s -w 1 </dev/null || echo free' % (port)
453 stdout, stderr = utils.execute(cmd)
454 if stdout.strip() == 'free':
455 return port
456 raise Exception(_('Unable to find an open port'))
457
458 def get_pty_for_instance(instance_name):
459 virt_dom = self._conn.lookupByName(instance_name)
460 xml = virt_dom.XMLDesc(0)
461 dom = minidom.parseString(xml)
462
463 for serial in dom.getElementsByTagName('serial'):
464 if serial.getAttribute('type') == 'pty':
465 source = serial.getElementsByTagName('source')[0]
466 return source.getAttribute('path')
467
468 port = get_open_port()
469 token = str(uuid.uuid4())
470 host = instance['host']
471
472 ajaxterm_cmd = 'sudo socat - %s' \
473 % get_pty_for_instance(instance['name'])
474
475 cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \
476 % (utils.novadir(), ajaxterm_cmd, token, port)
477
478 subprocess.Popen(cmd, shell=True)
479 return {'token': token, 'host': host, 'port': port}
480
436 def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None):481 def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None):
437 # syntactic nicety482 # syntactic nicety
438 basepath = lambda fname = '', prefix = prefix: os.path.join(483 basepath = lambda fname = '', prefix = prefix: os.path.join(
439484
=== modified file 'nova/virt/xenapi/vmops.py'
--- nova/virt/xenapi/vmops.py 2011-01-11 06:47:35 +0000
+++ nova/virt/xenapi/vmops.py 2011-01-12 03:17:16 +0000
@@ -284,6 +284,11 @@
284 # TODO: implement this to fix pylint!284 # TODO: implement this to fix pylint!
285 return 'FAKE CONSOLE OUTPUT of instance'285 return 'FAKE CONSOLE OUTPUT of instance'
286286
287 def get_ajax_console(self, instance):
288 """Return link to instance's ajax console"""
289 # TODO: implement this!
290 return 'http://fakeajaxconsole/fake_url'
291
287 def list_from_xenstore(self, vm, path):292 def list_from_xenstore(self, vm, path):
288 """Runs the xenstore-ls command to get a listing of all records293 """Runs the xenstore-ls command to get a listing of all records
289 from 'path' downward. Returns a dict with the sub-paths as keys,294 from 'path' downward. Returns a dict with the sub-paths as keys,
290295
=== modified file 'nova/virt/xenapi_conn.py'
--- nova/virt/xenapi_conn.py 2011-01-11 12:24:58 +0000
+++ nova/virt/xenapi_conn.py 2011-01-12 03:17:16 +0000
@@ -181,6 +181,10 @@
181 """Return snapshot of console"""181 """Return snapshot of console"""
182 return self._vmops.get_console_output(instance)182 return self._vmops.get_console_output(instance)
183183
184 def get_ajax_console(self, instance):
185 """Return link to instance's ajax console"""
186 return self._vmops.get_ajax_console(instance)
187
184 def attach_volume(self, instance_name, device_path, mountpoint):188 def attach_volume(self, instance_name, device_path, mountpoint):
185 """Attach volume storage to VM instance"""189 """Attach volume storage to VM instance"""
186 return self._volumeops.attach_volume(instance_name,190 return self._volumeops.attach_volume(instance_name,
187191
=== added directory 'tools/ajaxterm'
=== added file 'tools/ajaxterm/README.txt'
--- tools/ajaxterm/README.txt 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/README.txt 2011-01-12 03:17:16 +0000
@@ -0,0 +1,120 @@
1= [http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm Ajaxterm] =
2
3Ajaxterm is a web based terminal. It was totally inspired and works almost
4exactly like http://anyterm.org/ except it's much easier to install (see
5comparaison with anyterm below).
6
7Ajaxterm written in python (and some AJAX javascript for client side) and depends only on python2.3 or better.[[BR]]
8Ajaxterm is '''very simple to install''' on Linux, MacOS X, FreeBSD, Solaris, cygwin and any Unix that runs python2.3.[[BR]]
9Ajaxterm was written by Antony Lesuisse (email: al AT udev.org), License Public Domain.
10
11Use the [/qweb/forum/viewforum.php?id=2 Forum], if you have any question or remark.
12
13== News ==
14
15 * 2006-10-29: v0.10 allow space in login, cgi launch fix, redhat init
16 * 2006-07-12: v0.9 change uid, daemon fix (Daniel Fischer)
17 * 2006-07-04: v0.8 add login support to ssh (Sven Geggus), change max width to 256
18 * 2006-05-31: v0.7 minor fixes, daemon option
19 * 2006-05-23: v0.6 Applied debian and gentoo patches, renamed to Ajaxterm, default port 8022
20
21== Download and Install ==
22
23 * Release: [/qweb/files/Ajaxterm-0.10.tar.gz Ajaxterm-0.10.tar.gz]
24 * Browse src: [/qweb/trac/browser/trunk/ajaxterm/ ajaxterm/]
25
26To install Ajaxterm issue the following commands:
27{{{
28wget http://antony.lesuisse.org/qweb/files/Ajaxterm-0.10.tar.gz
29tar zxvf Ajaxterm-0.10.tar.gz
30cd Ajaxterm-0.10
31./ajaxterm.py
32}}}
33Then point your browser to this URL : http://localhost:8022/
34
35== Screenshot ==
36
37{{{
38#!html
39<center><img src="/qweb/trac/attachment/wiki/AjaxTerm/scr.png?format=raw" alt="ajaxterm screenshot" style=""/></center>
40}}}
41
42== Documentation and Caveats ==
43
44 * Ajaxterm only support latin1, if you use Ubuntu or any LANG==en_US.UTF-8 distribution don't forget to "unset LANG".
45
46 * If run as root ajaxterm will run /bin/login, otherwise it will run ssh
47 localhost. To use an other command use the -c option.
48
49 * By default Ajaxterm only listen at 127.0.0.1:8022. For remote access, it is
50 strongly recommended to use '''https SSL/TLS''', and that is simple to
51 configure if you use the apache web server using mod_proxy.[[BR]][[BR]]
52 Using ssl will also speed up ajaxterm (probably because of keepalive).[[BR]][[BR]]
53 Here is an configuration example:
54
55{{{
56 Listen 443
57 NameVirtualHost *:443
58
59 <VirtualHost *:443>
60 ServerName localhost
61 SSLEngine On
62 SSLCertificateKeyFile ssl/apache.pem
63 SSLCertificateFile ssl/apache.pem
64
65 ProxyRequests Off
66 <Proxy *>
67 Order deny,allow
68 Allow from all
69 </Proxy>
70 ProxyPass /ajaxterm/ http://localhost:8022/
71 ProxyPassReverse /ajaxterm/ http://localhost:8022/
72 </VirtualHost>
73}}}
74
75 * Using GET HTTP request seems to speed up ajaxterm, just click on GET in the
76 interface, but be warned that your keystrokes might be loggued (by apache or
77 any proxy). I usually enable it after the login.
78
79 * Ajaxterm commandline usage:
80
81{{{
82usage: ajaxterm.py [options]
83
84options:
85 -h, --help show this help message and exit
86 -pPORT, --port=PORT Set the TCP port (default: 8022)
87 -cCMD, --command=CMD set the command (default: /bin/login or ssh localhost)
88 -l, --log log requests to stderr (default: quiet mode)
89 -d, --daemon run as daemon in the background
90 -PPIDFILE, --pidfile=PIDFILE
91 set the pidfile (default: /var/run/ajaxterm.pid)
92 -iINDEX_FILE, --index=INDEX_FILE
93 default index file (default: ajaxterm.html)
94 -uUID, --uid=UID Set the daemon's user id
95}}}
96
97 * Ajaxterm was first written as a demo for qweb (my web framework), but
98 actually doesn't use many features of qweb.
99
100 * Compared to anyterm:
101 * There are no partial updates, ajaxterm updates either all the screen or
102 nothing. That make the code simpler and I also think it's faster. HTTP
103 replies are always gzencoded. When used in 80x25 mode, almost all of
104 them are below the 1500 bytes (size of an ethernet frame) and we just
105 replace the screen with the reply (no javascript string handling).
106 * Ajaxterm polls the server for updates with an exponentially growing
107 timeout when the screen hasn't changed. The timeout is also resetted as
108 soon as a key is pressed. Anyterm blocks on a pending request and use a
109 parallel connection for keypresses. The anyterm approch is better
110 when there aren't any keypress.
111
112 * Ajaxterm files are released in the Public Domain, (except [http://sarissa.sourceforge.net/doc/ sarissa*] which are LGPL).
113
114== TODO ==
115
116 * insert mode ESC [ 4 h
117 * change size x,y from gui (sending signal)
118 * vt102 graphic codepage
119 * use innerHTML or prototype instead of sarissa
120
0121
=== added file 'tools/ajaxterm/ajaxterm.1'
--- tools/ajaxterm/ajaxterm.1 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/ajaxterm.1 2011-01-12 03:17:16 +0000
@@ -0,0 +1,35 @@
1.TH ajaxterm "1" "May 2006" "ajaxterm 0.5" "User commands"
2.SH NAME
3ajaxterm \- Web based terminal written in python
4
5.SH DESCRITPION
6\fBajaxterm\fR is a web based terminal written in python and some AJAX
7javascript for client side.
8It can use almost any web browser and even works through firewalls.
9
10.SH USAGE
11\fBajaxterm.py\fR [options]
12
13.SH OPTIONS
14A summary of the options supported by \fBajaxterm\fR is included below.
15 \fB-h, --help\fR show this help message and exit
16 \fB-pPORT, --port=PORT\fR Set the TCP port (default: 8022)
17 \fB-cCMD, --command=CMD\fR set the command (default: /bin/login or ssh localhost)
18 \fB-l, --log\fR log requests to stderr (default: quiet mode)
19
20.SH AUTHOR
21Antony Lesuisse <al@udev.org>
22
23This manual page was written for the Debian system by
24Julien Valroff <julien@kirya.net> (but may be used by others).
25
26.SH "REPORTING BUGS"
27Report any bugs to the author: Antony Lesuisse <al@udev.org>
28
29.SH COPYRIGHT
30Copyright Antony Lesuisse <al@udev.org>
31
32.SH SEE ALSO
33- \fBajaxterm\fR wiki page: http://antony.lesuisse.org/qweb/trac/wiki/AjaxTerm
34.br
35- \fBajaxterm\fR forum: http://antony.lesuisse.org/qweb/forum/viewforum.php?id=2
036
=== added file 'tools/ajaxterm/ajaxterm.css'
--- tools/ajaxterm/ajaxterm.css 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/ajaxterm.css 2011-01-12 03:17:16 +0000
@@ -0,0 +1,64 @@
1pre.stat {
2 margin: 0px;
3 padding: 4px;
4 display: block;
5 font-family: monospace;
6 white-space: pre;
7 background-color: black;
8 border-top: 1px solid black;
9 color: white;
10}
11pre.stat span {
12 padding: 0px;
13}
14pre.stat .on {
15 background-color: #080;
16 font-weight: bold;
17 color: white;
18 cursor: pointer;
19}
20pre.stat .off {
21 background-color: #888;
22 font-weight: bold;
23 color: white;
24 cursor: pointer;
25}
26pre.term {
27 margin: 0px;
28 padding: 4px;
29 display: block;
30 font-family: monospace;
31 white-space: pre;
32 background-color: black;
33 border-top: 1px solid white;
34 color: #eee;
35}
36pre.term span.f0 { color: #000; }
37pre.term span.f1 { color: #b00; }
38pre.term span.f2 { color: #0b0; }
39pre.term span.f3 { color: #bb0; }
40pre.term span.f4 { color: #00b; }
41pre.term span.f5 { color: #b0b; }
42pre.term span.f6 { color: #0bb; }
43pre.term span.f7 { color: #bbb; }
44pre.term span.f8 { color: #666; }
45pre.term span.f9 { color: #f00; }
46pre.term span.f10 { color: #0f0; }
47pre.term span.f11 { color: #ff0; }
48pre.term span.f12 { color: #00f; }
49pre.term span.f13 { color: #f0f; }
50pre.term span.f14 { color: #0ff; }
51pre.term span.f15 { color: #fff; }
52pre.term span.b0 { background-color: #000; }
53pre.term span.b1 { background-color: #b00; }
54pre.term span.b2 { background-color: #0b0; }
55pre.term span.b3 { background-color: #bb0; }
56pre.term span.b4 { background-color: #00b; }
57pre.term span.b5 { background-color: #b0b; }
58pre.term span.b6 { background-color: #0bb; }
59pre.term span.b7 { background-color: #bbb; }
60
61body { background-color: #888; }
62#term {
63 float: left;
64}
065
=== added file 'tools/ajaxterm/ajaxterm.html'
--- tools/ajaxterm/ajaxterm.html 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/ajaxterm.html 2011-01-12 03:17:16 +0000
@@ -0,0 +1,25 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2<html>
3<head>
4 <title>Ajaxterm</title>
5 <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
6 <link rel="stylesheet" type="text/css" href="ajaxterm.css"/>
7 <script type="text/javascript" src="sarissa.js"></script>
8 <script type="text/javascript" src="sarissa_dhtml.js"></script>
9 <script type="text/javascript" src="ajaxterm.js"></script>
10 <script type="text/javascript">
11 /*
12 ajaxterm.py creates a random session_id to demultiplex multiple connections,
13 and to add a layer of security - in its shipping form, ajaxterm accepted any session_id
14 and was susceptible to an easy exploit
15 */
16 SESSION_ID = '$session_id';
17 window.onload=function() {
18 t=ajaxterm.Terminal("term",80,25);
19 };
20 </script>
21</head>
22<body>
23<div id="term"></div>
24</body>
25</html>
026
=== added file 'tools/ajaxterm/ajaxterm.js'
--- tools/ajaxterm/ajaxterm.js 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/ajaxterm.js 2011-01-12 03:17:16 +0000
@@ -0,0 +1,279 @@
1ajaxterm={};
2ajaxterm.Terminal_ctor=function(id,width,height) {
3 var ie=0;
4 if(window.ActiveXObject)
5 ie=1;
6 var sid=""+SESSION_ID;
7 var query0="s="+sid+"&w="+width+"&h="+height;
8 var query1=query0+"&c=1&k=";
9 var buf="";
10 var timeout;
11 var error_timeout;
12 var keybuf=[];
13 var sending=0;
14 var rmax=1;
15
16 var div=document.getElementById(id);
17 var dstat=document.createElement('pre');
18 var sled=document.createElement('span');
19 var opt_get=document.createElement('a');
20 var opt_color=document.createElement('a');
21 var opt_paste=document.createElement('a');
22 var sdebug=document.createElement('span');
23 var dterm=document.createElement('div');
24
25 function debug(s) {
26 sdebug.innerHTML=s;
27 }
28 function error() {
29 sled.className='off';
30 debug("Connection lost timeout ts:"+((new Date).getTime()));
31 }
32 function opt_add(opt,name) {
33 opt.className='off';
34 opt.innerHTML=' '+name+' ';
35 dstat.appendChild(opt);
36 dstat.appendChild(document.createTextNode(' '));
37 }
38 function do_get(event) {
39 opt_get.className=(opt_get.className=='off')?'on':'off';
40 debug('GET '+opt_get.className);
41 }
42 function do_color(event) {
43 var o=opt_color.className=(opt_color.className=='off')?'on':'off';
44 if(o=='on')
45 query1=query0+"&c=1&k=";
46 else
47 query1=query0+"&k=";
48 debug('Color '+opt_color.className);
49 }
50 function mozilla_clipboard() {
51 // mozilla sucks
52 try {
53 netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
54 } catch (err) {
55 debug('Access denied, <a href="http://kb.mozillazine.org/Granting_JavaScript_access_to_the_clipboard" target="_blank">more info</a>');
56 return undefined;
57 }
58 var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard);
59 var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
60 if (!clip || !trans) {
61 return undefined;
62 }
63 trans.addDataFlavor("text/unicode");
64 clip.getData(trans,clip.kGlobalClipboard);
65 var str=new Object();
66 var strLength=new Object();
67 try {
68 trans.getTransferData("text/unicode",str,strLength);
69 } catch(err) {
70 return "";
71 }
72 if (str) {
73 str=str.value.QueryInterface(Components.interfaces.nsISupportsString);
74 }
75 if (str) {
76 return str.data.substring(0,strLength.value / 2);
77 } else {
78 return "";
79 }
80 }
81 function do_paste(event) {
82 var p=undefined;
83 if (window.clipboardData) {
84 p=window.clipboardData.getData("Text");
85 } else if(window.netscape) {
86 p=mozilla_clipboard();
87 }
88 if (p) {
89 debug('Pasted');
90 queue(encodeURIComponent(p));
91 } else {
92 }
93 }
94 function update() {
95// debug("ts: "+((new Date).getTime())+" rmax:"+rmax);
96 if(sending==0) {
97 sending=1;
98 sled.className='on';
99 var r=new XMLHttpRequest();
100 var send="";
101 while(keybuf.length>0) {
102 send+=keybuf.pop();
103 }
104 var query=query1+send;
105 if(opt_get.className=='on') {
106 r.open("GET","u?"+query,true);
107 if(ie) {
108 r.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
109 }
110 } else {
111 r.open("POST","u",true);
112 }
113 r.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
114 r.onreadystatechange = function () {
115// debug("xhr:"+((new Date).getTime())+" state:"+r.readyState+" status:"+r.status+" statusText:"+r.statusText);
116 if (r.readyState==4) {
117 if(r.status==200) {
118 window.clearTimeout(error_timeout);
119 de=r.responseXML.documentElement;
120 if(de.tagName=="pre") {
121 if(ie) {
122 Sarissa.updateContentFromNode(de, dterm);
123 } else {
124 Sarissa.updateContentFromNode(de, dterm);
125// old=div.firstChild;
126// div.replaceChild(de,old);
127 }
128 rmax=100;
129 } else {
130 rmax*=2;
131 if(rmax>2000)
132 rmax=2000;
133 }
134 sending=0;
135 sled.className='off';
136 timeout=window.setTimeout(update,rmax);
137 } else {
138 debug("Connection error status:"+r.status);
139 }
140 }
141 }
142 error_timeout=window.setTimeout(error,5000);
143 if(opt_get.className=='on') {
144 r.send(null);
145 } else {
146 r.send(query);
147 }
148 }
149 }
150 function queue(s) {
151 keybuf.unshift(s);
152 if(sending==0) {
153 window.clearTimeout(timeout);
154 timeout=window.setTimeout(update,1);
155 }
156 }
157 function keypress(ev) {
158 if (!ev) var ev=window.event;
159// s="kp keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey;
160// debug(s);
161// return false;
162// else { if (!ev.ctrlKey || ev.keyCode==17) { return; }
163 var kc;
164 var k="";
165 if (ev.keyCode)
166 kc=ev.keyCode;
167 if (ev.which)
168 kc=ev.which;
169 if (ev.altKey) {
170 if (kc>=65 && kc<=90)
171 kc+=32;
172 if (kc>=97 && kc<=122) {
173 k=String.fromCharCode(27)+String.fromCharCode(kc);
174 }
175 } else if (ev.ctrlKey) {
176 if (kc>=65 && kc<=90) k=String.fromCharCode(kc-64); // Ctrl-A..Z
177 else if (kc>=97 && kc<=122) k=String.fromCharCode(kc-96); // Ctrl-A..Z
178 else if (kc==54) k=String.fromCharCode(30); // Ctrl-^
179 else if (kc==109) k=String.fromCharCode(31); // Ctrl-_
180 else if (kc==219) k=String.fromCharCode(27); // Ctrl-[
181 else if (kc==220) k=String.fromCharCode(28); // Ctrl-\
182 else if (kc==221) k=String.fromCharCode(29); // Ctrl-]
183 else if (kc==219) k=String.fromCharCode(29); // Ctrl-]
184 else if (kc==219) k=String.fromCharCode(0); // Ctrl-@
185 } else if (ev.which==0) {
186 if (kc==9) k=String.fromCharCode(9); // Tab
187 else if (kc==8) k=String.fromCharCode(127); // Backspace
188 else if (kc==27) k=String.fromCharCode(27); // Escape
189 else {
190 if (kc==33) k="[5~"; // PgUp
191 else if (kc==34) k="[6~"; // PgDn
192 else if (kc==35) k="[4~"; // End
193 else if (kc==36) k="[1~"; // Home
194 else if (kc==37) k="[D"; // Left
195 else if (kc==38) k="[A"; // Up
196 else if (kc==39) k="[C"; // Right
197 else if (kc==40) k="[B"; // Down
198 else if (kc==45) k="[2~"; // Ins
199 else if (kc==46) k="[3~"; // Del
200 else if (kc==112) k="[[A"; // F1
201 else if (kc==113) k="[[B"; // F2
202 else if (kc==114) k="[[C"; // F3
203 else if (kc==115) k="[[D"; // F4
204 else if (kc==116) k="[[E"; // F5
205 else if (kc==117) k="[17~"; // F6
206 else if (kc==118) k="[18~"; // F7
207 else if (kc==119) k="[19~"; // F8
208 else if (kc==120) k="[20~"; // F9
209 else if (kc==121) k="[21~"; // F10
210 else if (kc==122) k="[23~"; // F11
211 else if (kc==123) k="[24~"; // F12
212 if (k.length) {
213 k=String.fromCharCode(27)+k;
214 }
215 }
216 } else {
217 if (kc==8)
218 k=String.fromCharCode(127); // Backspace
219 else
220 k=String.fromCharCode(kc);
221 }
222 if(k.length) {
223// queue(encodeURIComponent(k));
224 if(k=="+") {
225 queue("%2B");
226 } else {
227 queue(escape(k));
228 }
229 }
230 ev.cancelBubble=true;
231 if (ev.stopPropagation) ev.stopPropagation();
232 if (ev.preventDefault) ev.preventDefault();
233 return false;
234 }
235 function keydown(ev) {
236 if (!ev) var ev=window.event;
237 if (ie) {
238// s="kd keyCode="+ev.keyCode+" which="+ev.which+" shiftKey="+ev.shiftKey+" ctrlKey="+ev.ctrlKey+" altKey="+ev.altKey;
239// debug(s);
240 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,
241 113:1,114:1,115:1,116:1,117:1,118:1,119:1,120:1,121:1,122:1,123:1};
242 if (o[ev.keyCode] || ev.ctrlKey || ev.altKey) {
243 ev.which=0;
244 return keypress(ev);
245 }
246 }
247 }
248 function init() {
249 sled.appendChild(document.createTextNode('\xb7'));
250 sled.className='off';
251 dstat.appendChild(sled);
252 dstat.appendChild(document.createTextNode(' '));
253 opt_add(opt_color,'Colors');
254 opt_color.className='on';
255 opt_add(opt_get,'GET');
256 opt_add(opt_paste,'Paste');
257 dstat.appendChild(sdebug);
258 dstat.className='stat';
259 div.appendChild(dstat);
260 div.appendChild(dterm);
261 if(opt_color.addEventListener) {
262 opt_get.addEventListener('click',do_get,true);
263 opt_color.addEventListener('click',do_color,true);
264 opt_paste.addEventListener('click',do_paste,true);
265 } else {
266 opt_get.attachEvent("onclick", do_get);
267 opt_color.attachEvent("onclick", do_color);
268 opt_paste.attachEvent("onclick", do_paste);
269 }
270 document.onkeypress=keypress;
271 document.onkeydown=keydown;
272 timeout=window.setTimeout(update,100);
273 }
274 init();
275}
276ajaxterm.Terminal=function(id,width,height) {
277 return new this.Terminal_ctor(id,width,height);
278}
279
0280
=== added file 'tools/ajaxterm/ajaxterm.py'
--- tools/ajaxterm/ajaxterm.py 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/ajaxterm.py 2011-01-12 03:17:16 +0000
@@ -0,0 +1,586 @@
1#!/usr/bin/env python
2
3""" Ajaxterm """
4
5import array,cgi,fcntl,glob,mimetypes,optparse,os,pty,random,re,signal,select,sys,threading,time,termios,struct,pwd
6
7os.chdir(os.path.normpath(os.path.dirname(__file__)))
8# Optional: Add QWeb in sys path
9sys.path[0:0]=glob.glob('../../python')
10
11import qweb
12import string, subprocess, uuid
13
14global g_server
15TIMEOUT=300
16
17class Terminal:
18 def __init__(self,width=80,height=24):
19 self.width=width
20 self.height=height
21 self.init()
22 self.reset()
23 def init(self):
24 self.esc_seq={
25 "\x00": None,
26 "\x05": self.esc_da,
27 "\x07": None,
28 "\x08": self.esc_0x08,
29 "\x09": self.esc_0x09,
30 "\x0a": self.esc_0x0a,
31 "\x0b": self.esc_0x0a,
32 "\x0c": self.esc_0x0a,
33 "\x0d": self.esc_0x0d,
34 "\x0e": None,
35 "\x0f": None,
36 "\x1b#8": None,
37 "\x1b=": None,
38 "\x1b>": None,
39 "\x1b(0": None,
40 "\x1b(A": None,
41 "\x1b(B": None,
42 "\x1b[c": self.esc_da,
43 "\x1b[0c": self.esc_da,
44 "\x1b]R": None,
45 "\x1b7": self.esc_save,
46 "\x1b8": self.esc_restore,
47 "\x1bD": None,
48 "\x1bE": None,
49 "\x1bH": None,
50 "\x1bM": self.esc_ri,
51 "\x1bN": None,
52 "\x1bO": None,
53 "\x1bZ": self.esc_da,
54 "\x1ba": None,
55 "\x1bc": self.reset,
56 "\x1bn": None,
57 "\x1bo": None,
58 }
59 for k,v in self.esc_seq.items():
60 if v==None:
61 self.esc_seq[k]=self.esc_ignore
62 # regex
63 d={
64 r'\[\??([0-9;]*)([@ABCDEFGHJKLMPXacdefghlmnqrstu`])' : self.csi_dispatch,
65 r'\]([^\x07]+)\x07' : self.esc_ignore,
66 }
67 self.esc_re=[]
68 for k,v in d.items():
69 self.esc_re.append((re.compile('\x1b'+k),v))
70 # define csi sequences
71 self.csi_seq={
72 '@': (self.csi_at,[1]),
73 '`': (self.csi_G,[1]),
74 'J': (self.csi_J,[0]),
75 'K': (self.csi_K,[0]),
76 }
77 for i in [i[4] for i in dir(self) if i.startswith('csi_') and len(i)==5]:
78 if not self.csi_seq.has_key(i):
79 self.csi_seq[i]=(getattr(self,'csi_'+i),[1])
80 # Init 0-256 to latin1 and html translation table
81 self.trl1=""
82 for i in range(256):
83 if i<32:
84 self.trl1+=" "
85 elif i<127 or i>160:
86 self.trl1+=chr(i)
87 else:
88 self.trl1+="?"
89 self.trhtml=""
90 for i in range(256):
91 if i==0x0a or (i>32 and i<127) or i>160:
92 self.trhtml+=chr(i)
93 elif i<=32:
94 self.trhtml+="\xa0"
95 else:
96 self.trhtml+="?"
97 def reset(self,s=""):
98 self.scr=array.array('i',[0x000700]*(self.width*self.height))
99 self.st=0
100 self.sb=self.height-1
101 self.cx_bak=self.cx=0
102 self.cy_bak=self.cy=0
103 self.cl=0
104 self.sgr=0x000700
105 self.buf=""
106 self.outbuf=""
107 self.last_html=""
108 def peek(self,y1,x1,y2,x2):
109 return self.scr[self.width*y1+x1:self.width*y2+x2]
110 def poke(self,y,x,s):
111 pos=self.width*y+x
112 self.scr[pos:pos+len(s)]=s
113 def zero(self,y1,x1,y2,x2):
114 w=self.width*(y2-y1)+x2-x1+1
115 z=array.array('i',[0x000700]*w)
116 self.scr[self.width*y1+x1:self.width*y2+x2+1]=z
117 def scroll_up(self,y1,y2):
118 self.poke(y1,0,self.peek(y1+1,0,y2,self.width))
119 self.zero(y2,0,y2,self.width-1)
120 def scroll_down(self,y1,y2):
121 self.poke(y1+1,0,self.peek(y1,0,y2-1,self.width))
122 self.zero(y1,0,y1,self.width-1)
123 def scroll_right(self,y,x):
124 self.poke(y,x+1,self.peek(y,x,y,self.width))
125 self.zero(y,x,y,x)
126 def cursor_down(self):
127 if self.cy>=self.st and self.cy<=self.sb:
128 self.cl=0
129 q,r=divmod(self.cy+1,self.sb+1)
130 if q:
131 self.scroll_up(self.st,self.sb)
132 self.cy=self.sb
133 else:
134 self.cy=r
135 def cursor_right(self):
136 q,r=divmod(self.cx+1,self.width)
137 if q:
138 self.cl=1
139 else:
140 self.cx=r
141 def echo(self,c):
142 if self.cl:
143 self.cursor_down()
144 self.cx=0
145 self.scr[(self.cy*self.width)+self.cx]=self.sgr|ord(c)
146 self.cursor_right()
147 def esc_0x08(self,s):
148 self.cx=max(0,self.cx-1)
149 def esc_0x09(self,s):
150 x=self.cx+8
151 q,r=divmod(x,8)
152 self.cx=(q*8)%self.width
153 def esc_0x0a(self,s):
154 self.cursor_down()
155 def esc_0x0d(self,s):
156 self.cl=0
157 self.cx=0
158 def esc_save(self,s):
159 self.cx_bak=self.cx
160 self.cy_bak=self.cy
161 def esc_restore(self,s):
162 self.cx=self.cx_bak
163 self.cy=self.cy_bak
164 self.cl=0
165 def esc_da(self,s):
166 self.outbuf="\x1b[?6c"
167 def esc_ri(self,s):
168 self.cy=max(self.st,self.cy-1)
169 if self.cy==self.st:
170 self.scroll_down(self.st,self.sb)
171 def esc_ignore(self,*s):
172 pass
173# print "term:ignore: %s"%repr(s)
174 def csi_dispatch(self,seq,mo):
175 # CSI sequences
176 s=mo.group(1)
177 c=mo.group(2)
178 f=self.csi_seq.get(c,None)
179 if f:
180 try:
181 l=[min(int(i),1024) for i in s.split(';') if len(i)<4]
182 except ValueError:
183 l=[]
184 if len(l)==0:
185 l=f[1]
186 f[0](l)
187# else:
188# print 'csi ignore',c,l
189 def csi_at(self,l):
190 for i in range(l[0]):
191 self.scroll_right(self.cy,self.cx)
192 def csi_A(self,l):
193 self.cy=max(self.st,self.cy-l[0])
194 def csi_B(self,l):
195 self.cy=min(self.sb,self.cy+l[0])
196 def csi_C(self,l):
197 self.cx=min(self.width-1,self.cx+l[0])
198 self.cl=0
199 def csi_D(self,l):
200 self.cx=max(0,self.cx-l[0])
201 self.cl=0
202 def csi_E(self,l):
203 self.csi_B(l)
204 self.cx=0
205 self.cl=0
206 def csi_F(self,l):
207 self.csi_A(l)
208 self.cx=0
209 self.cl=0
210 def csi_G(self,l):
211 self.cx=min(self.width,l[0])-1
212 def csi_H(self,l):
213 if len(l)<2: l=[1,1]
214 self.cx=min(self.width,l[1])-1
215 self.cy=min(self.height,l[0])-1
216 self.cl=0
217 def csi_J(self,l):
218 if l[0]==0:
219 self.zero(self.cy,self.cx,self.height-1,self.width-1)
220 elif l[0]==1:
221 self.zero(0,0,self.cy,self.cx)
222 elif l[0]==2:
223 self.zero(0,0,self.height-1,self.width-1)
224 def csi_K(self,l):
225 if l[0]==0:
226 self.zero(self.cy,self.cx,self.cy,self.width-1)
227 elif l[0]==1:
228 self.zero(self.cy,0,self.cy,self.cx)
229 elif l[0]==2:
230 self.zero(self.cy,0,self.cy,self.width-1)
231 def csi_L(self,l):
232 for i in range(l[0]):
233 if self.cy<self.sb:
234 self.scroll_down(self.cy,self.sb)
235 def csi_M(self,l):
236 if self.cy>=self.st and self.cy<=self.sb:
237 for i in range(l[0]):
238 self.scroll_up(self.cy,self.sb)
239 def csi_P(self,l):
240 w,cx,cy=self.width,self.cx,self.cy
241 end=self.peek(cy,cx,cy,w)
242 self.csi_K([0])
243 self.poke(cy,cx,end[l[0]:])
244 def csi_X(self,l):
245 self.zero(self.cy,self.cx,self.cy,self.cx+l[0])
246 def csi_a(self,l):
247 self.csi_C(l)
248 def csi_c(self,l):
249 #'\x1b[?0c' 0-8 cursor size
250 pass
251 def csi_d(self,l):
252 self.cy=min(self.height,l[0])-1
253 def csi_e(self,l):
254 self.csi_B(l)
255 def csi_f(self,l):
256 self.csi_H(l)
257 def csi_h(self,l):
258 if l[0]==4:
259 pass
260# print "insert on"
261 def csi_l(self,l):
262 if l[0]==4:
263 pass
264# print "insert off"
265 def csi_m(self,l):
266 for i in l:
267 if i==0 or i==39 or i==49 or i==27:
268 self.sgr=0x000700
269 elif i==1:
270 self.sgr=(self.sgr|0x000800)
271 elif i==7:
272 self.sgr=0x070000
273 elif i>=30 and i<=37:
274 c=i-30
275 self.sgr=(self.sgr&0xff08ff)|(c<<8)
276 elif i>=40 and i<=47:
277 c=i-40
278 self.sgr=(self.sgr&0x00ffff)|(c<<16)
279# else:
280# print "CSI sgr ignore",l,i
281# print 'sgr: %r %x'%(l,self.sgr)
282 def csi_r(self,l):
283 if len(l)<2: l=[0,self.height]
284 self.st=min(self.height-1,l[0]-1)
285 self.sb=min(self.height-1,l[1]-1)
286 self.sb=max(self.st,self.sb)
287 def csi_s(self,l):
288 self.esc_save(0)
289 def csi_u(self,l):
290 self.esc_restore(0)
291 def escape(self):
292 e=self.buf
293 if len(e)>32:
294# print "error %r"%e
295 self.buf=""
296 elif e in self.esc_seq:
297 self.esc_seq[e](e)
298 self.buf=""
299 else:
300 for r,f in self.esc_re:
301 mo=r.match(e)
302 if mo:
303 f(e,mo)
304 self.buf=""
305 break
306# if self.buf=='': print "ESC %r\n"%e
307 def write(self,s):
308 for i in s:
309 if len(self.buf) or (i in self.esc_seq):
310 self.buf+=i
311 self.escape()
312 elif i == '\x1b':
313 self.buf+=i
314 else:
315 self.echo(i)
316 def read(self):
317 b=self.outbuf
318 self.outbuf=""
319 return b
320 def dump(self):
321 r=''
322 for i in self.scr:
323 r+=chr(i&255)
324 return r
325 def dumplatin1(self):
326 return self.dump().translate(self.trl1)
327 def dumphtml(self,color=1):
328 h=self.height
329 w=self.width
330 r=""
331 span=""
332 span_bg,span_fg=-1,-1
333 for i in range(h*w):
334 q,c=divmod(self.scr[i],256)
335 if color:
336 bg,fg=divmod(q,256)
337 else:
338 bg,fg=0,7
339 if i==self.cy*w+self.cx:
340 bg,fg=1,7
341 if (bg!=span_bg or fg!=span_fg or i==h*w-1):
342 if len(span):
343 r+='<span class="f%d b%d">%s</span>'%(span_fg,span_bg,cgi.escape(span.translate(self.trhtml)))
344 span=""
345 span_bg,span_fg=bg,fg
346 span+=chr(c)
347 if i%w==w-1:
348 span+='\n'
349 r='<?xml version="1.0" encoding="ISO-8859-1"?><pre class="term">%s</pre>'%r
350 if self.last_html==r:
351 return '<?xml version="1.0"?><idem></idem>'
352 else:
353 self.last_html=r
354# print self
355 return r
356 def __repr__(self):
357 d=self.dumplatin1()
358 r=""
359 for i in range(self.height):
360 r+="|%s|\n"%d[self.width*i:self.width*(i+1)]
361 return r
362
363class SynchronizedMethod:
364 def __init__(self,lock,orig):
365 self.lock=lock
366 self.orig=orig
367 def __call__(self,*l):
368 self.lock.acquire()
369 r=self.orig(*l)
370 self.lock.release()
371 return r
372
373class Multiplex:
374 def __init__(self,cmd=None):
375 signal.signal(signal.SIGCHLD, signal.SIG_IGN)
376 self.cmd=cmd
377 self.proc={}
378 self.lock=threading.RLock()
379 self.thread=threading.Thread(target=self.loop)
380 self.alive=1
381 self.lastActivity=time.time()
382 # synchronize methods
383 for name in ['create','fds','proc_read','proc_write','dump','die','run']:
384 orig=getattr(self,name)
385 setattr(self,name,SynchronizedMethod(self.lock,orig))
386 self.thread.start()
387 def create(self,w=80,h=25):
388 pid,fd=pty.fork()
389 if pid==0:
390 try:
391 fdl=[int(i) for i in os.listdir('/proc/self/fd')]
392 except OSError:
393 fdl=range(256)
394 for i in [i for i in fdl if i>2]:
395 try:
396 os.close(i)
397 except OSError:
398 pass
399 if self.cmd:
400 cmd=['/bin/sh','-c',self.cmd]
401 elif os.getuid()==0:
402 cmd=['/bin/login']
403 else:
404 sys.stdout.write("Login: ")
405 login=sys.stdin.readline().strip()
406 if re.match('^[0-9A-Za-z-_. ]+$',login):
407 cmd=['ssh']
408 cmd+=['-oPreferredAuthentications=keyboard-interactive,password']
409 cmd+=['-oNoHostAuthenticationForLocalhost=yes']
410 cmd+=['-oLogLevel=FATAL']
411 cmd+=['-F/dev/null','-l',login,'localhost']
412 else:
413 os._exit(0)
414 env={}
415 env["COLUMNS"]=str(w)
416 env["LINES"]=str(h)
417 env["TERM"]="linux"
418 env["PATH"]=os.environ['PATH']
419 os.execvpe(cmd[0],cmd,env)
420 else:
421 fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
422 # python bug http://python.org/sf/1112949 on amd64
423 fcntl.ioctl(fd, struct.unpack('i',struct.pack('I',termios.TIOCSWINSZ))[0], struct.pack("HHHH",h,w,0,0))
424 self.proc[fd]={'pid':pid,'term':Terminal(w,h),'buf':'','time':time.time()}
425 return fd
426 def die(self):
427 self.alive=0
428 def run(self):
429 return self.alive
430 def fds(self):
431 return self.proc.keys()
432 def proc_kill(self,fd):
433 if fd in self.proc:
434 self.proc[fd]['time']=0
435 t=time.time()
436 for i in self.proc.keys():
437 t0=self.proc[i]['time']
438 if (t-t0)>TIMEOUT:
439 try:
440 os.close(i)
441 os.kill(self.proc[i]['pid'],signal.SIGTERM)
442 except (IOError,OSError):
443 pass
444 del self.proc[i]
445 def proc_read(self,fd):
446 try:
447 t=self.proc[fd]['term']
448 t.write(os.read(fd,65536))
449 reply=t.read()
450 if reply:
451 os.write(fd,reply)
452 self.proc[fd]['time']=time.time()
453 except (KeyError,IOError,OSError):
454 self.proc_kill(fd)
455 def proc_write(self,fd,s):
456 try:
457 os.write(fd,s)
458 except (IOError,OSError):
459 self.proc_kill(fd)
460 def dump(self,fd,color=1):
461 try:
462 return self.proc[fd]['term'].dumphtml(color)
463 except KeyError:
464 return False
465 def loop(self):
466 while self.run():
467 fds=self.fds()
468 i,o,e=select.select(fds, [], [], 1.0)
469 if time.time() - self.lastActivity > TIMEOUT:
470 global g_server
471 g_server.shutdown()
472 for fd in i:
473 self.proc_read(fd)
474 if len(i):
475 time.sleep(0.002)
476 for i in self.proc.keys():
477 try:
478 os.close(i)
479 os.kill(self.proc[i]['pid'],signal.SIGTERM)
480 except (IOError,OSError):
481 pass
482
483class AjaxTerm:
484 def __init__(self,cmd=None,index_file='ajaxterm.html',token=None):
485 self.files={}
486 self.token=token
487 for i in ['css','html','js']:
488 for j in glob.glob('*.%s'%i):
489 self.files[j]=file(j).read()
490 self.files['index']=file(index_file).read()
491 self.mime = mimetypes.types_map.copy()
492 self.mime['.html']= 'text/html; charset=UTF-8'
493 self.multi = Multiplex(cmd)
494 self.session = {}
495 def __call__(self, environ, start_response):
496 req = qweb.QWebRequest(environ, start_response,session=None)
497 if req.PATH_INFO.endswith('/u'):
498 s=req.REQUEST["s"]
499 k=req.REQUEST["k"]
500 c=req.REQUEST["c"]
501 w=req.REQUEST.int("w")
502 h=req.REQUEST.int("h")
503 if s in self.session:
504 term=self.session[s]
505 else:
506 raise Exception('Not Authorized')
507 # The original code below was insecure, because it allowed unauthorized sessions to be created
508 # if not (w>2 and w<256 and h>2 and h<100):
509 # w,h=80,25
510 # term=self.session[s]=self.multi.create(w,h)
511 if k:
512 self.multi.proc_write(term,k)
513 time.sleep(0.002)
514 self.multi.lastActivity = time.time();
515 dump=self.multi.dump(term,c)
516 req.response_headers['Content-Type']='text/xml'
517 if isinstance(dump,str):
518 req.write(dump)
519 req.response_gzencode=1
520 else:
521 del self.session[s]
522 req.write('<?xml version="1.0"?><idem></idem>')
523# print "sessions %r"%self.session
524 else:
525 n=os.path.basename(req.PATH_INFO)
526 if n in self.files:
527 req.response_headers['Content-Type'] = self.mime.get(os.path.splitext(n)[1].lower(), 'application/octet-stream')
528 req.write(self.files[n])
529 elif req.REQUEST['token'] == self.token:
530 req.response_headers['Content-Type'] = 'text/html; charset=UTF-8'
531 session_id = str(uuid.uuid4())
532 req.write(string.Template(self.files['index']).substitute(session_id=session_id))
533 term=self.session[session_id]=self.multi.create(80,25)
534 else:
535 raise Exception("Not Authorized")
536 return req
537
538def main():
539 parser = optparse.OptionParser()
540 parser.add_option("-p", "--port", dest="port", default="8022", help="Set the TCP port (default: 8022)")
541 parser.add_option("-c", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh 0.0.0.0)")
542 parser.add_option("-l", "--log", action="store_true", dest="log",default=0,help="log requests to stderr (default: quiet mode)")
543 parser.add_option("-d", "--daemon", action="store_true", dest="daemon", default=0, help="run as daemon in the background")
544 parser.add_option("-P", "--pidfile",dest="pidfile",default="/var/run/ajaxterm.pid",help="set the pidfile (default: /var/run/ajaxterm.pid)")
545 parser.add_option("-i", "--index", dest="index_file", default="ajaxterm.html",help="default index file (default: ajaxterm.html)")
546 parser.add_option("-u", "--uid", dest="uid", help="Set the daemon's user id")
547 parser.add_option("-t", "--token", dest="token", help="Set authorization token")
548 (o, a) = parser.parse_args()
549 if o.daemon:
550 pid=os.fork()
551 if pid == 0:
552 #os.setsid() ?
553 os.setpgrp()
554 nullin = file('/dev/null', 'r')
555 nullout = file('/dev/null', 'w')
556 os.dup2(nullin.fileno(), sys.stdin.fileno())
557 os.dup2(nullout.fileno(), sys.stdout.fileno())
558 os.dup2(nullout.fileno(), sys.stderr.fileno())
559 if os.getuid()==0 and o.uid:
560 try:
561 os.setuid(int(o.uid))
562 except:
563 os.setuid(pwd.getpwnam(o.uid).pw_uid)
564 else:
565 try:
566 file(o.pidfile,'w+').write(str(pid)+'\n')
567 except:
568 pass
569 print 'AjaxTerm at http://0.0.0.0:%s/ pid: %d' % (o.port,pid)
570 sys.exit(0)
571 else:
572 print 'AjaxTerm at http://0.0.0.0:%s/' % o.port
573 at=AjaxTerm(o.cmd,o.index_file,o.token)
574# f=lambda:os.system('firefox http://localhost:%s/&'%o.port)
575# qweb.qweb_wsgi_autorun(at,ip='localhost',port=int(o.port),threaded=0,log=o.log,callback_ready=None)
576 try:
577 global g_server
578 g_server = qweb.QWebWSGIServer(at,ip='0.0.0.0',port=int(o.port),threaded=0,log=o.log)
579 g_server.serve_forever()
580 except KeyboardInterrupt,e:
581 sys.excepthook(*sys.exc_info())
582 at.multi.die()
583
584if __name__ == '__main__':
585 main()
586
0587
=== added file 'tools/ajaxterm/configure'
--- tools/ajaxterm/configure 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/configure 2011-01-12 03:17:16 +0000
@@ -0,0 +1,32 @@
1#!/usr/bin/env python
2
3import optparse,os
4
5parser = optparse.OptionParser()
6parser.add_option("", "--prefix", dest="prefix",default="/usr/local",help="installation prefix (default: /usr/local)")
7parser.add_option("", "--confdir", dest="confdir", default="/etc",help="configuration files directory prefix (default: /etc)")
8parser.add_option("", "--port", dest="port", default="8022", help="set the listening TCP port (default: 8022)")
9parser.add_option("", "--command", dest="cmd", default=None,help="set the command (default: /bin/login or ssh localhost)")
10(o, a) = parser.parse_args()
11
12print "Configuring prefix=",o.prefix," port=",o.port
13
14etc=o.confdir
15port=o.port
16cmd=o.cmd
17bin=os.path.join(o.prefix,"bin")
18lib=os.path.join(o.prefix,"share/ajaxterm")
19man=os.path.join(o.prefix,"share/man/man1")
20
21file("ajaxterm.bin","w").write(file("configure.ajaxterm.bin").read()%locals())
22file("Makefile","w").write(file("configure.makefile").read()%locals())
23
24if os.path.isfile("/etc/gentoo-release"):
25 file("ajaxterm.initd","w").write(file("configure.initd.gentoo").read()%locals())
26elif os.path.isfile("/etc/fedora-release") or os.path.isfile("/etc/redhat-release"):
27 file("ajaxterm.initd","w").write(file("configure.initd.redhat").read()%locals())
28else:
29 file("ajaxterm.initd","w").write(file("configure.initd.debian").read()%locals())
30
31os.system("chmod a+x ajaxterm.bin")
32os.system("chmod a+x ajaxterm.initd")
033
=== added file 'tools/ajaxterm/configure.ajaxterm.bin'
--- tools/ajaxterm/configure.ajaxterm.bin 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/configure.ajaxterm.bin 2011-01-12 03:17:16 +0000
@@ -0,0 +1,2 @@
1#!/bin/sh
2PYTHONPATH=%(lib)s exec %(lib)s/ajaxterm.py $@
03
=== added file 'tools/ajaxterm/configure.initd.debian'
--- tools/ajaxterm/configure.initd.debian 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/configure.initd.debian 2011-01-12 03:17:16 +0000
@@ -0,0 +1,33 @@
1#!/bin/sh
2
3PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
4DAEMON=%(bin)s/ajaxterm
5PORT=%(port)s
6PIDFILE=/var/run/ajaxterm.pid
7
8[ -x "$DAEMON" ] || exit 0
9
10#. /lib/lsb/init-functions
11
12case "$1" in
13 start)
14 echo "Starting ajaxterm on port $PORT"
15 start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody || return 2
16 ;;
17 stop)
18 echo "Stopping ajaxterm"
19 start-stop-daemon --stop --pidfile $PIDFILE
20 rm -f $PIDFILE
21 ;;
22 restart|force-reload)
23 $0 stop
24 sleep 1
25 $0 start
26 ;;
27 *)
28 echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
29 exit 3
30 ;;
31esac
32
33:
034
=== added file 'tools/ajaxterm/configure.initd.gentoo'
--- tools/ajaxterm/configure.initd.gentoo 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/configure.initd.gentoo 2011-01-12 03:17:16 +0000
@@ -0,0 +1,27 @@
1#!/sbin/runscript
2
3# AjaxTerm Gentoo script, 08 May 2006 Mark Gillespie
4
5DAEMON=%(bin)s/ajaxterm
6PORT=%(port)s
7PIDFILE=/var/run/ajaxterm.pid
8
9depend()
10{
11 need net
12}
13
14start()
15{
16 ebegin "Starting AjaxTerm on port $PORT"
17 start-stop-daemon --start --pidfile $PIDFILE --exec $DAEMON -- --daemon --port=$PORT --uid=nobody
18 eend $?
19}
20
21stop()
22{
23 ebegin "Stopping AjaxTerm"
24 start-stop-daemon --stop --pidfile $PIDFILE
25 rm -f $PIDFILE
26 eend $?
27}
028
=== added file 'tools/ajaxterm/configure.initd.redhat'
--- tools/ajaxterm/configure.initd.redhat 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/configure.initd.redhat 2011-01-12 03:17:16 +0000
@@ -0,0 +1,75 @@
1#
2# ajaxterm Startup script for ajaxterm
3#
4# chkconfig: - 99 99
5# description: Ajaxterm is a yadda yadda yadda
6# processname: ajaxterm
7# pidfile: /var/run/ajaxterm.pid
8# version: 1.0 Kevin Reichhart - ajaxterminit at lastname dot org
9
10# Source function library.
11. /etc/rc.d/init.d/functions
12
13if [ -f /etc/sysconfig/ajaxterm ]; then
14 . /etc/sysconfig/ajaxterm
15fi
16
17ajaxterm=/usr/local/bin/ajaxterm
18prog=ajaxterm
19pidfile=${PIDFILE-/var/run/ajaxterm.pid}
20lockfile=${LOCKFILE-/var/lock/subsys/ajaxterm}
21port=${PORT-8022}
22user=${xUSER-nobody}
23RETVAL=0
24
25
26start() {
27 echo -n $"Starting $prog: "
28 daemon $ajaxterm --daemon --port=$port --uid=$user $OPTIONS
29 RETVAL=$?
30 echo
31 [ $RETVAL = 0 ] && touch ${lockfile}
32 return $RETVAL
33}
34stop() {
35 echo -n $"Stopping $prog: "
36 killproc $ajaxterm
37 RETVAL=$?
38 echo
39 [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile}
40}
41reload() {
42 echo -n $"Reloading $prog: "
43 killproc $ajaxterm -HUP
44 RETVAL=$?
45 echo
46}
47
48# See how we were called.
49case "$1" in
50 start)
51 start
52 ;;
53 stop)
54 stop
55 ;;
56 status)
57 status python ajaxterm
58 RETVAL=$?
59 ;;
60 restart)
61 stop
62 start
63 ;;
64 condrestart)
65 if [ -f ${pidfile} ] ; then
66 stop
67 start
68 fi
69 ;;
70 *)
71 echo $"Usage: $prog {start|stop|restart|condrestart}"
72 exit 1
73esac
74
75exit $RETVAL
076
=== added file 'tools/ajaxterm/configure.makefile'
--- tools/ajaxterm/configure.makefile 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/configure.makefile 2011-01-12 03:17:16 +0000
@@ -0,0 +1,20 @@
1build:
2 true
3
4install:
5 install -d "%(bin)s"
6 install -d "%(lib)s"
7 install ajaxterm.bin "%(bin)s/ajaxterm"
8 install ajaxterm.initd "%(etc)s/init.d/ajaxterm"
9 install -m 644 ajaxterm.css ajaxterm.html ajaxterm.js qweb.py sarissa.js sarissa_dhtml.js "%(lib)s"
10 install -m 755 ajaxterm.py "%(lib)s"
11 gzip --best -c ajaxterm.1 > ajaxterm.1.gz
12 install -d "%(man)s"
13 install ajaxterm.1.gz "%(man)s"
14
15clean:
16 rm ajaxterm.bin
17 rm ajaxterm.initd
18 rm ajaxterm.1.gz
19 rm Makefile
20
021
=== added file 'tools/ajaxterm/qweb.py'
--- tools/ajaxterm/qweb.py 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/qweb.py 2011-01-12 03:17:16 +0000
@@ -0,0 +1,1356 @@
1#!/usr/bin/python2.3
2#
3# vim:set et ts=4 fdc=0 fdn=2 fdl=0:
4#
5# There are no blank lines between blocks beacause i use folding from:
6# http://www.vim.org/scripts/script.php?script_id=515
7#
8
9"""= QWeb Framework =
10
11== What is QWeb ? ==
12
13QWeb is a python based [http://www.python.org/doc/peps/pep-0333/ WSGI]
14compatible web framework, it provides an infratructure to quickly build web
15applications consisting of:
16
17 * A lightweight request handler (QWebRequest)
18 * An xml templating engine (QWebXml and QWebHtml)
19 * A simple name based controler (qweb_control)
20 * A standalone WSGI Server (QWebWSGIServer)
21 * A cgi and fastcgi WSGI wrapper (taken from flup)
22 * A startup function that starts cgi, factgi or standalone according to the
23 evironement (qweb_autorun).
24
25QWeb applications are runnable in standalone mode (from commandline), via
26FastCGI, Regular CGI or by any python WSGI compliant server.
27
28QWeb doesn't provide any database access but it integrates nicely with ORMs
29such as SQLObject, SQLAlchemy or plain DB-API.
30
31Written by Antony Lesuisse (email al AT udev.org)
32
33Homepage: http://antony.lesuisse.org/qweb/trac/
34
35Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
36
37== Quick Start (for Linux, MacOS X and cygwin) ==
38
39Make sure you have at least python 2.3 installed and run the following commands:
40
41{{{
42$ wget http://antony.lesuisse.org/qweb/files/QWeb-0.7.tar.gz
43$ tar zxvf QWeb-0.7.tar.gz
44$ cd QWeb-0.7/examples/blog
45$ ./blog.py
46}}}
47
48And point your browser to http://localhost:8080/
49
50You may also try AjaxTerm which uses qweb request handler.
51
52== Download ==
53
54 * Version 0.7:
55 * Source [/qweb/files/QWeb-0.7.tar.gz QWeb-0.7.tar.gz]
56 * Python 2.3 Egg [/qweb/files/QWeb-0.7-py2.3.egg QWeb-0.7-py2.3.egg]
57 * Python 2.4 Egg [/qweb/files/QWeb-0.7-py2.4.egg QWeb-0.7-py2.4.egg]
58
59 * [/qweb/trac/browser Browse the source repository]
60
61== Documentation ==
62
63 * [/qweb/trac/browser/trunk/README.txt?format=raw Read the included documentation]
64 * QwebTemplating
65
66== Mailin-list ==
67
68 * Forum: [http://antony.lesuisse.org/qweb/forum/viewforum.php?id=1 Forum]
69 * 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]
70
71QWeb Components:
72----------------
73
74QWeb also feature a simple components api, that enables developers to easily
75produces reusable components.
76
77Default qweb components:
78
79 - qweb_static:
80 A qweb component to serve static content from the filesystem or from
81 zipfiles.
82
83 - qweb_dbadmin:
84 scaffolding for sqlobject
85
86License
87-------
88qweb/fcgi.py wich is BSD-like from saddi.com.
89Everything else is put in the public domain.
90
91
92TODO
93----
94 Announce QWeb to python-announce-list@python.org web-sig@python.org
95 qweb_core
96 rename request methods into
97 request_save_files
98 response_404
99 response_redirect
100 response_download
101 request callback_generator, callback_function ?
102 wsgi callback_server_local
103 xml tags explicitly call render_attributes(t_att)?
104 priority form-checkbox over t-value (for t-option)
105
106"""
107
108import BaseHTTPServer,SocketServer,Cookie
109import cgi,datetime,email,email.Message,errno,gzip,os,random,re,socket,sys,tempfile,time,types,urllib,urlparse,xml.dom
110try:
111 import cPickle as pickle
112except ImportError:
113 import pickle
114try:
115 import cStringIO as StringIO
116except ImportError:
117 import StringIO
118
119#----------------------------------------------------------
120# Qweb Xml t-raw t-esc t-if t-foreach t-set t-call t-trim
121#----------------------------------------------------------
122class QWebEval:
123 def __init__(self,data):
124 self.data=data
125 def __getitem__(self,expr):
126 if self.data.has_key(expr):
127 return self.data[expr]
128 r=None
129 try:
130 r=eval(expr,self.data)
131 except NameError,e:
132 pass
133 except AttributeError,e:
134 pass
135 except Exception,e:
136 print "qweb: expression error '%s' "%expr,e
137 if self.data.has_key("__builtins__"):
138 del self.data["__builtins__"]
139 return r
140 def eval_object(self,expr):
141 return self[expr]
142 def eval_str(self,expr):
143 if expr=="0":
144 return self.data[0]
145 if isinstance(self[expr],unicode):
146 return self[expr].encode("utf8")
147 return str(self[expr])
148 def eval_format(self,expr):
149 try:
150 return str(expr%self)
151 except:
152 return "qweb: format error '%s' "%expr
153# if isinstance(r,unicode):
154# return r.encode("utf8")
155 def eval_bool(self,expr):
156 if self.eval_object(expr):
157 return 1
158 else:
159 return 0
160class QWebXml:
161 """QWeb Xml templating engine
162
163 The templating engine use a very simple syntax, "magic" xml attributes, to
164 produce any kind of texutal output (even non-xml).
165
166 QWebXml:
167 the template engine core implements the basic magic attributes:
168
169 t-att t-raw t-esc t-if t-foreach t-set t-call t-trim
170
171 """
172 def __init__(self,x=None,zipname=None):
173 self.node=xml.dom.Node
174 self._t={}
175 self._render_tag={}
176 prefix='render_tag_'
177 for i in [j for j in dir(self) if j.startswith(prefix)]:
178 name=i[len(prefix):].replace('_','-')
179 self._render_tag[name]=getattr(self.__class__,i)
180
181 self._render_att={}
182 prefix='render_att_'
183 for i in [j for j in dir(self) if j.startswith(prefix)]:
184 name=i[len(prefix):].replace('_','-')
185 self._render_att[name]=getattr(self.__class__,i)
186
187 if x!=None:
188 if zipname!=None:
189 import zipfile
190 zf=zipfile.ZipFile(zipname, 'r')
191 self.add_template(zf.read(x))
192 else:
193 self.add_template(x)
194 def register_tag(self,tag,func):
195 self._render_tag[tag]=func
196 def add_template(self,x):
197 if hasattr(x,'documentElement'):
198 dom=x
199 elif x.startswith("<?xml"):
200 import xml.dom.minidom
201 dom=xml.dom.minidom.parseString(x)
202 else:
203 import xml.dom.minidom
204 dom=xml.dom.minidom.parse(x)
205 for n in dom.documentElement.childNodes:
206 if n.nodeName=="t":
207 self._t[str(n.getAttribute("t-name"))]=n
208 def get_template(self,name):
209 return self._t[name]
210
211 def eval_object(self,expr,v):
212 return QWebEval(v).eval_object(expr)
213 def eval_str(self,expr,v):
214 return QWebEval(v).eval_str(expr)
215 def eval_format(self,expr,v):
216 return QWebEval(v).eval_format(expr)
217 def eval_bool(self,expr,v):
218 return QWebEval(v).eval_bool(expr)
219
220 def render(self,tname,v={},out=None):
221 if self._t.has_key(tname):
222 return self.render_node(self._t[tname],v)
223 else:
224 return 'qweb: template "%s" not found'%tname
225 def render_node(self,e,v):
226 r=""
227 if e.nodeType==self.node.TEXT_NODE or e.nodeType==self.node.CDATA_SECTION_NODE:
228 r=e.data.encode("utf8")
229 elif e.nodeType==self.node.ELEMENT_NODE:
230 pre=""
231 g_att=""
232 t_render=None
233 t_att={}
234 for (an,av) in e.attributes.items():
235 an=str(an)
236 if isinstance(av,types.UnicodeType):
237 av=av.encode("utf8")
238 else:
239 av=av.nodeValue.encode("utf8")
240 if an.startswith("t-"):
241 for i in self._render_att:
242 if an[2:].startswith(i):
243 g_att+=self._render_att[i](self,e,an,av,v)
244 break
245 else:
246 if self._render_tag.has_key(an[2:]):
247 t_render=an[2:]
248 t_att[an[2:]]=av
249 else:
250 g_att+=' %s="%s"'%(an,cgi.escape(av,1));
251 if t_render:
252 if self._render_tag.has_key(t_render):
253 r=self._render_tag[t_render](self,e,t_att,g_att,v)
254 else:
255 r=self.render_element(e,g_att,v,pre,t_att.get("trim",0))
256 return r
257 def render_element(self,e,g_att,v,pre="",trim=0):
258 g_inner=[]
259 for n in e.childNodes:
260 g_inner.append(self.render_node(n,v))
261 name=str(e.nodeName)
262 inner="".join(g_inner)
263 if trim==0:
264 pass
265 elif trim=='left':
266 inner=inner.lstrip()
267 elif trim=='right':
268 inner=inner.rstrip()
269 elif trim=='both':
270 inner=inner.strip()
271 if name=="t":
272 return inner
273 elif len(inner):
274 return "<%s%s>%s%s</%s>"%(name,g_att,pre,inner,name)
275 else:
276 return "<%s%s/>"%(name,g_att)
277
278 # Attributes
279 def render_att_att(self,e,an,av,v):
280 if an.startswith("t-attf-"):
281 att,val=an[7:],self.eval_format(av,v)
282 elif an.startswith("t-att-"):
283 att,val=(an[6:],self.eval_str(av,v))
284 else:
285 att,val=self.eval_object(av,v)
286 return ' %s="%s"'%(att,cgi.escape(val,1))
287
288 # Tags
289 def render_tag_raw(self,e,t_att,g_att,v):
290 return self.eval_str(t_att["raw"],v)
291 def render_tag_rawf(self,e,t_att,g_att,v):
292 return self.eval_format(t_att["rawf"],v)
293 def render_tag_esc(self,e,t_att,g_att,v):
294 return cgi.escape(self.eval_str(t_att["esc"],v))
295 def render_tag_escf(self,e,t_att,g_att,v):
296 return cgi.escape(self.eval_format(t_att["escf"],v))
297 def render_tag_foreach(self,e,t_att,g_att,v):
298 expr=t_att["foreach"]
299 enum=self.eval_object(expr,v)
300 if enum!=None:
301 var=t_att.get('as',expr).replace('.','_')
302 d=v.copy()
303 size=-1
304 if isinstance(enum,types.ListType):
305 size=len(enum)
306 elif isinstance(enum,types.TupleType):
307 size=len(enum)
308 elif hasattr(enum,'count'):
309 size=enum.count()
310 d["%s_size"%var]=size
311 d["%s_all"%var]=enum
312 index=0
313 ru=[]
314 for i in enum:
315 d["%s_value"%var]=i
316 d["%s_index"%var]=index
317 d["%s_first"%var]=index==0
318 d["%s_even"%var]=index%2
319 d["%s_odd"%var]=(index+1)%2
320 d["%s_last"%var]=index+1==size
321 if index%2:
322 d["%s_parity"%var]='odd'
323 else:
324 d["%s_parity"%var]='even'
325 if isinstance(i,types.DictType):
326 d.update(i)
327 else:
328 d[var]=i
329 ru.append(self.render_element(e,g_att,d))
330 index+=1
331 return "".join(ru)
332 else:
333 return "qweb: t-foreach %s not found."%expr
334 def render_tag_if(self,e,t_att,g_att,v):
335 if self.eval_bool(t_att["if"],v):
336 return self.render_element(e,g_att,v)
337 else:
338 return ""
339 def render_tag_call(self,e,t_att,g_att,v):
340 # TODO t-prefix
341 if t_att.has_key("import"):
342 d=v
343 else:
344 d=v.copy()
345 d[0]=self.render_element(e,g_att,d)
346 return self.render(t_att["call"],d)
347 def render_tag_set(self,e,t_att,g_att,v):
348 if t_att.has_key("eval"):
349 v[t_att["set"]]=self.eval_object(t_att["eval"],v)
350 else:
351 v[t_att["set"]]=self.render_element(e,g_att,v)
352 return ""
353
354#----------------------------------------------------------
355# QWeb HTML (+deprecated QWebFORM and QWebOLD)
356#----------------------------------------------------------
357class QWebURL:
358 """ URL helper
359 assert req.PATH_INFO== "/site/admin/page_edit"
360 u = QWebURL(root_path="/site/",req_path=req.PATH_INFO)
361 s=u.url2_href("user/login",{'a':'1'})
362 assert s=="../user/login?a=1"
363
364 """
365 def __init__(self, root_path="/", req_path="/",defpath="",defparam={}):
366 self.defpath=defpath
367 self.defparam=defparam
368 self.root_path=root_path
369 self.req_path=req_path
370 self.req_list=req_path.split("/")[:-1]
371 self.req_len=len(self.req_list)
372 def decode(self,s):
373 h={}
374 for k,v in cgi.parse_qsl(s,1):
375 h[k]=v
376 return h
377 def encode(self,h):
378 return urllib.urlencode(h.items())
379 def request(self,req):
380 return req.REQUEST
381 def copy(self,path=None,param=None):
382 npath=self.defpath
383 if path:
384 npath=path
385 nparam=self.defparam.copy()
386 if param:
387 nparam.update(param)
388 return QWebURL(self.root_path,self.req_path,npath,nparam)
389 def path(self,path=''):
390 if not path:
391 path=self.defpath
392 pl=(self.root_path+path).split('/')
393 i=0
394 for i in range(min(len(pl), self.req_len)):
395 if pl[i]!=self.req_list[i]:
396 break
397 else:
398 i+=1
399 dd=self.req_len-i
400 if dd<0:
401 dd=0
402 return '/'.join(['..']*dd+pl[i:])
403 def href(self,path='',arg={}):
404 p=self.path(path)
405 tmp=self.defparam.copy()
406 tmp.update(arg)
407 s=self.encode(tmp)
408 if len(s):
409 return p+"?"+s
410 else:
411 return p
412 def form(self,path='',arg={}):
413 p=self.path(path)
414 tmp=self.defparam.copy()
415 tmp.update(arg)
416 r=''.join(['<input type="hidden" name="%s" value="%s"/>'%(k,cgi.escape(str(v),1)) for k,v in tmp.items()])
417 return (p,r)
418class QWebField:
419 def __init__(self,name=None,default="",check=None):
420 self.name=name
421 self.default=default
422 self.check=check
423 # optional attributes
424 self.type=None
425 self.trim=1
426 self.required=1
427 self.cssvalid="form_valid"
428 self.cssinvalid="form_invalid"
429 # set by addfield
430 self.form=None
431 # set by processing
432 self.input=None
433 self.css=None
434 self.value=None
435 self.valid=None
436 self.invalid=None
437 self.validate(1)
438 def validate(self,val=1,update=1):
439 if val:
440 self.valid=1
441 self.invalid=0
442 self.css=self.cssvalid
443 else:
444 self.valid=0
445 self.invalid=1
446 self.css=self.cssinvalid
447 if update and self.form:
448 self.form.update()
449 def invalidate(self,update=1):
450 self.validate(0,update)
451class QWebForm:
452 class QWebFormF:
453 pass
454 def __init__(self,e=None,arg=None,default=None):
455 self.fields={}
456 # all fields have been submitted
457 self.submitted=False
458 self.missing=[]
459 # at least one field is invalid or missing
460 self.invalid=False
461 self.error=[]
462 # all fields have been submitted and are valid
463 self.valid=False
464 # fields under self.f for convenience
465 self.f=self.QWebFormF()
466 if e:
467 self.add_template(e)
468 # assume that the fields are done with the template
469 if default:
470 self.set_default(default,e==None)
471 if arg!=None:
472 self.process_input(arg)
473 def __getitem__(self,k):
474 return self.fields[k]
475 def set_default(self,default,add_missing=1):
476 for k,v in default.items():
477 if self.fields.has_key(k):
478 self.fields[k].default=str(v)
479 elif add_missing:
480 self.add_field(QWebField(k,v))
481 def add_field(self,f):
482 self.fields[f.name]=f
483 f.form=self
484 setattr(self.f,f.name,f)
485 def add_template(self,e):
486 att={}
487 for (an,av) in e.attributes.items():
488 an=str(an)
489 if an.startswith("t-"):
490 att[an[2:]]=av.encode("utf8")
491 for i in ["form-text", "form-password", "form-radio", "form-checkbox", "form-select","form-textarea"]:
492 if att.has_key(i):
493 name=att[i].split(".")[-1]
494 default=att.get("default","")
495 check=att.get("check",None)
496 f=QWebField(name,default,check)
497 if i=="form-textarea":
498 f.type="textarea"
499 f.trim=0
500 if i=="form-checkbox":
501 f.type="checkbox"
502 f.required=0
503 self.add_field(f)
504 for n in e.childNodes:
505 if n.nodeType==n.ELEMENT_NODE:
506 self.add_template(n)
507 def process_input(self,arg):
508 for f in self.fields.values():
509 if arg.has_key(f.name):
510 f.input=arg[f.name]
511 f.value=f.input
512 if f.trim:
513 f.input=f.input.strip()
514 f.validate(1,False)
515 if f.check==None:
516 continue
517 elif callable(f.check):
518 pass
519 elif isinstance(f.check,str):
520 v=f.check
521 if f.check=="email":
522 v=r"/^[^@#!& ]+@[A-Za-z0-9-][.A-Za-z0-9-]{0,64}\.[A-Za-z]{2,5}$/"
523 if f.check=="date":
524 v=r"/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/"
525 if not re.match(v[1:-1],f.input):
526 f.validate(0,False)
527 else:
528 f.value=f.default
529 self.update()
530 def validate_all(self,val=1):
531 for f in self.fields.values():
532 f.validate(val,0)
533 self.update()
534 def invalidate_all(self):
535 self.validate_all(0)
536 def update(self):
537 self.submitted=True
538 self.valid=True
539 self.errors=[]
540 for f in self.fields.values():
541 if f.required and f.input==None:
542 self.submitted=False
543 self.valid=False
544 self.missing.append(f.name)
545 if f.invalid:
546 self.valid=False
547 self.error.append(f.name)
548 # invalid have been submitted and
549 self.invalid=self.submitted and self.valid==False
550 def collect(self):
551 d={}
552 for f in self.fields.values():
553 d[f.name]=f.value
554 return d
555class QWebURLEval(QWebEval):
556 def __init__(self,data):
557 QWebEval.__init__(self,data)
558 def __getitem__(self,expr):
559 r=QWebEval.__getitem__(self,expr)
560 if isinstance(r,str):
561 return urllib.quote_plus(r)
562 else:
563 return r
564class QWebHtml(QWebXml):
565 """QWebHtml
566 QWebURL:
567 QWebField:
568 QWebForm:
569 QWebHtml:
570 an extended template engine, with a few utility class to easily produce
571 HTML, handle URLs and process forms, it adds the following magic attributes:
572
573 t-href t-action t-form-text t-form-password t-form-textarea t-form-radio
574 t-form-checkbox t-form-select t-option t-selected t-checked t-pager
575
576 # explication URL:
577 # v['tableurl']=QWebUrl({p=afdmin,saar=,orderby=,des=,mlink;meta_active=})
578 # t-href="tableurl?desc=1"
579 #
580 # explication FORM: t-if="form.valid()"
581 # Foreach i
582 # email: <input type="text" t-esc-name="i" t-esc-value="form[i].value" t-esc-class="form[i].css"/>
583 # <input type="radio" name="spamtype" t-esc-value="i" t-selected="i==form.f.spamtype.value"/>
584 # <option t-esc-value="cc" t-selected="cc==form.f.country.value"><t t-esc="cname"></option>
585 # Simple forms:
586 # <input t-form-text="form.email" t-check="email"/>
587 # <input t-form-password="form.email" t-check="email"/>
588 # <input t-form-radio="form.email" />
589 # <input t-form-checkbox="form.email" />
590 # <textarea t-form-textarea="form.email" t-check="email"/>
591 # <select t-form-select="form.email"/>
592 # <option t-value="1">
593 # <input t-form-radio="form.spamtype" t-value="1"/> Cars
594 # <input t-form-radio="form.spamtype" t-value="2"/> Sprt
595 """
596 # QWebForm from a template
597 def form(self,tname,arg=None,default=None):
598 form=QWebForm(self._t[tname],arg,default)
599 return form
600
601 # HTML Att
602 def eval_url(self,av,v):
603 s=QWebURLEval(v).eval_format(av)
604 a=s.split('?',1)
605 arg={}
606 if len(a)>1:
607 for k,v in cgi.parse_qsl(a[1],1):
608 arg[k]=v
609 b=a[0].split('/',1)
610 path=''
611 if len(b)>1:
612 path=b[1]
613 u=b[0]
614 return u,path,arg
615 def render_att_url_(self,e,an,av,v):
616 u,path,arg=self.eval_url(av,v)
617 if not isinstance(v.get(u,0),QWebURL):
618 out='qweb: missing url %r %r %r'%(u,path,arg)
619 else:
620 out=v[u].href(path,arg)
621 return ' %s="%s"'%(an[6:],cgi.escape(out,1))
622 def render_att_href(self,e,an,av,v):
623 return self.render_att_url_(e,"t-url-href",av,v)
624 def render_att_checked(self,e,an,av,v):
625 if self.eval_bool(av,v):
626 return ' %s="%s"'%(an[2:],an[2:])
627 else:
628 return ''
629 def render_att_selected(self,e,an,av,v):
630 return self.render_att_checked(e,an,av,v)
631
632 # HTML Tags forms
633 def render_tag_rawurl(self,e,t_att,g_att,v):
634 u,path,arg=self.eval_url(t_att["rawurl"],v)
635 return v[u].href(path,arg)
636 def render_tag_escurl(self,e,t_att,g_att,v):
637 u,path,arg=self.eval_url(t_att["escurl"],v)
638 return cgi.escape(v[u].href(path,arg))
639 def render_tag_action(self,e,t_att,g_att,v):
640 u,path,arg=self.eval_url(t_att["action"],v)
641 if not isinstance(v.get(u,0),QWebURL):
642 action,input=('qweb: missing url %r %r %r'%(u,path,arg),'')
643 else:
644 action,input=v[u].form(path,arg)
645 g_att+=' action="%s"'%action
646 return self.render_element(e,g_att,v,input)
647 def render_tag_form_text(self,e,t_att,g_att,v):
648 f=self.eval_object(t_att["form-text"],v)
649 g_att+=' type="text" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
650 return self.render_element(e,g_att,v)
651 def render_tag_form_password(self,e,t_att,g_att,v):
652 f=self.eval_object(t_att["form-password"],v)
653 g_att+=' type="password" name="%s" value="%s" class="%s"'%(f.name,cgi.escape(f.value,1),f.css)
654 return self.render_element(e,g_att,v)
655 def render_tag_form_textarea(self,e,t_att,g_att,v):
656 type="textarea"
657 f=self.eval_object(t_att["form-textarea"],v)
658 g_att+=' name="%s" class="%s"'%(f.name,f.css)
659 r="<%s%s>%s</%s>"%(type,g_att,cgi.escape(f.value,1),type)
660 return r
661 def render_tag_form_radio(self,e,t_att,g_att,v):
662 f=self.eval_object(t_att["form-radio"],v)
663 val=t_att["value"]
664 g_att+=' type="radio" name="%s" value="%s"'%(f.name,val)
665 if f.value==val:
666 g_att+=' checked="checked"'
667 return self.render_element(e,g_att,v)
668 def render_tag_form_checkbox(self,e,t_att,g_att,v):
669 f=self.eval_object(t_att["form-checkbox"],v)
670 val=t_att["value"]
671 g_att+=' type="checkbox" name="%s" value="%s"'%(f.name,val)
672 if f.value==val:
673 g_att+=' checked="checked"'
674 return self.render_element(e,g_att,v)
675 def render_tag_form_select(self,e,t_att,g_att,v):
676 f=self.eval_object(t_att["form-select"],v)
677 g_att+=' name="%s" class="%s"'%(f.name,f.css)
678 return self.render_element(e,g_att,v)
679 def render_tag_option(self,e,t_att,g_att,v):
680 f=self.eval_object(e.parentNode.getAttribute("t-form-select"),v)
681 val=t_att["option"]
682 g_att+=' value="%s"'%(val)
683 if f.value==val:
684 g_att+=' selected="selected"'
685 return self.render_element(e,g_att,v)
686
687 # HTML Tags others
688 def render_tag_pager(self,e,t_att,g_att,v):
689 pre=t_att["pager"]
690 total=int(self.eval_str(t_att["total"],v))
691 start=int(self.eval_str(t_att["start"],v))
692 step=int(self.eval_str(t_att.get("step","100"),v))
693 scope=int(self.eval_str(t_att.get("scope","5"),v))
694 # Compute Pager
695 p=pre+"_"
696 d={}
697 d[p+"tot_size"]=total
698 d[p+"tot_page"]=tot_page=total/step
699 d[p+"win_start0"]=total and start
700 d[p+"win_start1"]=total and start+1
701 d[p+"win_end0"]=max(0,min(start+step-1,total-1))
702 d[p+"win_end1"]=min(start+step,total)
703 d[p+"win_page0"]=win_page=start/step
704 d[p+"win_page1"]=win_page+1
705 d[p+"prev"]=(win_page!=0)
706 d[p+"prev_start"]=(win_page-1)*step
707 d[p+"next"]=(tot_page>=win_page+1)
708 d[p+"next_start"]=(win_page+1)*step
709 l=[]
710 begin=win_page-scope
711 end=win_page+scope
712 if begin<0:
713 end-=begin
714 if end>tot_page:
715 begin-=(end-tot_page)
716 i=max(0,begin)
717 while i<=min(end,tot_page) and total!=step:
718 l.append( { p+"page0":i, p+"page1":i+1, p+"start":i*step, p+"sel":(win_page==i) })
719 i+=1
720 d[p+"active"]=len(l)>1
721 d[p+"list"]=l
722 # Update v
723 v.update(d)
724 return ""
725
726#----------------------------------------------------------
727# QWeb Simple Controller
728#----------------------------------------------------------
729def qweb_control(self,jump='main',p=[]):
730 """ qweb_control(self,jump='main',p=[]):
731 A simple function to handle the controler part of your application. It
732 dispatch the control to the jump argument, while ensuring that prefix
733 function have been called.
734
735 qweb_control replace '/' to '_' and strip '_' from the jump argument.
736
737 name1
738 name1_name2
739 name1_name2_name3
740
741 """
742 jump=jump.replace('/','_').strip('_')
743 if not hasattr(self,jump):
744 return 0
745 done={}
746 todo=[]
747 while 1:
748 if jump!=None:
749 tmp=""
750 todo=[]
751 for i in jump.split("_"):
752 tmp+=i+"_";
753 if not done.has_key(tmp[:-1]):
754 todo.append(tmp[:-1])
755 jump=None
756 elif len(todo):
757 i=todo.pop(0)
758 done[i]=1
759 if hasattr(self,i):
760 f=getattr(self,i)
761 r=f(*p)
762 if isinstance(r,types.StringType):
763 jump=r
764 else:
765 break
766 return 1
767
768#----------------------------------------------------------
769# QWeb WSGI Request handler
770#----------------------------------------------------------
771class QWebSession(dict):
772 def __init__(self,environ,**kw):
773 dict.__init__(self)
774 default={
775 "path" : tempfile.gettempdir(),
776 "cookie_name" : "QWEBSID",
777 "cookie_lifetime" : 0,
778 "cookie_path" : '/',
779 "cookie_domain" : '',
780 "limit_cache" : 1,
781 "probability" : 0.01,
782 "maxlifetime" : 3600,
783 "disable" : 0,
784 }
785 for k,v in default.items():
786 setattr(self,'session_%s'%k,kw.get(k,v))
787 # Try to find session
788 self.session_found_cookie=0
789 self.session_found_url=0
790 self.session_found=0
791 self.session_orig=""
792 # Try cookie
793 c=Cookie.SimpleCookie()
794 c.load(environ.get('HTTP_COOKIE', ''))
795 if c.has_key(self.session_cookie_name):
796 sid=c[self.session_cookie_name].value[:64]
797 if re.match('[a-f0-9]+$',sid) and self.session_load(sid):
798 self.session_id=sid
799 self.session_found_cookie=1
800 self.session_found=1
801 # Try URL
802 if not self.session_found_cookie:
803 mo=re.search('&%s=([a-f0-9]+)'%self.session_cookie_name,environ.get('QUERY_STRING',''))
804 if mo and self.session_load(mo.group(1)):
805 self.session_id=mo.group(1)
806 self.session_found_url=1
807 self.session_found=1
808 # New session
809 if not self.session_found:
810 self.session_id='%032x'%random.randint(1,2**128)
811 self.session_trans_sid="&amp;%s=%s"%(self.session_cookie_name,self.session_id)
812 # Clean old session
813 if random.random() < self.session_probability:
814 self.session_clean()
815 def session_get_headers(self):
816 h=[]
817 if (not self.session_disable) and (len(self) or len(self.session_orig)):
818 self.session_save()
819 if not self.session_found_cookie:
820 c=Cookie.SimpleCookie()
821 c[self.session_cookie_name] = self.session_id
822 c[self.session_cookie_name]['path'] = self.session_cookie_path
823 if self.session_cookie_domain:
824 c[self.session_cookie_name]['domain'] = self.session_cookie_domain
825# if self.session_cookie_lifetime:
826# c[self.session_cookie_name]['expires'] = TODO date localtime or not, datetime.datetime(1970, 1, 1)
827 h.append(("Set-Cookie", c[self.session_cookie_name].OutputString()))
828 if self.session_limit_cache:
829 h.append(('Cache-Control','no-store, no-cache, must-revalidate, post-check=0, pre-check=0'))
830 h.append(('Expires','Thu, 19 Nov 1981 08:52:00 GMT'))
831 h.append(('Pragma','no-cache'))
832 return h
833 def session_load(self,sid):
834 fname=os.path.join(self.session_path,'qweb_sess_%s'%sid)
835 try:
836 orig=file(fname).read()
837 d=pickle.loads(orig)
838 except:
839 return
840 self.session_orig=orig
841 self.update(d)
842 return 1
843 def session_save(self):
844 if not os.path.isdir(self.session_path):
845 os.makedirs(self.session_path)
846 fname=os.path.join(self.session_path,'qweb_sess_%s'%self.session_id)
847 try:
848 oldtime=os.path.getmtime(fname)
849 except OSError,IOError:
850 oldtime=0
851 dump=pickle.dumps(self.copy())
852 if (dump != self.session_orig) or (time.time() > oldtime+self.session_maxlifetime/4):
853 tmpname=os.path.join(self.session_path,'qweb_sess_%s_%x'%(self.session_id,random.randint(1,2**32)))
854 f=file(tmpname,'wb')
855 f.write(dump)
856 f.close()
857 if sys.platform=='win32' and os.path.isfile(fname):
858 os.remove(fname)
859 os.rename(tmpname,fname)
860 def session_clean(self):
861 t=time.time()
862 try:
863 for i in [os.path.join(self.session_path,i) for i in os.listdir(self.session_path) if i.startswith('qweb_sess_')]:
864 if (t > os.path.getmtime(i)+self.session_maxlifetime):
865 os.unlink(i)
866 except OSError,IOError:
867 pass
868class QWebSessionMem(QWebSession):
869 def session_load(self,sid):
870 global _qweb_sessions
871 if not "_qweb_sessions" in globals():
872 _qweb_sessions={}
873 if _qweb_sessions.has_key(sid):
874 self.session_orig=_qweb_sessions[sid]
875 self.update(self.session_orig)
876 return 1
877 def session_save(self):
878 global _qweb_sessions
879 if not "_qweb_sessions" in globals():
880 _qweb_sessions={}
881 _qweb_sessions[self.session_id]=self.copy()
882class QWebSessionService:
883 def __init__(self, wsgiapp, url_rewrite=0):
884 self.wsgiapp=wsgiapp
885 self.url_rewrite_tags="a=href,area=href,frame=src,form=,fieldset="
886 def __call__(self, environ, start_response):
887 # TODO
888 # use QWebSession to provide environ["qweb.session"]
889 return self.wsgiapp(environ,start_response)
890class QWebDict(dict):
891 def __init__(self,*p):
892 dict.__init__(self,*p)
893 def __getitem__(self,key):
894 return self.get(key,"")
895 def int(self,key):
896 try:
897 return int(self.get(key,"0"))
898 except ValueError:
899 return 0
900class QWebListDict(dict):
901 def __init__(self,*p):
902 dict.__init__(self,*p)
903 def __getitem__(self,key):
904 return self.get(key,[])
905 def appendlist(self,key,val):
906 if self.has_key(key):
907 self[key].append(val)
908 else:
909 self[key]=[val]
910 def get_qwebdict(self):
911 d=QWebDict()
912 for k,v in self.items():
913 d[k]=v[-1]
914 return d
915class QWebRequest:
916 """QWebRequest a WSGI request handler.
917
918 QWebRequest is a WSGI request handler that feature GET, POST and POST
919 multipart methods, handles cookies and headers and provide a dict-like
920 SESSION Object (either on the filesystem or in memory).
921
922 It is constructed with the environ and start_response WSGI arguments:
923
924 req=qweb.QWebRequest(environ, start_response)
925
926 req has the folowing attributes :
927
928 req.environ standard WSGI dict (CGI and wsgi ones)
929
930 Some CGI vars as attributes from environ for convenience:
931
932 req.SCRIPT_NAME
933 req.PATH_INFO
934 req.REQUEST_URI
935
936 Some computed value (also for convenience)
937
938 req.FULL_URL full URL recontructed (http://host/query)
939 req.FULL_PATH (URL path before ?querystring)
940
941 Dict constructed from querystring and POST datas, PHP-like.
942
943 req.GET contains GET vars
944 req.POST contains POST vars
945 req.REQUEST contains merge of GET and POST
946 req.FILES contains uploaded files
947 req.GET_LIST req.POST_LIST req.REQUEST_LIST req.FILES_LIST multiple arguments versions
948 req.debug() returns an HTML dump of those vars
949
950 A dict-like session object.
951
952 req.SESSION the session start when the dict is not empty.
953
954 Attribute for handling the response
955
956 req.response_headers dict-like to set headers
957 req.response_cookies a SimpleCookie to set cookies
958 req.response_status a string to set the status like '200 OK'
959
960 req.write() to write to the buffer
961
962 req itselfs is an iterable object with the buffer, it will also also call
963 start_response automatically before returning anything via the iterator.
964
965 To make it short, it means that you may use
966
967 return req
968
969 at the end of your request handling to return the reponse to any WSGI
970 application server.
971 """
972 #
973 # This class contains part ripped from colubrid (with the permission of
974 # mitsuhiko) see http://wsgiarea.pocoo.org/colubrid/
975 #
976 # - the class HttpHeaders
977 # - the method load_post_data (tuned version)
978 #
979 class HttpHeaders(object):
980 def __init__(self):
981 self.data = [('Content-Type', 'text/html')]
982 def __setitem__(self, key, value):
983 self.set(key, value)
984 def __delitem__(self, key):
985 self.remove(key)
986 def __contains__(self, key):
987 key = key.lower()
988 for k, v in self.data:
989 if k.lower() == key:
990 return True
991 return False
992 def add(self, key, value):
993 self.data.append((key, value))
994 def remove(self, key, count=-1):
995 removed = 0
996 data = []
997 for _key, _value in self.data:
998 if _key.lower() != key.lower():
999 if count > -1:
1000 if removed >= count:
1001 break
1002 else:
1003 removed += 1
1004 data.append((_key, _value))
1005 self.data = data
1006 def clear(self):
1007 self.data = []
1008 def set(self, key, value):
1009 self.remove(key)
1010 self.add(key, value)
1011 def get(self, key=False, httpformat=False):
1012 if not key:
1013 result = self.data
1014 else:
1015 result = []
1016 for _key, _value in self.data:
1017 if _key.lower() == key.lower():
1018 result.append((_key, _value))
1019 if httpformat:
1020 return '\n'.join(['%s: %s' % item for item in result])
1021 return result
1022 def load_post_data(self,environ,POST,FILES):
1023 length = int(environ['CONTENT_LENGTH'])
1024 DATA = environ['wsgi.input'].read(length)
1025 if environ.get('CONTENT_TYPE', '').startswith('multipart'):
1026 lines = ['Content-Type: %s' % environ.get('CONTENT_TYPE', '')]
1027 for key, value in environ.items():
1028 if key.startswith('HTTP_'):
1029 lines.append('%s: %s' % (key, value))
1030 raw = '\r\n'.join(lines) + '\r\n\r\n' + DATA
1031 msg = email.message_from_string(raw)
1032 for sub in msg.get_payload():
1033 if not isinstance(sub, email.Message.Message):
1034 continue
1035 name_dict = cgi.parse_header(sub['Content-Disposition'])[1]
1036 if 'filename' in name_dict:
1037 # Nested MIME Messages are not supported'
1038 if type([]) == type(sub.get_payload()):
1039 continue
1040 if not name_dict['filename'].strip():
1041 continue
1042 filename = name_dict['filename']
1043 # why not keep all the filename? because IE always send 'C:\documents and settings\blub\blub.png'
1044 filename = filename[filename.rfind('\\') + 1:]
1045 if 'Content-Type' in sub:
1046 content_type = sub['Content-Type']
1047 else:
1048 content_type = None
1049 s = { "name":filename, "type":content_type, "data":sub.get_payload() }
1050 FILES.appendlist(name_dict['name'], s)
1051 else:
1052 POST.appendlist(name_dict['name'], sub.get_payload())
1053 else:
1054 POST.update(cgi.parse_qs(DATA,keep_blank_values=1))
1055 return DATA
1056
1057 def __init__(self,environ,start_response,session=QWebSession):
1058 self.environ=environ
1059 self.start_response=start_response
1060 self.buffer=[]
1061
1062 self.SCRIPT_NAME = environ.get('SCRIPT_NAME', '')
1063 self.PATH_INFO = environ.get('PATH_INFO', '')
1064 # extensions:
1065 self.FULL_URL = environ['FULL_URL'] = self.get_full_url(environ)
1066 # REQUEST_URI is optional, fake it if absent
1067 if not environ.has_key("REQUEST_URI"):
1068 environ["REQUEST_URI"]=urllib.quote(self.SCRIPT_NAME+self.PATH_INFO)
1069 if environ.get('QUERY_STRING'):
1070 environ["REQUEST_URI"]+='?'+environ['QUERY_STRING']
1071 self.REQUEST_URI = environ["REQUEST_URI"]
1072 # full quote url path before the ?
1073 self.FULL_PATH = environ['FULL_PATH'] = self.REQUEST_URI.split('?')[0]
1074
1075 self.request_cookies=Cookie.SimpleCookie()
1076 self.request_cookies.load(environ.get('HTTP_COOKIE', ''))
1077
1078 self.response_started=False
1079 self.response_gzencode=False
1080 self.response_cookies=Cookie.SimpleCookie()
1081 # to delete a cookie use: c[key]['expires'] = datetime.datetime(1970, 1, 1)
1082 self.response_headers=self.HttpHeaders()
1083 self.response_status="200 OK"
1084
1085 self.php=None
1086 if self.environ.has_key("php"):
1087 self.php=environ["php"]
1088 self.SESSION=self.php._SESSION
1089 self.GET=self.php._GET
1090 self.POST=self.php._POST
1091 self.REQUEST=self.php._ARG
1092 self.FILES=self.php._FILES
1093 else:
1094 if isinstance(session,QWebSession):
1095 self.SESSION=session
1096 elif session:
1097 self.SESSION=session(environ)
1098 else:
1099 self.SESSION=None
1100 self.GET_LIST=QWebListDict(cgi.parse_qs(environ.get('QUERY_STRING', ''),keep_blank_values=1))
1101 self.POST_LIST=QWebListDict()
1102 self.FILES_LIST=QWebListDict()
1103 self.REQUEST_LIST=QWebListDict(self.GET_LIST)
1104 if environ['REQUEST_METHOD'] == 'POST':
1105 self.DATA=self.load_post_data(environ,self.POST_LIST,self.FILES_LIST)
1106 self.REQUEST_LIST.update(self.POST_LIST)
1107 self.GET=self.GET_LIST.get_qwebdict()
1108 self.POST=self.POST_LIST.get_qwebdict()
1109 self.FILES=self.FILES_LIST.get_qwebdict()
1110 self.REQUEST=self.REQUEST_LIST.get_qwebdict()
1111 def get_full_url(environ):
1112 # taken from PEP 333
1113 if 'FULL_URL' in environ:
1114 return environ['FULL_URL']
1115 url = environ['wsgi.url_scheme']+'://'
1116 if environ.get('HTTP_HOST'):
1117 url += environ['HTTP_HOST']
1118 else:
1119 url += environ['SERVER_NAME']
1120 if environ['wsgi.url_scheme'] == 'https':
1121 if environ['SERVER_PORT'] != '443':
1122 url += ':' + environ['SERVER_PORT']
1123 else:
1124 if environ['SERVER_PORT'] != '80':
1125 url += ':' + environ['SERVER_PORT']
1126 if environ.has_key('REQUEST_URI'):
1127 url += environ['REQUEST_URI']
1128 else:
1129 url += urllib.quote(environ.get('SCRIPT_NAME', ''))
1130 url += urllib.quote(environ.get('PATH_INFO', ''))
1131 if environ.get('QUERY_STRING'):
1132 url += '?' + environ['QUERY_STRING']
1133 return url
1134 get_full_url=staticmethod(get_full_url)
1135 def save_files(self):
1136 for k,v in self.FILES.items():
1137 if not v.has_key("tmp_file"):
1138 f=tempfile.NamedTemporaryFile()
1139 f.write(v["data"])
1140 f.flush()
1141 v["tmp_file"]=f
1142 v["tmp_name"]=f.name
1143 def debug(self):
1144 body=''
1145 for name,d in [
1146 ("GET",self.GET), ("POST",self.POST), ("REQUEST",self.REQUEST), ("FILES",self.FILES),
1147 ("GET_LIST",self.GET_LIST), ("POST_LIST",self.POST_LIST), ("REQUEST_LIST",self.REQUEST_LIST), ("FILES_LIST",self.FILES_LIST),
1148 ("SESSION",self.SESSION), ("environ",self.environ),
1149 ]:
1150 body+='<table border="1" width="100%" align="center">\n'
1151 body+='<tr><th colspan="2" align="center">%s</th></tr>\n'%name
1152 keys=d.keys()
1153 keys.sort()
1154 body+=''.join(['<tr><td>%s</td><td>%s</td></tr>\n'%(k,cgi.escape(repr(d[k]))) for k in keys])
1155 body+='</table><br><br>\n\n'
1156 return body
1157 def write(self,s):
1158 self.buffer.append(s)
1159 def echo(self,*s):
1160 self.buffer.extend([str(i) for i in s])
1161 def response(self):
1162 if not self.response_started:
1163 if not self.php:
1164 for k,v in self.FILES.items():
1165 if v.has_key("tmp_file"):
1166 try:
1167 v["tmp_file"].close()
1168 except OSError:
1169 pass
1170 if self.response_gzencode and self.environ.get('HTTP_ACCEPT_ENCODING','').find('gzip')!=-1:
1171 zbuf=StringIO.StringIO()
1172 zfile=gzip.GzipFile(mode='wb', fileobj=zbuf)
1173 zfile.write(''.join(self.buffer))
1174 zfile.close()
1175 zbuf=zbuf.getvalue()
1176 self.buffer=[zbuf]
1177 self.response_headers['Content-Encoding']="gzip"
1178 self.response_headers['Content-Length']=str(len(zbuf))
1179 headers = self.response_headers.get()
1180 if isinstance(self.SESSION, QWebSession):
1181 headers.extend(self.SESSION.session_get_headers())
1182 headers.extend([('Set-Cookie', self.response_cookies[i].OutputString()) for i in self.response_cookies])
1183 self.start_response(self.response_status, headers)
1184 self.response_started=True
1185 return self.buffer
1186 def __iter__(self):
1187 return self.response().__iter__()
1188 def http_redirect(self,url,permanent=1):
1189 if permanent:
1190 self.response_status="301 Moved Permanently"
1191 else:
1192 self.response_status="302 Found"
1193 self.response_headers["Location"]=url
1194 def http_404(self,msg="<h1>404 Not Found</h1>"):
1195 self.response_status="404 Not Found"
1196 if msg:
1197 self.write(msg)
1198 def http_download(self,fname,fstr,partial=0):
1199# allow fstr to be a file-like object
1200# if parital:
1201# say accept ranages
1202# parse range headers...
1203# if range:
1204# header("HTTP/1.1 206 Partial Content");
1205# header("Content-Range: bytes $offset-".($fsize-1)."/".$fsize);
1206# header("Content-Length: ".($fsize-$offset));
1207# fseek($fd,$offset);
1208# else:
1209 self.response_headers["Content-Type"]="application/octet-stream"
1210 self.response_headers["Content-Disposition"]="attachment; filename=\"%s\""%fname
1211 self.response_headers["Content-Transfer-Encoding"]="binary"
1212 self.response_headers["Content-Length"]="%d"%len(fstr)
1213 self.write(fstr)
1214
1215#----------------------------------------------------------
1216# QWeb WSGI HTTP Server to run any WSGI app
1217# autorun, run an app as FCGI or CGI otherwise launch the server
1218#----------------------------------------------------------
1219class QWebWSGIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
1220 def log_message(self,*p):
1221 if self.server.log:
1222 return BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,*p)
1223 def address_string(self):
1224 return self.client_address[0]
1225 def start_response(self,status,headers):
1226 l=status.split(' ',1)
1227 self.send_response(int(l[0]),l[1])
1228 ctype_sent=0
1229 for i in headers:
1230 if i[0].lower()=="content-type":
1231 ctype_sent=1
1232 self.send_header(*i)
1233 if not ctype_sent:
1234 self.send_header("Content-type", "text/html")
1235 self.end_headers()
1236 return self.write
1237 def write(self,data):
1238 try:
1239 self.wfile.write(data)
1240 except (socket.error, socket.timeout),e:
1241 print e
1242 def bufferon(self):
1243 if not getattr(self,'wfile_buf',0):
1244 self.wfile_buf=1
1245 self.wfile_bak=self.wfile
1246 self.wfile=StringIO.StringIO()
1247 def bufferoff(self):
1248 if self.wfile_buf:
1249 buf=self.wfile
1250 self.wfile=self.wfile_bak
1251 self.write(buf.getvalue())
1252 self.wfile_buf=0
1253 def serve(self,type):
1254 path_info, parameters, query = urlparse.urlparse(self.path)[2:5]
1255 environ = {
1256 'wsgi.version': (1,0),
1257 'wsgi.url_scheme': 'http',
1258 'wsgi.input': self.rfile,
1259 'wsgi.errors': sys.stderr,
1260 'wsgi.multithread': 0,
1261 'wsgi.multiprocess': 0,
1262 'wsgi.run_once': 0,
1263 'REQUEST_METHOD': self.command,
1264 'SCRIPT_NAME': '',
1265 'QUERY_STRING': query,
1266 'CONTENT_TYPE': self.headers.get('Content-Type', ''),
1267 'CONTENT_LENGTH': self.headers.get('Content-Length', ''),
1268 'REMOTE_ADDR': self.client_address[0],
1269 'REMOTE_PORT': str(self.client_address[1]),
1270 'SERVER_NAME': self.server.server_address[0],
1271 'SERVER_PORT': str(self.server.server_address[1]),
1272 'SERVER_PROTOCOL': self.request_version,
1273 # extention
1274 'FULL_PATH': self.path,
1275 'qweb.mode': 'standalone',
1276 }
1277 if path_info:
1278 environ['PATH_INFO'] = urllib.unquote(path_info)
1279 for key, value in self.headers.items():
1280 environ['HTTP_' + key.upper().replace('-', '_')] = value
1281 # Hack to avoid may TCP packets
1282 self.bufferon()
1283 appiter=self.server.wsgiapp(environ, self.start_response)
1284 for data in appiter:
1285 self.write(data)
1286 self.bufferoff()
1287 self.bufferoff()
1288 def do_GET(self):
1289 self.serve('GET')
1290 def do_POST(self):
1291 self.serve('GET')
1292class QWebWSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
1293 """ QWebWSGIServer
1294 qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1)
1295 A WSGI HTTP server threaded or not and a function to automatically run your
1296 app according to the environement (either standalone, CGI or FastCGI).
1297
1298 This feature is called QWeb autorun. If you want to To use it on your
1299 application use the following lines at the end of the main application
1300 python file:
1301
1302 if __name__ == '__main__':
1303 qweb.qweb_wsgi_autorun(your_wsgi_app)
1304
1305 this function will select the approriate running mode according to the
1306 calling environement (http-server, FastCGI or CGI).
1307 """
1308 def __init__(self, wsgiapp, ip, port, threaded=1, log=1):
1309 BaseHTTPServer.HTTPServer.__init__(self, (ip, port), QWebWSGIHandler)
1310 self.wsgiapp = wsgiapp
1311 self.threaded = threaded
1312 self.log = log
1313 def process_request(self,*p):
1314 if self.threaded:
1315 return SocketServer.ThreadingMixIn.process_request(self,*p)
1316 else:
1317 return BaseHTTPServer.HTTPServer.process_request(self,*p)
1318def qweb_wsgi_autorun(wsgiapp,ip='127.0.0.1',port=8080,threaded=1,log=1,callback_ready=None):
1319 if sys.platform=='win32':
1320 fcgi=0
1321 else:
1322 fcgi=1
1323 sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
1324 try:
1325 sock.getpeername()
1326 except socket.error, e:
1327 if e[0] == errno.ENOTSOCK:
1328 fcgi=0
1329 if fcgi or os.environ.has_key('REQUEST_METHOD'):
1330 import fcgi
1331 fcgi.WSGIServer(wsgiapp,multithreaded=False).run()
1332 else:
1333 if log:
1334 print 'Serving on %s:%d'%(ip,port)
1335 s=QWebWSGIServer(wsgiapp,ip=ip,port=port,threaded=threaded,log=log)
1336 if callback_ready:
1337 callback_ready()
1338 try:
1339 s.serve_forever()
1340 except KeyboardInterrupt,e:
1341 sys.excepthook(*sys.exc_info())
1342
1343#----------------------------------------------------------
1344# Qweb Documentation
1345#----------------------------------------------------------
1346def qweb_doc():
1347 body=__doc__
1348 for i in [QWebXml ,QWebHtml ,QWebForm ,QWebURL ,qweb_control ,QWebRequest ,QWebSession ,QWebWSGIServer ,qweb_wsgi_autorun]:
1349 n=i.__name__
1350 d=i.__doc__
1351 body+='\n\n%s\n%s\n\n%s'%(n,'-'*len(n),d)
1352 return body
1353
1354 print qweb_doc()
1355
1356#
01357
=== added file 'tools/ajaxterm/sarissa.js'
--- tools/ajaxterm/sarissa.js 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/sarissa.js 2011-01-12 03:17:16 +0000
@@ -0,0 +1,647 @@
1/**
2 * ====================================================================
3 * About
4 * ====================================================================
5 * Sarissa is an ECMAScript library acting as a cross-browser wrapper for native XML APIs.
6 * The library supports Gecko based browsers like Mozilla and Firefox,
7 * Internet Explorer (5.5+ with MSXML3.0+), Konqueror, Safari and a little of Opera
8 * @version 0.9.6.1
9 * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
10 * ====================================================================
11 * Licence
12 * ====================================================================
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License version 2 or
15 * the GNU Lesser General Public License version 2.1 as published by
16 * the Free Software Foundation (your choice between the two).
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License or GNU Lesser General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * or GNU Lesser General Public License along with this program; if not,
25 * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
26 * or visit http://www.gnu.org
27 *
28 */
29/**
30 * <p>Sarissa is a utility class. Provides "static" methods for DOMDocument and
31 * XMLHTTP objects, DOM Node serializatrion to XML strings and other goodies.</p>
32 * @constructor
33 */
34function Sarissa(){};
35/** @private */
36Sarissa.PARSED_OK = "Document contains no parsing errors";
37/**
38 * Tells you whether transformNode and transformNodeToObject are available. This functionality
39 * is contained in sarissa_ieemu_xslt.js and is deprecated. If you want to control XSLT transformations
40 * use the XSLTProcessor
41 * @deprecated
42 * @type boolean
43 */
44Sarissa.IS_ENABLED_TRANSFORM_NODE = false;
45/**
46 * tells you whether XMLHttpRequest (or equivalent) is available
47 * @type boolean
48 */
49Sarissa.IS_ENABLED_XMLHTTP = false;
50/**
51 * tells you whether selectNodes/selectSingleNode is available
52 * @type boolean
53 */
54Sarissa.IS_ENABLED_SELECT_NODES = false;
55var _sarissa_iNsCounter = 0;
56var _SARISSA_IEPREFIX4XSLPARAM = "";
57var _SARISSA_HAS_DOM_IMPLEMENTATION = document.implementation && true;
58var _SARISSA_HAS_DOM_CREATE_DOCUMENT = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.createDocument;
59var _SARISSA_HAS_DOM_FEATURE = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.hasFeature;
60var _SARISSA_IS_MOZ = _SARISSA_HAS_DOM_CREATE_DOCUMENT && _SARISSA_HAS_DOM_FEATURE;
61var _SARISSA_IS_SAFARI = (navigator.userAgent && navigator.vendor && (navigator.userAgent.toLowerCase().indexOf("applewebkit") != -1 || navigator.vendor.indexOf("Apple") != -1));
62var _SARISSA_IS_IE = document.all && window.ActiveXObject && navigator.userAgent.toLowerCase().indexOf("msie") > -1 && navigator.userAgent.toLowerCase().indexOf("opera") == -1;
63if(!window.Node || !window.Node.ELEMENT_NODE){
64 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};
65};
66
67// IE initialization
68if(_SARISSA_IS_IE){
69 // for XSLT parameter names, prefix needed by IE
70 _SARISSA_IEPREFIX4XSLPARAM = "xsl:";
71 // used to store the most recent ProgID available out of the above
72 var _SARISSA_DOM_PROGID = "";
73 var _SARISSA_XMLHTTP_PROGID = "";
74 /**
75 * Called when the Sarissa_xx.js file is parsed, to pick most recent
76 * ProgIDs for IE, then gets destroyed.
77 * @param idList an array of MSXML PROGIDs from which the most recent will be picked for a given object
78 * @param enabledList an array of arrays where each array has two items; the index of the PROGID for which a certain feature is enabled
79 */
80 pickRecentProgID = function (idList, enabledList){
81 // found progID flag
82 var bFound = false;
83 for(var i=0; i < idList.length && !bFound; i++){
84 try{
85 var oDoc = new ActiveXObject(idList[i]);
86 o2Store = idList[i];
87 bFound = true;
88 for(var j=0;j<enabledList.length;j++)
89 if(i <= enabledList[j][1])
90 Sarissa["IS_ENABLED_"+enabledList[j][0]] = true;
91 }catch (objException){
92 // trap; try next progID
93 };
94 };
95 if (!bFound)
96 throw "Could not retreive a valid progID of Class: " + idList[idList.length-1]+". (original exception: "+e+")";
97 idList = null;
98 return o2Store;
99 };
100 // pick best available MSXML progIDs
101 _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]]);
102 _SARISSA_XMLHTTP_PROGID = pickRecentProgID(["Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"], [["XMLHTTP", 4]]);
103 _SARISSA_THREADEDDOM_PROGID = pickRecentProgID(["Msxml2.FreeThreadedDOMDocument.5.0", "MSXML2.FreeThreadedDOMDocument.4.0", "MSXML2.FreeThreadedDOMDocument.3.0"]);
104 _SARISSA_XSLTEMPLATE_PROGID = pickRecentProgID(["Msxml2.XSLTemplate.5.0", "Msxml2.XSLTemplate.4.0", "MSXML2.XSLTemplate.3.0"], [["XSLTPROC", 2]]);
105 // we dont need this anymore
106 pickRecentProgID = null;
107 //============================================
108 // Factory methods (IE)
109 //============================================
110 // see non-IE version
111 Sarissa.getDomDocument = function(sUri, sName){
112 var oDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
113 // if a root tag name was provided, we need to load it in the DOM
114 // object
115 if (sName){
116 // if needed, create an artifical namespace prefix the way Moz
117 // does
118 if (sUri){
119 oDoc.loadXML("<a" + _sarissa_iNsCounter + ":" + sName + " xmlns:a" + _sarissa_iNsCounter + "=\"" + sUri + "\" />");
120 // don't use the same prefix again
121 ++_sarissa_iNsCounter;
122 }
123 else
124 oDoc.loadXML("<" + sName + "/>");
125 };
126 return oDoc;
127 };
128 // see non-IE version
129 Sarissa.getParseErrorText = function (oDoc) {
130 var parseErrorText = Sarissa.PARSED_OK;
131 if(oDoc.parseError != 0){
132 parseErrorText = "XML Parsing Error: " + oDoc.parseError.reason +
133 "\nLocation: " + oDoc.parseError.url +
134 "\nLine Number " + oDoc.parseError.line + ", Column " +
135 oDoc.parseError.linepos +
136 ":\n" + oDoc.parseError.srcText +
137 "\n";
138 for(var i = 0; i < oDoc.parseError.linepos;i++){
139 parseErrorText += "-";
140 };
141 parseErrorText += "^\n";
142 };
143 return parseErrorText;
144 };
145 // see non-IE version
146 Sarissa.setXpathNamespaces = function(oDoc, sNsSet) {
147 oDoc.setProperty("SelectionLanguage", "XPath");
148 oDoc.setProperty("SelectionNamespaces", sNsSet);
149 };
150 /**
151 * Basic implementation of Mozilla's XSLTProcessor for IE.
152 * Reuses the same XSLT stylesheet for multiple transforms
153 * @constructor
154 */
155 XSLTProcessor = function(){
156 this.template = new ActiveXObject(_SARISSA_XSLTEMPLATE_PROGID);
157 this.processor = null;
158 };
159 /**
160 * Impoprts the given XSLT DOM and compiles it to a reusable transform
161 * @argument xslDoc The XSLT DOMDocument to import
162 */
163 XSLTProcessor.prototype.importStylesheet = function(xslDoc){
164 // convert stylesheet to free threaded
165 var converted = new ActiveXObject(_SARISSA_THREADEDDOM_PROGID);
166 converted.loadXML(xslDoc.xml);
167 this.template.stylesheet = converted;
168 this.processor = this.template.createProcessor();
169 // (re)set default param values
170 this.paramsSet = new Array();
171 };
172 /**
173 * Transform the given XML DOM
174 * @argument sourceDoc The XML DOMDocument to transform
175 * @return The transformation result as a DOM Document
176 */
177 XSLTProcessor.prototype.transformToDocument = function(sourceDoc){
178 this.processor.input = sourceDoc;
179 var outDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
180 this.processor.output = outDoc;
181 this.processor.transform();
182 return outDoc;
183 };
184 /**
185 * Set global XSLT parameter of the imported stylesheet
186 * @argument nsURI The parameter namespace URI
187 * @argument name The parameter base name
188 * @argument value The new parameter value
189 */
190 XSLTProcessor.prototype.setParameter = function(nsURI, name, value){
191 /* nsURI is optional but cannot be null */
192 if(nsURI){
193 this.processor.addParameter(name, value, nsURI);
194 }else{
195 this.processor.addParameter(name, value);
196 };
197 /* update updated params for getParameter */
198 if(!this.paramsSet[""+nsURI]){
199 this.paramsSet[""+nsURI] = new Array();
200 };
201 this.paramsSet[""+nsURI][name] = value;
202 };
203 /**
204 * Gets a parameter if previously set by setParameter. Returns null
205 * otherwise
206 * @argument name The parameter base name
207 * @argument value The new parameter value
208 * @return The parameter value if reviously set by setParameter, null otherwise
209 */
210 XSLTProcessor.prototype.getParameter = function(nsURI, name){
211 nsURI = nsURI || "";
212 if(nsURI in this.paramsSet && name in this.paramsSet[nsURI]){
213 return this.paramsSet[nsURI][name];
214 }else{
215 return null;
216 };
217 };
218}
219else{ /* end IE initialization, try to deal with real browsers now ;-) */
220 if(_SARISSA_HAS_DOM_CREATE_DOCUMENT){
221 /**
222 * <p>Ensures the document was loaded correctly, otherwise sets the
223 * parseError to -1 to indicate something went wrong. Internal use</p>
224 * @private
225 */
226 Sarissa.__handleLoad__ = function(oDoc){
227 if (!oDoc.documentElement || oDoc.documentElement.tagName == "parsererror")
228 oDoc.parseError = -1;
229 Sarissa.__setReadyState__(oDoc, 4);
230 };
231 /**
232 * <p>Attached by an event handler to the load event. Internal use.</p>
233 * @private
234 */
235 _sarissa_XMLDocument_onload = function(){
236 Sarissa.__handleLoad__(this);
237 };
238 /**
239 * <p>Sets the readyState property of the given DOM Document object.
240 * Internal use.</p>
241 * @private
242 * @argument oDoc the DOM Document object to fire the
243 * readystatechange event
244 * @argument iReadyState the number to change the readystate property to
245 */
246 Sarissa.__setReadyState__ = function(oDoc, iReadyState){
247 oDoc.readyState = iReadyState;
248 if (oDoc.onreadystatechange != null && typeof oDoc.onreadystatechange == "function")
249 oDoc.onreadystatechange();
250 };
251 Sarissa.getDomDocument = function(sUri, sName){
252 var oDoc = document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
253 oDoc.addEventListener("load", _sarissa_XMLDocument_onload, false);
254 return oDoc;
255 };
256 if(false && window.XMLDocument){
257 /**
258 * <p>Emulate IE's onreadystatechange attribute</p>
259 */
260 XMLDocument.prototype.onreadystatechange = null;
261 /**
262 * <p>Emulates IE's readyState property, which always gives an integer from 0 to 4:</p>
263 * <ul><li>1 == LOADING,</li>
264 * <li>2 == LOADED,</li>
265 * <li>3 == INTERACTIVE,</li>
266 * <li>4 == COMPLETED</li></ul>
267 */
268 XMLDocument.prototype.readyState = 0;
269 /**
270 * <p>Emulate IE's parseError attribute</p>
271 */
272 XMLDocument.prototype.parseError = 0;
273
274 // NOTE: setting async to false will only work with documents
275 // called over HTTP (meaning a server), not the local file system,
276 // unless you are using Moz 1.4+.
277 // BTW the try>catch block is for 1.4; I haven't found a way to check if
278 // the property is implemented without
279 // causing an error and I dont want to use user agent stuff for that...
280 var _SARISSA_SYNC_NON_IMPLEMENTED = false;// ("async" in XMLDocument.prototype) ? false: true;
281 /**
282 * <p>Keeps a handle to the original load() method. Internal use and only
283 * if Mozilla version is lower than 1.4</p>
284 * @private
285 */
286 XMLDocument.prototype._sarissa_load = XMLDocument.prototype.load;
287
288 /**
289 * <p>Overrides the original load method to provide synchronous loading for
290 * Mozilla versions prior to 1.4, using an XMLHttpRequest object (if
291 * async is set to false)</p>
292 * @returns the DOM Object as it was before the load() call (may be empty)
293 */
294 XMLDocument.prototype.load = function(sURI) {
295 var oDoc = document.implementation.createDocument("", "", null);
296 Sarissa.copyChildNodes(this, oDoc);
297 this.parseError = 0;
298 Sarissa.__setReadyState__(this, 1);
299 try {
300 if(this.async == false && _SARISSA_SYNC_NON_IMPLEMENTED) {
301 var tmp = new XMLHttpRequest();
302 tmp.open("GET", sURI, false);
303 tmp.send(null);
304 Sarissa.__setReadyState__(this, 2);
305 Sarissa.copyChildNodes(tmp.responseXML, this);
306 Sarissa.__setReadyState__(this, 3);
307 }
308 else {
309 this._sarissa_load(sURI);
310 };
311 }
312 catch (objException) {
313 this.parseError = -1;
314 }
315 finally {
316 if(this.async == false){
317 Sarissa.__handleLoad__(this);
318 };
319 };
320 return oDoc;
321 };
322
323
324 }//if(window.XMLDocument)
325 else if(document.implementation && document.implementation.hasFeature && document.implementation.hasFeature('LS', '3.0')){
326 Document.prototype.async = true;
327 Document.prototype.onreadystatechange = null;
328 Document.prototype.parseError = 0;
329 Document.prototype.load = function(sURI) {
330 var parser = document.implementation.createLSParser(this.async ? document.implementation.MODE_ASYNCHRONOUS : document.implementation.MODE_SYNCHRONOUS, null);
331 if(this.async){
332 var self = this;
333 parser.addEventListener("load",
334 function(e) {
335 self.readyState = 4;
336 Sarissa.copyChildNodes(e.newDocument, self.documentElement, false);
337 self.onreadystatechange.call();
338 },
339 false);
340 };
341 try {
342 var oDoc = parser.parseURI(sURI);
343 }
344 catch(e){
345 this.parseError = -1;
346 };
347 if(!this.async)
348 Sarissa.copyChildNodes(oDoc, this.documentElement, false);
349 return oDoc;
350 };
351 /**
352 * <p>Factory method to obtain a new DOM Document object</p>
353 * @argument sUri the namespace of the root node (if any)
354 * @argument sUri the local name of the root node (if any)
355 * @returns a new DOM Document
356 */
357 Sarissa.getDomDocument = function(sUri, sName){
358 return document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
359 };
360 };
361 };//if(_SARISSA_HAS_DOM_CREATE_DOCUMENT)
362};
363//==========================================
364// Common stuff
365//==========================================
366if(!window.DOMParser){
367 /*
368 * DOMParser is a utility class, used to construct DOMDocuments from XML strings
369 * @constructor
370 */
371 DOMParser = function() {
372 };
373 if(_SARISSA_IS_SAFARI){
374 /**
375 * Construct a new DOM Document from the given XMLstring
376 * @param sXml the given XML string
377 * @param contentType the content type of the document the given string represents (one of text/xml, application/xml, application/xhtml+xml).
378 * @return a new DOM Document from the given XML string
379 */
380 DOMParser.prototype.parseFromString = function(sXml, contentType){
381 if(contentType.toLowerCase() != "application/xml"){
382 throw "Cannot handle content type: \"" + contentType + "\"";
383 };
384 var xmlhttp = new XMLHttpRequest();
385 xmlhttp.open("GET", "data:text/xml;charset=utf-8," + encodeURIComponent(str), false);
386 xmlhttp.send(null);
387 return xmlhttp.responseXML;
388 };
389 }else if(Sarissa.getDomDocument && Sarissa.getDomDocument() && "loadXML" in Sarissa.getDomDocument()){
390 DOMParser.prototype.parseFromString = function(sXml, contentType){
391 var doc = Sarissa.getDomDocument();
392 doc.loadXML(sXml);
393 return doc;
394 };
395 };
396};
397
398if(window.XMLHttpRequest){
399 Sarissa.IS_ENABLED_XMLHTTP = true;
400}
401else if(_SARISSA_IS_IE){
402 /**
403 * Emulate XMLHttpRequest
404 * @constructor
405 */
406 XMLHttpRequest = function() {
407 return new ActiveXObject(_SARISSA_XMLHTTP_PROGID);
408 };
409 Sarissa.IS_ENABLED_XMLHTTP = true;
410};
411
412if(!window.document.importNode && _SARISSA_IS_IE){
413 try{
414 /**
415 * Implements importNode for the current window document in IE using innerHTML.
416 * Testing showed that DOM was multiple times slower than innerHTML for this,
417 * sorry folks. If you encounter trouble (who knows what IE does behind innerHTML)
418 * please gimme a call.
419 * @param oNode the Node to import
420 * @param bChildren whether to include the children of oNode
421 * @returns the imported node for further use
422 */
423 window.document.importNode = function(oNode, bChildren){
424 var importNode = document.createElement("div");
425 if(bChildren)
426 importNode.innerHTML = Sarissa.serialize(oNode);
427 else
428 importNode.innerHTML = Sarissa.serialize(oNode.cloneNode(false));
429 return importNode.firstChild;
430 };
431 }catch(e){};
432};
433if(!Sarissa.getParseErrorText){
434 /**
435 * <p>Returns a human readable description of the parsing error. Usefull
436 * for debugging. Tip: append the returned error string in a &lt;pre&gt;
437 * element if you want to render it.</p>
438 * <p>Many thanks to Christian Stocker for the initial patch.</p>
439 * @argument oDoc The target DOM document
440 * @returns The parsing error description of the target Document in
441 * human readable form (preformated text)
442 */
443 Sarissa.getParseErrorText = function (oDoc){
444 var parseErrorText = Sarissa.PARSED_OK;
445 if(oDoc && oDoc.parseError && oDoc.parseError != 0){
446 /*moz*/
447 if(oDoc.documentElement.tagName == "parsererror"){
448 parseErrorText = oDoc.documentElement.firstChild.data;
449 parseErrorText += "\n" + oDoc.documentElement.firstChild.nextSibling.firstChild.data;
450 }/*konq*/
451 else{
452 parseErrorText = Sarissa.getText(oDoc.documentElement);/*.getElementsByTagName("h1")[0], false) + "\n";
453 parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("body")[0], false) + "\n";
454 parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("pre")[0], false);*/
455 };
456 };
457 return parseErrorText;
458 };
459};
460Sarissa.getText = function(oNode, deep){
461 var s = "";
462 var nodes = oNode.childNodes;
463 for(var i=0; i < nodes.length; i++){
464 var node = nodes[i];
465 var nodeType = node.nodeType;
466 if(nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE){
467 s += node.data;
468 }else if(deep == true
469 && (nodeType == Node.ELEMENT_NODE
470 || nodeType == Node.DOCUMENT_NODE
471 || nodeType == Node.DOCUMENT_FRAGMENT_NODE)){
472 s += Sarissa.getText(node, true);
473 };
474 };
475 return s;
476};
477if(window.XMLSerializer){
478 /**
479 * <p>Factory method to obtain the serialization of a DOM Node</p>
480 * @returns the serialized Node as an XML string
481 */
482 Sarissa.serialize = function(oDoc){
483 var s = null;
484 if(oDoc){
485 s = oDoc.innerHTML?oDoc.innerHTML:(new XMLSerializer()).serializeToString(oDoc);
486 };
487 return s;
488 };
489}else{
490 if(Sarissa.getDomDocument && (Sarissa.getDomDocument("","foo", null)).xml){
491 // see non-IE version
492 Sarissa.serialize = function(oDoc) {
493 var s = null;
494 if(oDoc){
495 s = oDoc.innerHTML?oDoc.innerHTML:oDoc.xml;
496 };
497 return s;
498 };
499 /**
500 * Utility class to serialize DOM Node objects to XML strings
501 * @constructor
502 */
503 XMLSerializer = function(){};
504 /**
505 * Serialize the given DOM Node to an XML string
506 * @param oNode the DOM Node to serialize
507 */
508 XMLSerializer.prototype.serializeToString = function(oNode) {
509 return oNode.xml;
510 };
511 };
512};
513
514/**
515 * strips tags from a markup string
516 */
517Sarissa.stripTags = function (s) {
518 return s.replace(/<[^>]+>/g,"");
519};
520/**
521 * <p>Deletes all child nodes of the given node</p>
522 * @argument oNode the Node to empty
523 */
524Sarissa.clearChildNodes = function(oNode) {
525 // need to check for firstChild due to opera 8 bug with hasChildNodes
526 while(oNode.firstChild){
527 oNode.removeChild(oNode.firstChild);
528 };
529};
530/**
531 * <p> Copies the childNodes of nodeFrom to nodeTo</p>
532 * <p> <b>Note:</b> The second object's original content is deleted before
533 * the copy operation, unless you supply a true third parameter</p>
534 * @argument nodeFrom the Node to copy the childNodes from
535 * @argument nodeTo the Node to copy the childNodes to
536 * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is false
537 */
538Sarissa.copyChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
539 if((!nodeFrom) || (!nodeTo)){
540 throw "Both source and destination nodes must be provided";
541 };
542 if(!bPreserveExisting){
543 Sarissa.clearChildNodes(nodeTo);
544 };
545 var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
546 var nodes = nodeFrom.childNodes;
547 if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
548 for(var i=0;i < nodes.length;i++) {
549 nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
550 };
551 }
552 else{
553 for(var i=0;i < nodes.length;i++) {
554 nodeTo.appendChild(nodes[i].cloneNode(true));
555 };
556 };
557};
558
559/**
560 * <p> Moves the childNodes of nodeFrom to nodeTo</p>
561 * <p> <b>Note:</b> The second object's original content is deleted before
562 * the move operation, unless you supply a true third parameter</p>
563 * @argument nodeFrom the Node to copy the childNodes from
564 * @argument nodeTo the Node to copy the childNodes to
565 * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is
566 */
567Sarissa.moveChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
568 if((!nodeFrom) || (!nodeTo)){
569 throw "Both source and destination nodes must be provided";
570 };
571 if(!bPreserveExisting){
572 Sarissa.clearChildNodes(nodeTo);
573 };
574 var nodes = nodeFrom.childNodes;
575 // if within the same doc, just move, else copy and delete
576 if(nodeFrom.ownerDocument == nodeTo.ownerDocument){
577 while(nodeFrom.firstChild){
578 nodeTo.appendChild(nodeFrom.firstChild);
579 };
580 }else{
581 var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
582 if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
583 for(var i=0;i < nodes.length;i++) {
584 nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
585 };
586 }else{
587 for(var i=0;i < nodes.length;i++) {
588 nodeTo.appendChild(nodes[i].cloneNode(true));
589 };
590 };
591 Sarissa.clearChildNodes(nodeFrom);
592 };
593};
594
595/**
596 * <p>Serialize any object to an XML string. All properties are serialized using the property name
597 * as the XML element name. Array elements are rendered as <code>array-item</code> elements,
598 * using their index/key as the value of the <code>key</code> attribute.</p>
599 * @argument anyObject the object to serialize
600 * @argument objectName a name for that object
601 * @return the XML serializationj of the given object as a string
602 */
603Sarissa.xmlize = function(anyObject, objectName, indentSpace){
604 indentSpace = indentSpace?indentSpace:'';
605 var s = indentSpace + '<' + objectName + '>';
606 var isLeaf = false;
607 if(!(anyObject instanceof Object) || anyObject instanceof Number || anyObject instanceof String
608 || anyObject instanceof Boolean || anyObject instanceof Date){
609 s += Sarissa.escape(""+anyObject);
610 isLeaf = true;
611 }else{
612 s += "\n";
613 var itemKey = '';
614 var isArrayItem = anyObject instanceof Array;
615 for(var name in anyObject){
616 s += Sarissa.xmlize(anyObject[name], (isArrayItem?"array-item key=\""+name+"\"":name), indentSpace + " ");
617 };
618 s += indentSpace;
619 };
620 return s += (objectName.indexOf(' ')!=-1?"</array-item>\n":"</" + objectName + ">\n");
621};
622
623/**
624 * Escape the given string chacters that correspond to the five predefined XML entities
625 * @param sXml the string to escape
626 */
627Sarissa.escape = function(sXml){
628 return sXml.replace(/&/g, "&amp;")
629 .replace(/</g, "&lt;")
630 .replace(/>/g, "&gt;")
631 .replace(/"/g, "&quot;")
632 .replace(/'/g, "&apos;");
633};
634
635/**
636 * Unescape the given string. This turns the occurences of the predefined XML
637 * entities to become the characters they represent correspond to the five predefined XML entities
638 * @param sXml the string to unescape
639 */
640Sarissa.unescape = function(sXml){
641 return sXml.replace(/&apos;/g,"'")
642 .replace(/&quot;/g,"\"")
643 .replace(/&gt;/g,">")
644 .replace(/&lt;/g,"<")
645 .replace(/&amp;/g,"&");
646};
647// EOF
0648
=== added file 'tools/ajaxterm/sarissa_dhtml.js'
--- tools/ajaxterm/sarissa_dhtml.js 1970-01-01 00:00:00 +0000
+++ tools/ajaxterm/sarissa_dhtml.js 2011-01-12 03:17:16 +0000
@@ -0,0 +1,105 @@
1/**
2 * ====================================================================
3 * About
4 * ====================================================================
5 * Sarissa cross browser XML library - AJAX module
6 * @version 0.9.6.1
7 * @author: Copyright Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
8 *
9 * This module contains some convinient AJAX tricks based on Sarissa
10 *
11 * ====================================================================
12 * Licence
13 * ====================================================================
14 * This program is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License version 2 or
16 * the GNU Lesser General Public License version 2.1 as published by
17 * the Free Software Foundation (your choice between the two).
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License or GNU Lesser General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * or GNU Lesser General Public License along with this program; if not,
26 * write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
27 * or visit http://www.gnu.org
28 *
29 */
30/**
31 * Update an element with response of a GET request on the given URL.
32 * @addon
33 * @param sFromUrl the URL to make the request to
34 * @param oTargetElement the element to update
35 * @param xsltproc (optional) the transformer to use on the returned
36 * content before updating the target element with it
37 */
38Sarissa.updateContentFromURI = function(sFromUrl, oTargetElement, xsltproc) {
39 try{
40 oTargetElement.style.cursor = "wait";
41 var xmlhttp = new XMLHttpRequest();
42 xmlhttp.open("GET", sFromUrl);
43 function sarissa_dhtml_loadHandler() {
44 if (xmlhttp.readyState == 4) {
45 oTargetElement.style.cursor = "auto";
46 Sarissa.updateContentFromNode(xmlhttp.responseXML, oTargetElement, xsltproc);
47 };
48 };
49 xmlhttp.onreadystatechange = sarissa_dhtml_loadHandler;
50 xmlhttp.send(null);
51 oTargetElement.style.cursor = "auto";
52 }
53 catch(e){
54 oTargetElement.style.cursor = "auto";
55 throw e;
56 };
57};
58
59/**
60 * Update an element's content with the given DOM node.
61 * @addon
62 * @param sFromUrl the URL to make the request to
63 * @param oTargetElement the element to update
64 * @param xsltproc (optional) the transformer to use on the given
65 * DOM node before updating the target element with it
66 */
67Sarissa.updateContentFromNode = function(oNode, oTargetElement, xsltproc) {
68 try {
69 oTargetElement.style.cursor = "wait";
70 Sarissa.clearChildNodes(oTargetElement);
71 // check for parsing errors
72 var ownerDoc = oNode.nodeType == Node.DOCUMENT_NODE?oNode:oNode.ownerDocument;
73 if(ownerDoc.parseError && ownerDoc.parseError != 0) {
74 var pre = document.createElement("pre");
75 pre.appendChild(document.createTextNode(Sarissa.getParseErrorText(ownerDoc)));
76 oTargetElement.appendChild(pre);
77 }
78 else {
79 // transform if appropriate
80 if(xsltproc) {
81 oNode = xsltproc.transformToDocument(oNode);
82 };
83 // be smart, maybe the user wants to display the source instead
84 if(oTargetElement.tagName.toLowerCase == "textarea" || oTargetElement.tagName.toLowerCase == "input") {
85 oTargetElement.value = Sarissa.serialize(oNode);
86 }
87 else {
88 // ok that was not smart; it was paranoid. Keep up the good work by trying to use DOM instead of innerHTML
89 if(oNode.nodeType == Node.DOCUMENT_NODE || oNode.ownerDocument.documentElement == oNode) {
90 oTargetElement.innerHTML = Sarissa.serialize(oNode);
91 }
92 else{
93 oTargetElement.appendChild(oTargetElement.ownerDocument.importNode(oNode, true));
94 };
95 };
96 };
97 }
98 catch(e) {
99 throw e;
100 }
101 finally{
102 oTargetElement.style.cursor = "auto";
103 };
104};
105
0106
=== added file 'tools/euca-get-ajax-console'
--- tools/euca-get-ajax-console 1970-01-01 00:00:00 +0000
+++ tools/euca-get-ajax-console 2011-01-12 03:17:16 +0000
@@ -0,0 +1,164 @@
1#!/usr/bin/env python
2# pylint: disable-msg=C0103
3# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
5# Copyright 2010 United States Government as represented by the
6# Administrator of the National Aeronautics and Space Administration.
7# All Rights Reserved.
8#
9# Licensed under the Apache License, Version 2.0 (the "License");
10# you may not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20
21"""Euca add-on to use ajax console"""
22
23import getopt
24import os
25import sys
26
27# If ../nova/__init__.py exists, add ../ to Python search path, so that
28# it will override what happens to be installed in /usr/(local/)lib/python...
29possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
30 os.pardir,
31 os.pardir))
32if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
33 sys.path.insert(0, possible_topdir)
34
35import boto
36import nova
37from boto.ec2.connection import EC2Connection
38from euca2ools import Euca2ool, InstanceValidationError, Util, ConnectionFailed
39
40usage_string = """
41Retrieves a url to an ajax console terminal
42
43euca-get-ajax-console [-h, --help] [--version] [--debug] instance_id
44
45REQUIRED PARAMETERS
46
47instance_id: unique identifier for the instance show the console output for.
48
49OPTIONAL PARAMETERS
50
51"""
52
53
54# This class extends boto to add AjaxConsole functionality
55class NovaEC2Connection(EC2Connection):
56
57 def get_ajax_console(self, instance_id):
58 """
59 Retrieves a console connection for the specified instance.
60
61 :type instance_id: string
62 :param instance_id: The instance ID of a running instance on the cloud.
63
64 :rtype: :class:`AjaxConsole`
65 """
66
67 class AjaxConsole:
68 def __init__(self, parent=None):
69 self.parent = parent
70 self.instance_id = None
71 self.url = None
72
73 def startElement(self, name, attrs, connection):
74 return None
75
76 def endElement(self, name, value, connection):
77 if name == 'instanceId':
78 self.instance_id = value
79 elif name == 'url':
80 self.url = value
81 else:
82 setattr(self, name, value)
83
84 params = {}
85 self.build_list_params(params, [instance_id], 'InstanceId')
86 return self.get_object('GetAjaxConsole', params, AjaxConsole)
87 pass
88
89
90def override_connect_ec2(aws_access_key_id=None,
91 aws_secret_access_key=None, **kwargs):
92 return NovaEC2Connection(aws_access_key_id,
93 aws_secret_access_key, **kwargs)
94
95# override boto's connect_ec2 method, so that we can use NovaEC2Connection
96boto.connect_ec2 = override_connect_ec2
97
98
99def usage(status=1):
100 print usage_string
101 Util().usage()
102 sys.exit(status)
103
104
105def version():
106 print Util().version()
107 sys.exit()
108
109
110def display_console_output(console_output):
111 print console_output.instance_id
112 print console_output.timestamp
113 print console_output.output
114
115
116def display_ajax_console_output(console_output):
117 print console_output.url
118
119
120def main():
121 try:
122 euca = Euca2ool()
123 except Exception, e:
124 print e
125 usage()
126
127 instance_id = None
128
129 for name, value in euca.opts:
130 if name in ('-h', '--help'):
131 usage(0)
132 elif name == '--version':
133 version()
134 elif name == '--debug':
135 debug = True
136
137 for arg in euca.args:
138 instance_id = arg
139 break
140
141 if instance_id:
142 try:
143 euca.validate_instance_id(instance_id)
144 except InstanceValidationError:
145 print 'Invalid instance id'
146 sys.exit(1)
147
148 try:
149 euca_conn = euca.make_connection()
150 except ConnectionFailed, e:
151 print e.message
152 sys.exit(1)
153 try:
154 console_output = euca_conn.get_ajax_console(instance_id)
155 except Exception, ex:
156 euca.display_error_and_exit('%s' % ex)
157
158 display_ajax_console_output(console_output)
159 else:
160 print 'instance_id must be specified'
161 usage()
162
163if __name__ == "__main__":
164 main()